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", "path": "a.txt",
"content": "hello", "content": "hello",
}) })

View file

@ -696,7 +696,14 @@ func (al *AgentLoop) runLLMIteration(
} }
toolCtx := sandbox.WithSessionKey(ctx, opts.SessionKey) 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 // Send ForUser content to user immediately if not Silent
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse { if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {

View file

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

View file

@ -27,7 +27,8 @@ func TestContainerSandbox_Integration_ExecReadWrite(t *testing.T) {
} }
defer cli.Close() defer cli.Close()
if _, err := cli.Ping(ctx); err != nil { _, err = cli.Ping(ctx)
if err != nil {
t.Skipf("docker daemon unavailable: %v", err) t.Skipf("docker daemon unavailable: %v", err)
} }
@ -43,7 +44,8 @@ func TestContainerSandbox_Integration_ExecReadWrite(t *testing.T) {
ContainerName: containerName, ContainerName: containerName,
Workspace: workspace, Workspace: workspace,
}) })
if err := sb.Start(ctx); err != nil { err = sb.Start(ctx)
if err != nil {
t.Fatalf("sandbox start failed: %v", err) t.Fatalf("sandbox start failed: %v", err)
} }
defer func() { defer func() {
@ -54,7 +56,8 @@ func TestContainerSandbox_Integration_ExecReadWrite(t *testing.T) {
}() }()
content := []byte("hello from integration test") 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) t.Fatalf("write file failed: %v", err)
} }
@ -116,7 +119,8 @@ func TestContainerSandbox_Integration_WriteFileMkdirInContainerTmp(t *testing.T)
} }
defer cli.Close() defer cli.Close()
if _, err := cli.Ping(ctx); err != nil { _, err = cli.Ping(ctx)
if err != nil {
t.Skipf("docker daemon unavailable: %v", err) t.Skipf("docker daemon unavailable: %v", err)
} }
@ -130,7 +134,8 @@ func TestContainerSandbox_Integration_WriteFileMkdirInContainerTmp(t *testing.T)
Image: image, Image: image,
ContainerName: containerName, ContainerName: containerName,
}) })
if err := sb.Start(ctx); err != nil { err = sb.Start(ctx)
if err != nil {
t.Fatalf("sandbox start failed: %v", err) t.Fatalf("sandbox start failed: %v", err)
} }
defer func() { defer func() {
@ -141,7 +146,8 @@ func TestContainerSandbox_Integration_WriteFileMkdirInContainerTmp(t *testing.T)
}() }()
content := []byte("mkdir path works") 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) 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) t.Fatalf("unexpected pids limit: %#v", hc.Resources.PidsLimit)
} }
if hc.Memory <= 0 || hc.MemorySwap <= 0 || hc.NanoCPUs <= 0 { 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"] != "" { if len(hc.Tmpfs) != 2 || hc.Tmpfs["/run"] != "" {
t.Fatalf("unexpected tmpfs map: %#v", hc.Tmpfs) 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) t.Fatalf("security options missing expected profiles: %v", hc.SecurityOpt)
} }
if len(hc.Resources.Ulimits) != 2 { 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) == "" { if strings.TrimSpace(req.Command) == "" {
return nil, fmt.Errorf("empty 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 return err
} }
if mkdir { 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 err
} }
} }
return os.WriteFile(resolved, data, 0644) return os.WriteFile(resolved, data, 0o644)
} }
relPath, err := h.getSafeRelPath(path) relPath, err := h.getSafeRelPath(path)
@ -239,12 +243,12 @@ func (h *hostFS) WriteFile(ctx context.Context, path string, data []byte, mkdir
if mkdir { if mkdir {
// MkdirAll natively resolves inside os.Root to avoid escapes. // 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 return err
} }
} }
// Uses OS-level guarantees to restrict the file writing within the root descriptor. // 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. // 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) 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) t.Fatalf("WriteFile() error: %v", err)
} }
b, err := sb.Fs().ReadFile(context.Background(), "dir/a.txt") 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 // Test case where root is nil explicitly
sb2 := NewHostSandbox(root, true) sb2 := NewHostSandbox(root, true)
sb2.fs.(*hostFS).root = nil 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) t.Fatalf("WriteFile failed: %v", err)
} }
@ -307,7 +309,8 @@ func TestValidatePathErrors(t *testing.T) {
root := t.TempDir() root := t.TempDir()
// target parent is file, evalSymlinks should fail // 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) t.Fatal(err)
} }
_, err = validatePath("a.txt/b.txt", root, true) _, 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) 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) { if !m.shouldSandbox(ctx) {
return m.host.ExecStream(ctx, req, onEvent) return m.host.ExecStream(ctx, req, onEvent)
} }
@ -382,7 +386,8 @@ func (m *scopedSandboxManager) normalizeSessionKey(raw string) string {
return main return main
} }
if parsed := routing.ParseAgentSessionKey(trimmed); parsed != nil { 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 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 return nil, u.err
} }

View file

@ -34,12 +34,12 @@ type registryFileLock struct {
func acquireRegistryFileLock(registryPath string) (*registryFileLock, error) { func acquireRegistryFileLock(registryPath string) (*registryFileLock, error) {
lockPath := registryPath + ".lock" 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 return nil, err
} }
deadline := time.Now().Add(registryLockTimeout) deadline := time.Now().Add(registryLockTimeout)
for { 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 { if err == nil {
_ = f.Close() _ = f.Close()
return &registryFileLock{path: lockPath}, nil return &registryFileLock{path: lockPath}, nil
@ -89,7 +89,7 @@ func loadRegistry(path string) (*registryData, error) {
} }
func saveRegistry(path string, data *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 return err
} }
raw, err := json.MarshalIndent(data, "", " ") raw, err := json.MarshalIndent(data, "", " ")
@ -97,7 +97,7 @@ func saveRegistry(path string, data *registryData) error {
return err return err
} }
tmp := fmt.Sprintf("%s.%d.tmp", path, time.Now().UnixNano()) 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 return err
} }
if err := os.Rename(tmp, path); err != nil { 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) 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) t.Fatalf("removeRegistryEntry failed: %v", err)
} }
data, err = loadRegistry(path) data, err = loadRegistry(path)

View file

@ -167,16 +167,16 @@ type SessionConfig struct {
} }
type AgentDefaults struct { type AgentDefaults struct {
Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"` Workspace string `json:"workspace" env:"PICOCLAW_AGENTS_DEFAULTS_WORKSPACE"`
RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"` RestrictToWorkspace bool `json:"restrict_to_workspace" env:"PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE"`
Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"` Provider string `json:"provider" env:"PICOCLAW_AGENTS_DEFAULTS_PROVIDER"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"`
ModelFallbacks []string `json:"model_fallbacks,omitempty"` 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"` ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
Sandbox AgentSandboxConfig `json:"sandbox"` Sandbox AgentSandboxConfig `json:"sandbox"`
} }
@ -454,7 +454,7 @@ type ExecConfig struct {
} }
type AgentSandboxPruneConfig 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"` 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 { 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"` ContainerPrefix string `json:"container_prefix" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CONTAINER_PREFIX"`
Workdir string `json:"workdir" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_WORKDIR"` 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"` ReadOnlyRoot bool `json:"read_only_root" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_READ_ONLY_ROOT"`
Tmpfs []string `json:"tmpfs"` Tmpfs []string `json:"tmpfs"`
Network string `json:"network" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_NETWORK"` Network string `json:"network" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_NETWORK"`
User string `json:"user" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_USER"` User string `json:"user" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_USER"`
CapDrop []string `json:"cap_drop"` CapDrop []string `json:"cap_drop"`
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
SetupCommand string `json:"setup_command" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_SETUP_COMMAND"` 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"` PidsLimit int64 `json:"pids_limit" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_PIDS_LIMIT"`
Memory string `json:"memory" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY"` Memory string `json:"memory" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY"`
MemorySwap string `json:"memory_swap" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY_SWAP"` MemorySwap string `json:"memory_swap" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_MEMORY_SWAP"`
Cpus float64 `json:"cpus" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CPUS"` Cpus float64 `json:"cpus" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CPUS"`
Ulimits map[string]AgentSandboxDockerUlimitValue `json:"ulimits"` 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"` ApparmorProfile string `json:"apparmor_profile" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_APPARMOR_PROFILE"`
DNS []string `json:"dns"` DNS []string `json:"dns"`
ExtraHosts []string `json:"extra_hosts"` ExtraHosts []string `json:"extra_hosts"`
@ -525,10 +525,10 @@ type AgentSandboxDockerConfig struct {
} }
type AgentSandboxConfig struct { type AgentSandboxConfig struct {
Mode string `json:"mode" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_MODE"` Mode string `json:"mode" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_MODE"`
Scope string `json:"scope" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_SCOPE"` Scope string `json:"scope" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_SCOPE"`
WorkspaceAccess string `json:"workspace_access" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_WORKSPACE_ACCESS"` 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"` Docker AgentSandboxDockerConfig `json:"docker"`
Prune AgentSandboxPruneConfig `json:"prune"` Prune AgentSandboxPruneConfig `json:"prune"`
} }

View file

@ -33,7 +33,15 @@ type CronTool struct {
// NewCronTool creates a new CronTool. // NewCronTool creates a new CronTool.
// execTimeout: 0 means no timeout, >0 sets the timeout duration. // 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) sb := sandbox.NewFromConfig(workspace, restrict, config)
guard := NewExecToolWithConfig(workspace, restrict, config) guard := NewExecToolWithConfig(workspace, restrict, config)
return &CronTool{ 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) 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.calls++
s.last = req s.last = req
if s.err != nil { if s.err != nil {
@ -38,12 +42,16 @@ func (s *cronStubSandbox) ExecStream(ctx context.Context, req sandbox.ExecReques
if s.res != nil { if s.res != nil {
if onEvent != nil { if onEvent != nil {
if s.res.Stdout != "" { 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 return nil, err
} }
} }
if s.res.Stderr != "" { 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 return nil, err
} }
} }
@ -66,7 +74,10 @@ func (s *cronStubSandbox) ExecStream(ctx context.Context, req sandbox.ExecReques
type noopExecutor struct{} 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 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) newContent := strings.Replace(contentStr, oldText, newText, 1)
if sb != nil { 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)) return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
} }
} else { } 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)) 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) sb := sandbox.SandboxFromContext(ctx)
if sb != nil { if sb != nil {
// Implement Append using Read + Write if no Append in FsBridge // 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") { if err != nil && !strings.Contains(err.Error(), "no such file") {
return ErrorResult(fmt.Sprintf("failed to read file for append: %v", err)) return ErrorResult(fmt.Sprintf("failed to read file for append: %v", err))
} }
newContent := string(oldContent) + content 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 ErrorResult(fmt.Sprintf("failed to append (write) to file: %v", err))
} }
return SilentResult(fmt.Sprintf("Appended to %s", path)) 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) entries, err := os.ReadDir(resolvedPath)
if err != nil { if err != nil {
return ErrorResult(fmt.Sprintf("failed to read directory: %v", err)) 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) 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 s.lastReq = req
if s.err != nil { if s.err != nil {
return nil, s.err return nil, s.err
@ -37,12 +41,16 @@ func (s *stubSandbox) ExecStream(ctx context.Context, req sandbox.ExecRequest, o
if s.res != nil { if s.res != nil {
if onEvent != nil { if onEvent != nil {
if s.res.Stdout != "" { 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 return nil, err
} }
} }
if s.res.Stderr != "" { 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 return nil, err
} }
} }
@ -361,7 +369,7 @@ func TestShellTool_SandboxMapsHostWorkingDirToRelative(t *testing.T) {
tool := NewExecTool(workspace, true) tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb) ctx := sandbox.WithSandbox(context.Background(), sb)
args := map[string]interface{}{ args := map[string]any{
"command": "echo test", "command": "echo test",
"working_dir": filepath.Join(workspace, "subdir"), "working_dir": filepath.Join(workspace, "subdir"),
} }
@ -380,7 +388,7 @@ func TestShellTool_SandboxAllowsAbsoluteWorkspaceWorkingDir(t *testing.T) {
tool := NewExecTool(workspace, true) tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb) ctx := sandbox.WithSandbox(context.Background(), sb)
args := map[string]interface{}{ args := map[string]any{
"command": "echo test", "command": "echo test",
"working_dir": "/workspace/subdir", "working_dir": "/workspace/subdir",
} }
@ -399,7 +407,7 @@ func TestShellTool_SandboxBlocksAbsoluteNonWorkspaceWorkingDirWhenRestricted(t *
tool := NewExecTool(workspace, true) tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb) ctx := sandbox.WithSandbox(context.Background(), sb)
args := map[string]interface{}{ args := map[string]any{
"command": "echo test", "command": "echo test",
"working_dir": "/tmp/logs", "working_dir": "/tmp/logs",
} }
@ -418,7 +426,7 @@ func TestShellTool_SandboxExecError(t *testing.T) {
tool := NewExecTool(workspace, true) tool := NewExecTool(workspace, true)
ctx := sandbox.WithSandbox(context.Background(), sb) 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 { if !result.IsError {
t.Fatal("expected sandbox error result") t.Fatal("expected sandbox error result")
} }