refactor: update error handling, code formatting, and configuration field tags across agent sandbox, tools, and config files.

This commit is contained in:
0x5487 2026-02-23 07:56:49 +08:00
parent bd8af1ba1e
commit c81f3d6a88
16 changed files with 144 additions and 69 deletions

View file

@ -127,7 +127,7 @@ func TestNewAgentInstance_ReadOnlyContainerOmitsWriteTools(t *testing.T) {
}
}
writeRes := agent.Tools.Execute(context.Background(), "write_file", map[string]interface{}{
writeRes := agent.Tools.Execute(context.Background(), "write_file", map[string]any{
"path": "a.txt",
"content": "hello",
})

View file

@ -696,7 +696,14 @@ func (al *AgentLoop) runLLMIteration(
}
toolCtx := sandbox.WithSessionKey(ctx, opts.SessionKey)
toolResult := agent.Tools.ExecuteWithContext(toolCtx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback)
toolResult := agent.Tools.ExecuteWithContext(
toolCtx,
tc.Name,
tc.Arguments,
opts.Channel,
opts.ChatID,
asyncCallback,
)
// Send ForUser content to user immediately if not Silent
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {

View file

@ -23,6 +23,7 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-units"
"github.com/sipeed/picoclaw/pkg/config"
)
@ -192,7 +193,11 @@ func (c *ContainerSandbox) Exec(ctx context.Context, req ExecRequest) (*ExecResu
})
}
func (c *ContainerSandbox) ExecStream(ctx context.Context, req ExecRequest, onEvent func(ExecEvent) error) (*ExecResult, error) {
func (c *ContainerSandbox) ExecStream(
ctx context.Context,
req ExecRequest,
onEvent func(ExecEvent) error,
) (*ExecResult, error) {
if c.startErr != nil {
return nil, c.startErr
}
@ -240,7 +245,8 @@ func (c *ContainerSandbox) ExecStream(ctx context.Context, req ExecRequest, onEv
onEvent: onEvent,
buffer: &stderr,
}
if _, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, attach.Reader); err != nil && err != io.EOF {
_, err = stdcopy.StdCopy(stdoutWriter, stderrWriter, attach.Reader)
if err != nil && err != io.EOF {
if execCtx.Err() != nil {
return nil, execCtx.Err()
}
@ -578,7 +584,7 @@ func (f *containerFS) WriteFile(ctx context.Context, p string, data []byte, mkdi
tw := tar.NewWriter(&buf)
if err := tw.WriteHeader(&tar.Header{
Name: base,
Mode: 0644,
Mode: 0o644,
Size: int64(len(data)),
}); err != nil {
_ = tw.Close()

View file

@ -27,7 +27,8 @@ func TestContainerSandbox_Integration_ExecReadWrite(t *testing.T) {
}
defer cli.Close()
if _, err := cli.Ping(ctx); err != nil {
_, err = cli.Ping(ctx)
if err != nil {
t.Skipf("docker daemon unavailable: %v", err)
}
@ -43,7 +44,8 @@ func TestContainerSandbox_Integration_ExecReadWrite(t *testing.T) {
ContainerName: containerName,
Workspace: workspace,
})
if err := sb.Start(ctx); err != nil {
err = sb.Start(ctx)
if err != nil {
t.Fatalf("sandbox start failed: %v", err)
}
defer func() {
@ -54,7 +56,8 @@ func TestContainerSandbox_Integration_ExecReadWrite(t *testing.T) {
}()
content := []byte("hello from integration test")
if err := sb.Fs().WriteFile(ctx, "it/write.txt", content, true); err != nil {
err = sb.Fs().WriteFile(ctx, "it/write.txt", content, true)
if err != nil {
t.Fatalf("write file failed: %v", err)
}
@ -116,7 +119,8 @@ func TestContainerSandbox_Integration_WriteFileMkdirInContainerTmp(t *testing.T)
}
defer cli.Close()
if _, err := cli.Ping(ctx); err != nil {
_, err = cli.Ping(ctx)
if err != nil {
t.Skipf("docker daemon unavailable: %v", err)
}
@ -130,7 +134,8 @@ func TestContainerSandbox_Integration_WriteFileMkdirInContainerTmp(t *testing.T)
Image: image,
ContainerName: containerName,
})
if err := sb.Start(ctx); err != nil {
err = sb.Start(ctx)
if err != nil {
t.Fatalf("sandbox start failed: %v", err)
}
defer func() {
@ -141,7 +146,8 @@ func TestContainerSandbox_Integration_WriteFileMkdirInContainerTmp(t *testing.T)
}()
content := []byte("mkdir path works")
if err := sb.Fs().WriteFile(ctx, "/workspace/it_mkdir/nested/file.txt", content, true); err != nil {
err = sb.Fs().WriteFile(ctx, "/workspace/it_mkdir/nested/file.txt", content, true)
if err != nil {
t.Fatalf("write with mkdir failed: %v", err)
}

View file

@ -377,12 +377,21 @@ func TestParseByteLimitAndHostConfig(t *testing.T) {
t.Fatalf("unexpected pids limit: %#v", hc.Resources.PidsLimit)
}
if hc.Memory <= 0 || hc.MemorySwap <= 0 || hc.NanoCPUs <= 0 {
t.Fatalf("expected memory/swap/cpu limits set, got mem=%d swap=%d cpu=%d", hc.Memory, hc.MemorySwap, hc.NanoCPUs)
t.Fatalf(
"expected memory/swap/cpu limits set, got mem=%d swap=%d cpu=%d",
hc.Memory,
hc.MemorySwap,
hc.NanoCPUs,
)
}
if len(hc.Tmpfs) != 2 || hc.Tmpfs["/run"] != "" {
t.Fatalf("unexpected tmpfs map: %#v", hc.Tmpfs)
}
if got := strings.Join(hc.SecurityOpt, ","); !strings.Contains(got, "seccomp=sec-profile.json") || !strings.Contains(got, "apparmor=apparmor-profile") {
if got := strings.Join(
hc.SecurityOpt,
",",
); !strings.Contains(got, "seccomp=sec-profile.json") ||
!strings.Contains(got, "apparmor=apparmor-profile") {
t.Fatalf("security options missing expected profiles: %v", hc.SecurityOpt)
}
if len(hc.Resources.Ulimits) != 2 {

View file

@ -63,7 +63,11 @@ func (h *HostSandbox) Exec(ctx context.Context, req ExecRequest) (*ExecResult, e
})
}
func (h *HostSandbox) ExecStream(ctx context.Context, req ExecRequest, onEvent func(ExecEvent) error) (*ExecResult, error) {
func (h *HostSandbox) ExecStream(
ctx context.Context,
req ExecRequest,
onEvent func(ExecEvent) error,
) (*ExecResult, error) {
if strings.TrimSpace(req.Command) == "" {
return nil, fmt.Errorf("empty command")
}
@ -225,11 +229,11 @@ func (h *hostFS) WriteFile(ctx context.Context, path string, data []byte, mkdir
return err
}
if mkdir {
if err := os.MkdirAll(filepath.Dir(resolved), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(resolved), 0o755); err != nil {
return err
}
}
return os.WriteFile(resolved, data, 0644)
return os.WriteFile(resolved, data, 0o644)
}
relPath, err := h.getSafeRelPath(path)
@ -239,12 +243,12 @@ func (h *hostFS) WriteFile(ctx context.Context, path string, data []byte, mkdir
if mkdir {
// MkdirAll natively resolves inside os.Root to avoid escapes.
if err := h.root.MkdirAll(filepath.Dir(relPath), 0755); err != nil {
if err := h.root.MkdirAll(filepath.Dir(relPath), 0o755); err != nil {
return err
}
}
// Uses OS-level guarantees to restrict the file writing within the root descriptor.
return h.root.WriteFile(relPath, data, 0644)
return h.root.WriteFile(relPath, data, 0o644)
}
// validatePath ensures the given path is within the workspace if restrict is true but does not ensure atomic TOCTOU protection.

View file

@ -61,7 +61,8 @@ func TestHostSandbox_ExecAndFs(t *testing.T) {
t.Fatalf("expected working dir restriction error, got: %v", err)
}
if err := sb.Fs().WriteFile(context.Background(), "dir/a.txt", []byte("x"), true); err != nil {
err = sb.Fs().WriteFile(context.Background(), "dir/a.txt", []byte("x"), true)
if err != nil {
t.Fatalf("WriteFile() error: %v", err)
}
b, err := sb.Fs().ReadFile(context.Background(), "dir/a.txt")
@ -285,7 +286,8 @@ func TestHostFS_ReadFileWriteFile_WithoutWorkspaceOrRoot(t *testing.T) {
// Test case where root is nil explicitly
sb2 := NewHostSandbox(root, true)
sb2.fs.(*hostFS).root = nil
if err := sb2.Fs().WriteFile(context.Background(), "nil_root_test.txt", content, true); err != nil {
err = sb2.Fs().WriteFile(context.Background(), "nil_root_test.txt", content, true)
if err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
@ -307,7 +309,8 @@ func TestValidatePathErrors(t *testing.T) {
root := t.TempDir()
// target parent is file, evalSymlinks should fail
if err := os.WriteFile(filepath.Join(root, "a.txt"), []byte("a"), 0644); err != nil {
err = os.WriteFile(filepath.Join(root, "a.txt"), []byte("a"), 0o644)
if err != nil {
t.Fatal(err)
}
_, err = validatePath("a.txt/b.txt", root, true)

View file

@ -332,7 +332,11 @@ func (m *scopedSandboxManager) Exec(ctx context.Context, req ExecRequest) (*Exec
return sb.Exec(ctx, req)
}
func (m *scopedSandboxManager) ExecStream(ctx context.Context, req ExecRequest, onEvent func(ExecEvent) error) (*ExecResult, error) {
func (m *scopedSandboxManager) ExecStream(
ctx context.Context,
req ExecRequest,
onEvent func(ExecEvent) error,
) (*ExecResult, error) {
if !m.shouldSandbox(ctx) {
return m.host.ExecStream(ctx, req, onEvent)
}
@ -382,7 +386,8 @@ func (m *scopedSandboxManager) normalizeSessionKey(raw string) string {
return main
}
if parsed := routing.ParseAgentSessionKey(trimmed); parsed != nil {
if routing.NormalizeAgentID(parsed.AgentID) == m.agentID && strings.EqualFold(strings.TrimSpace(parsed.Rest), "main") {
if routing.NormalizeAgentID(parsed.AgentID) == m.agentID &&
strings.EqualFold(strings.TrimSpace(parsed.Rest), "main") {
return main
}
}
@ -538,7 +543,11 @@ func (u *unavailableSandboxManager) Exec(ctx context.Context, req ExecRequest) (
})
}
func (u *unavailableSandboxManager) ExecStream(ctx context.Context, req ExecRequest, onEvent func(ExecEvent) error) (*ExecResult, error) {
func (u *unavailableSandboxManager) ExecStream(
ctx context.Context,
req ExecRequest,
onEvent func(ExecEvent) error,
) (*ExecResult, error) {
return nil, u.err
}

View file

@ -34,12 +34,12 @@ type registryFileLock struct {
func acquireRegistryFileLock(registryPath string) (*registryFileLock, error) {
lockPath := registryPath + ".lock"
if err := os.MkdirAll(filepath.Dir(lockPath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(lockPath), 0o755); err != nil {
return nil, err
}
deadline := time.Now().Add(registryLockTimeout)
for {
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err == nil {
_ = f.Close()
return &registryFileLock{path: lockPath}, nil
@ -89,7 +89,7 @@ func loadRegistry(path string) (*registryData, error) {
}
func saveRegistry(path string, data *registryData) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
raw, err := json.MarshalIndent(data, "", " ")
@ -97,7 +97,7 @@ func saveRegistry(path string, data *registryData) error {
return err
}
tmp := fmt.Sprintf("%s.%d.tmp", path, time.Now().UnixNano())
if err := os.WriteFile(tmp, append(raw, '\n'), 0644); err != nil {
if err := os.WriteFile(tmp, append(raw, '\n'), 0o644); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {

View file

@ -53,7 +53,8 @@ func TestRegistryUpsertAndRemove(t *testing.T) {
t.Fatalf("createdAt preserved = %d, want %d", data.Entries[0].CreatedAtMs, now)
}
if err := removeRegistryEntry(path, "c1"); err != nil {
err = removeRegistryEntry(path, "c1")
if err != nil {
t.Fatalf("removeRegistryEntry failed: %v", err)
}
data, err = loadRegistry(path)

View file

@ -167,16 +167,16 @@ type SessionConfig struct {
}
type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
ModelFallbacks []string `json:"model_fallbacks,omitempty"`
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
Sandbox AgentSandboxConfig `json:"sandbox"`
}
@ -454,7 +454,7 @@ type ExecConfig struct {
}
type AgentSandboxPruneConfig struct {
IdleHours int `json:"idle_hours" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_PRUNE_IDLE_HOURS"`
IdleHours int `json:"idle_hours" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_PRUNE_IDLE_HOURS"`
MaxAgeDays int `json:"max_age_days" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_PRUNE_MAX_AGE_DAYS"`
}
@ -502,22 +502,22 @@ func (v AgentSandboxDockerUlimitValue) MarshalJSON() ([]byte, error) {
}
type AgentSandboxDockerConfig struct {
Image string `json:"image" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_IMAGE"`
Image string `json:"image" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_IMAGE"`
ContainerPrefix string `json:"container_prefix" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CONTAINER_PREFIX"`
Workdir string `json:"workdir" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_WORKDIR"`
ReadOnlyRoot bool `json:"read_only_root" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_READ_ONLY_ROOT"`
Workdir string `json:"workdir" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_WORKDIR"`
ReadOnlyRoot bool `json:"read_only_root" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_READ_ONLY_ROOT"`
Tmpfs []string `json:"tmpfs"`
Network string `json:"network" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_NETWORK"`
User string `json:"user" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_USER"`
Network string `json:"network" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_NETWORK"`
User string `json:"user" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_USER"`
CapDrop []string `json:"cap_drop"`
Env map[string]string `json:"env"`
SetupCommand string `json:"setup_command" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_SETUP_COMMAND"`
PidsLimit int64 `json:"pids_limit" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_PIDS_LIMIT"`
Memory string `json:"memory" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY"`
MemorySwap string `json:"memory_swap" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY_SWAP"`
Cpus float64 `json:"cpus" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CPUS"`
SetupCommand string `json:"setup_command" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_SETUP_COMMAND"`
PidsLimit int64 `json:"pids_limit" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_PIDS_LIMIT"`
Memory string `json:"memory" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY"`
MemorySwap string `json:"memory_swap" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY_SWAP"`
Cpus float64 `json:"cpus" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CPUS"`
Ulimits map[string]AgentSandboxDockerUlimitValue `json:"ulimits"`
SeccompProfile string `json:"seccomp_profile" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_SECCOMP_PROFILE"`
SeccompProfile string `json:"seccomp_profile" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_SECCOMP_PROFILE"`
ApparmorProfile string `json:"apparmor_profile" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_APPARMOR_PROFILE"`
DNS []string `json:"dns"`
ExtraHosts []string `json:"extra_hosts"`
@ -525,10 +525,10 @@ type AgentSandboxDockerConfig struct {
}
type AgentSandboxConfig struct {
Mode string `json:"mode" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_MODE"`
Scope string `json:"scope" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_SCOPE"`
Mode string `json:"mode" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_MODE"`
Scope string `json:"scope" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_SCOPE"`
WorkspaceAccess string `json:"workspace_access" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_WORKSPACE_ACCESS"`
WorkspaceRoot string `json:"workspace_root" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_WORKSPACE_ROOT"`
WorkspaceRoot string `json:"workspace_root" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_WORKSPACE_ROOT"`
Docker AgentSandboxDockerConfig `json:"docker"`
Prune AgentSandboxPruneConfig `json:"prune"`
}

View file

@ -33,7 +33,15 @@ type CronTool struct {
// NewCronTool creates a new CronTool.
// execTimeout: 0 means no timeout, >0 sets the timeout duration.
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
func NewCronTool(
cronService *cron.CronService,
executor JobExecutor,
msgBus *bus.MessageBus,
workspace string,
restrict bool,
execTimeout time.Duration,
config *config.Config,
) *CronTool {
sb := sandbox.NewFromConfig(workspace, restrict, config)
guard := NewExecToolWithConfig(workspace, restrict, config)
return &CronTool{

View file

@ -29,7 +29,11 @@ func (s *cronStubSandbox) Exec(ctx context.Context, req sandbox.ExecRequest) (*s
return s.ExecStream(ctx, req, nil)
}
func (s *cronStubSandbox) ExecStream(ctx context.Context, req sandbox.ExecRequest, onEvent func(sandbox.ExecEvent) error) (*sandbox.ExecResult, error) {
func (s *cronStubSandbox) ExecStream(
ctx context.Context,
req sandbox.ExecRequest,
onEvent func(sandbox.ExecEvent) error,
) (*sandbox.ExecResult, error) {
s.calls++
s.last = req
if s.err != nil {
@ -38,12 +42,16 @@ func (s *cronStubSandbox) ExecStream(ctx context.Context, req sandbox.ExecReques
if s.res != nil {
if onEvent != nil {
if s.res.Stdout != "" {
if err := onEvent(sandbox.ExecEvent{Type: sandbox.ExecEventStdout, Chunk: []byte(s.res.Stdout)}); err != nil {
if err := onEvent(
sandbox.ExecEvent{Type: sandbox.ExecEventStdout, Chunk: []byte(s.res.Stdout)},
); err != nil {
return nil, err
}
}
if s.res.Stderr != "" {
if err := onEvent(sandbox.ExecEvent{Type: sandbox.ExecEventStderr, Chunk: []byte(s.res.Stderr)}); err != nil {
if err := onEvent(
sandbox.ExecEvent{Type: sandbox.ExecEventStderr, Chunk: []byte(s.res.Stderr)},
); err != nil {
return nil, err
}
}
@ -66,7 +74,10 @@ func (s *cronStubSandbox) ExecStream(ctx context.Context, req sandbox.ExecReques
type noopExecutor struct{}
func (n *noopExecutor) ProcessDirectWithChannel(ctx context.Context, content, sessionKey, channel, chatID string) (string, error) {
func (n *noopExecutor) ProcessDirectWithChannel(
ctx context.Context,
content, sessionKey, channel, chatID string,
) (string, error) {
return "ok", nil
}

View file

@ -104,11 +104,13 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]any) *ToolRe
newContent := strings.Replace(contentStr, oldText, newText, 1)
if sb != nil {
if err := sb.Fs().WriteFile(ctx, path, []byte(newContent), true); err != nil {
err = sb.Fs().WriteFile(ctx, path, []byte(newContent), true)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
}
} else {
if err := os.WriteFile(resolvedPath, []byte(newContent), 0o644); err != nil {
err = os.WriteFile(resolvedPath, []byte(newContent), 0o644)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
}
}
@ -169,12 +171,14 @@ func (t *AppendFileTool) Execute(ctx context.Context, args map[string]any) *Tool
sb := sandbox.SandboxFromContext(ctx)
if sb != nil {
// Implement Append using Read + Write if no Append in FsBridge
oldContent, err := sb.Fs().ReadFile(ctx, path)
var oldContent []byte
oldContent, err = sb.Fs().ReadFile(ctx, path)
if err != nil && !strings.Contains(err.Error(), "no such file") {
return ErrorResult(fmt.Sprintf("failed to read file for append: %v", err))
}
newContent := string(oldContent) + content
if err := sb.Fs().WriteFile(ctx, path, []byte(newContent), true); err != nil {
err = sb.Fs().WriteFile(ctx, path, []byte(newContent), true)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to append (write) to file: %v", err))
}
return SilentResult(fmt.Sprintf("Appended to %s", path))

View file

@ -252,7 +252,6 @@ func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolRes
}
entries, err := os.ReadDir(resolvedPath)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to read directory: %v", err))
}

View file

@ -29,7 +29,11 @@ func (s *stubSandbox) Exec(ctx context.Context, req sandbox.ExecRequest) (*sandb
return sandboxAggregateFromStub(ctx, req, s.ExecStream)
}
func (s *stubSandbox) ExecStream(ctx context.Context, req sandbox.ExecRequest, onEvent func(sandbox.ExecEvent) error) (*sandbox.ExecResult, error) {
func (s *stubSandbox) ExecStream(
ctx context.Context,
req sandbox.ExecRequest,
onEvent func(sandbox.ExecEvent) error,
) (*sandbox.ExecResult, error) {
s.lastReq = req
if s.err != nil {
return nil, s.err
@ -37,12 +41,16 @@ func (s *stubSandbox) ExecStream(ctx context.Context, req sandbox.ExecRequest, o
if s.res != nil {
if onEvent != nil {
if s.res.Stdout != "" {
if err := onEvent(sandbox.ExecEvent{Type: sandbox.ExecEventStdout, Chunk: []byte(s.res.Stdout)}); err != nil {
if err := onEvent(
sandbox.ExecEvent{Type: sandbox.ExecEventStdout, Chunk: []byte(s.res.Stdout)},
); err != nil {
return nil, err
}
}
if s.res.Stderr != "" {
if err := onEvent(sandbox.ExecEvent{Type: sandbox.ExecEventStderr, Chunk: []byte(s.res.Stderr)}); err != nil {
if err := onEvent(
sandbox.ExecEvent{Type: sandbox.ExecEventStderr, Chunk: []byte(s.res.Stderr)},
); err != nil {
return nil, err
}
}
@ -361,7 +369,7 @@ func TestShellTool_SandboxMapsHostWorkingDirToRelative(t *testing.T) {
tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb)
args := map[string]interface{}{
args := map[string]any{
"command": "echo test",
"working_dir": filepath.Join(workspace, "subdir"),
}
@ -380,7 +388,7 @@ func TestShellTool_SandboxAllowsAbsoluteWorkspaceWorkingDir(t *testing.T) {
tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb)
args := map[string]interface{}{
args := map[string]any{
"command": "echo test",
"working_dir": "/workspace/subdir",
}
@ -399,7 +407,7 @@ func TestShellTool_SandboxBlocksAbsoluteNonWorkspaceWorkingDirWhenRestricted(t *
tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb)
args := map[string]interface{}{
args := map[string]any{
"command": "echo test",
"working_dir": "/tmp/logs",
}
@ -418,7 +426,7 @@ func TestShellTool_SandboxExecError(t *testing.T) {
tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb)
result := tool.Execute(ctx, map[string]interface{}{"command": "echo test"})
result := tool.Execute(ctx, map[string]any{"command": "echo test"})
if !result.IsError {
t.Fatal("expected sandbox error result")
}