refactor: update error handling, code formatting, and configuration field tags across agent sandbox, tools, and config files.
This commit is contained in:
parent
bd8af1ba1e
commit
c81f3d6a88
16 changed files with 144 additions and 69 deletions
|
|
@ -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",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ®istryFileLock{path: lockPath}, nil
|
return ®istryFileLock{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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue