refactor: Consolidate sandbox context functions, update default image, and enhance container configuration with environment variable support.
This commit is contained in:
parent
8762367ed3
commit
0186122a87
11 changed files with 65 additions and 84 deletions
3
Makefile
3
Makefile
|
|
@ -96,6 +96,9 @@ build-all: generate
|
|||
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
|
||||
@echo "All builds complete"
|
||||
|
||||
build-docker-images:
|
||||
docker build -t picoclaw-sandbox:bookworm-slim .
|
||||
|
||||
## install: Install picoclaw to system and copy builtin skills
|
||||
install: build
|
||||
@echo "Installing $(BINARY_NAME)..."
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const defaultSandboxRegistryFile = "containers.json"
|
|||
// NewContainerSandbox creates a container sandbox with normalized defaults and precomputed config hash.
|
||||
func NewContainerSandbox(cfg ContainerSandboxConfig) *ContainerSandbox {
|
||||
if strings.TrimSpace(cfg.Image) == "" {
|
||||
cfg.Image = "debian:bookworm-slim"
|
||||
cfg.Image = "picoclaw-sandbox:bookworm-slim"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ContainerPrefix) == "" {
|
||||
cfg.ContainerPrefix = "picoclaw-sandbox-"
|
||||
|
|
@ -204,7 +204,7 @@ func (c *ContainerSandbox) ExecStream(
|
|||
execCtx := ctx
|
||||
cancel := func() {}
|
||||
if req.TimeoutMs > 0 {
|
||||
execCtx, cancel = context.WithTimeout(ctx, durationMs(req.TimeoutMs))
|
||||
execCtx, cancel = context.WithTimeout(ctx, time.Duration(req.TimeoutMs)*time.Millisecond)
|
||||
} else if _, hasDeadline := ctx.Deadline(); !hasDeadline {
|
||||
execCtx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
||||
}
|
||||
|
|
@ -334,6 +334,7 @@ func (c *ContainerSandbox) createAndStart(ctx context.Context) error {
|
|||
cfg := &container.Config{
|
||||
Image: c.cfg.Image,
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
Entrypoint: []string{},
|
||||
WorkingDir: c.cfg.Workdir,
|
||||
User: strings.TrimSpace(c.cfg.User),
|
||||
Env: c.containerEnv(),
|
||||
|
|
@ -396,7 +397,7 @@ func (c *ContainerSandbox) registryPath() string {
|
|||
}
|
||||
|
||||
func (c *ContainerSandbox) sandboxStateDir() string {
|
||||
return filepath.Join(resolvePicoClawHomeDir(), "sandbox")
|
||||
return filepath.Join(resolvePicoClawHomeDir(), "sandboxes")
|
||||
}
|
||||
|
||||
func resolvePicoClawHomeDir() string {
|
||||
|
|
@ -890,6 +891,8 @@ func computeContainerConfigHash(cfg ContainerSandboxConfig) string {
|
|||
DNS []string `json:"dns"`
|
||||
ExtraHosts []string `json:"extra_hosts"`
|
||||
Binds []string `json:"binds"`
|
||||
Cmd []string `json:"cmd"`
|
||||
Entrypoint []string `json:"entrypoint"`
|
||||
}{
|
||||
Image: strings.TrimSpace(cfg.Image),
|
||||
ContainerPrefix: strings.TrimSpace(cfg.ContainerPrefix),
|
||||
|
|
@ -915,6 +918,8 @@ func computeContainerConfigHash(cfg ContainerSandboxConfig) string {
|
|||
DNS: cfg.DNS,
|
||||
ExtraHosts: cfg.ExtraHosts,
|
||||
Binds: cfg.Binds,
|
||||
Cmd: []string{"sleep", "infinity"},
|
||||
Entrypoint: []string{},
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
return computeConfigHash(string(raw))
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ func TestContainerSandbox_RegistryPath_UsesSandboxStateDir(t *testing.T) {
|
|||
Workspace: "/tmp/ws",
|
||||
WorkspaceRoot: "/tmp/sbx",
|
||||
})
|
||||
want := filepath.Join(home, ".picoclaw", "sandbox", "containers.json")
|
||||
want := filepath.Join(home, ".picoclaw", "sandboxes", "containers.json")
|
||||
if got := sb.registryPath(); got != want {
|
||||
t.Fatalf("registryPath = %q, want %q", got, want)
|
||||
}
|
||||
|
|
@ -280,7 +280,7 @@ func TestContainerSandbox_RegistryPath_UsesPicoClawHomeOverride(t *testing.T) {
|
|||
picoHome := t.TempDir()
|
||||
t.Setenv("PICOCLAW_HOME", picoHome)
|
||||
sb := NewContainerSandbox(ContainerSandboxConfig{})
|
||||
want := filepath.Join(picoHome, "sandbox", "containers.json")
|
||||
want := filepath.Join(picoHome, "sandboxes", "containers.json")
|
||||
if got := sb.registryPath(); got != want {
|
||||
t.Fatalf("registryPath = %q, want %q", got, want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HostSandbox struct {
|
||||
|
|
@ -75,7 +76,7 @@ func (h *HostSandbox) ExecStream(
|
|||
cmdCtx := ctx
|
||||
cancel := func() {}
|
||||
if req.TimeoutMs > 0 {
|
||||
cmdCtx, cancel = context.WithTimeout(ctx, durationMs(req.TimeoutMs))
|
||||
cmdCtx, cancel = context.WithTimeout(ctx, time.Duration(req.TimeoutMs)*time.Millisecond)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
|
|
@ -165,8 +166,8 @@ func (h *HostSandbox) ExecStream(
|
|||
|
||||
exitCode := 0
|
||||
if waitErr != nil {
|
||||
var ee *exec.ExitError
|
||||
if ok := asExitError(waitErr, &ee); ok {
|
||||
ee, ok := waitErr.(*exec.ExitError)
|
||||
if ok {
|
||||
exitCode = ee.ExitCode()
|
||||
} else {
|
||||
return nil, waitErr
|
||||
|
|
|
|||
|
|
@ -121,13 +121,6 @@ func TestUnavailableSandboxAndUtilHelpers(t *testing.T) {
|
|||
if err := sb.Fs().WriteFile(context.Background(), "a.txt", []byte("x"), true); err == nil {
|
||||
t.Fatal("expected Fs().WriteFile error")
|
||||
}
|
||||
|
||||
if got := durationMs(123).Milliseconds(); got != 123 {
|
||||
t.Fatalf("durationMs() got %d, want 123", got)
|
||||
}
|
||||
if asExitError(errors.New("x"), nil) {
|
||||
t.Fatal("asExitError should be false for non-exit errors")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostFS_ReadFileWriteFile_Restricted(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ func NewFromConfigWithAgent(workspace string, restrict bool, cfg *config.Config,
|
|||
scope := "agent"
|
||||
workspaceAccess := "none"
|
||||
workspaceRoot := "~/.picoclaw/sandboxes"
|
||||
image := "debian:bookworm-slim"
|
||||
image := "picoclaw-sandbox:bookworm-slim"
|
||||
containerPrefix := "picoclaw-sandbox-"
|
||||
pruneIdleHours := 24
|
||||
pruneMaxAgeDays := 7
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
package sandbox
|
||||
|
||||
import "context"
|
||||
|
||||
type sessionContextKey struct{}
|
||||
|
||||
// WithSessionKey returns a derived context carrying the current routing session key.
|
||||
func WithSessionKey(ctx context.Context, sessionKey string) context.Context {
|
||||
return context.WithValue(ctx, sessionContextKey{}, sessionKey)
|
||||
}
|
||||
|
||||
// SessionKeyFromContext returns the session key attached by WithSessionKey.
|
||||
func SessionKeyFromContext(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := ctx.Value(sessionContextKey{}).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
type sandboxContextKey struct{}
|
||||
|
||||
// WithSandbox returns a derived context carrying the current sandbox instance.
|
||||
func WithSandbox(ctx context.Context, sb Sandbox) context.Context {
|
||||
return context.WithValue(ctx, sandboxContextKey{}, sb)
|
||||
}
|
||||
|
||||
// SandboxFromContext returns the sandbox instance attached by WithSandbox.
|
||||
func SandboxFromContext(ctx context.Context) Sandbox {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
v, _ := ctx.Value(sandboxContextKey{}).(Sandbox)
|
||||
return v
|
||||
}
|
||||
|
|
@ -74,6 +74,38 @@ type ExecEvent struct {
|
|||
ExitCode int
|
||||
}
|
||||
|
||||
type sessionContextKey struct{}
|
||||
|
||||
// WithSessionKey returns a derived context carrying the current routing session key.
|
||||
func WithSessionKey(ctx context.Context, sessionKey string) context.Context {
|
||||
return context.WithValue(ctx, sessionContextKey{}, sessionKey)
|
||||
}
|
||||
|
||||
// SessionKeyFromContext returns the session key attached by WithSessionKey.
|
||||
func SessionKeyFromContext(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := ctx.Value(sessionContextKey{}).(string)
|
||||
return v
|
||||
}
|
||||
|
||||
type sandboxContextKey struct{}
|
||||
|
||||
// WithSandbox returns a derived context carrying the current sandbox instance.
|
||||
func WithSandbox(ctx context.Context, sb Sandbox) context.Context {
|
||||
return context.WithValue(ctx, sandboxContextKey{}, sb)
|
||||
}
|
||||
|
||||
// SandboxFromContext returns the sandbox instance attached by WithSandbox.
|
||||
func SandboxFromContext(ctx context.Context) Sandbox {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
v, _ := ctx.Value(sandboxContextKey{}).(Sandbox)
|
||||
return v
|
||||
}
|
||||
|
||||
// FsBridge abstracts sandbox-scoped file I/O.
|
||||
type FsBridge interface {
|
||||
// ReadFile reads a file from sandbox-visible filesystem.
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package sandbox
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
func durationMs(ms int64) time.Duration {
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
func asExitError(err error, target **exec.ExitError) bool {
|
||||
ee, ok := err.(*exec.ExitError)
|
||||
if ok {
|
||||
*target = ee
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
|
@ -177,7 +177,7 @@ type AgentDefaults struct {
|
|||
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"`
|
||||
Sandbox AgentSandboxConfig `json:"sandbox" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX"`
|
||||
}
|
||||
|
||||
type ChannelsConfig struct {
|
||||
|
|
@ -508,22 +508,22 @@ type AgentSandboxDockerConfig struct {
|
|||
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"`
|
||||
Tmpfs []string `json:"tmpfs"`
|
||||
Tmpfs []string `json:"tmpfs" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_TMPFS"`
|
||||
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"`
|
||||
CapDrop []string `json:"cap_drop" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_CAP_DROP"`
|
||||
Env map[string]string `json:"env" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_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"`
|
||||
Ulimits map[string]AgentSandboxDockerUlimitValue `json:"ulimits"`
|
||||
Ulimits map[string]AgentSandboxDockerUlimitValue `json:"ulimits" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_ULIMITS"`
|
||||
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"`
|
||||
Binds []string `json:"binds"`
|
||||
DNS []string `json:"dns" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_DNS"`
|
||||
ExtraHosts []string `json:"extra_hosts" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_EXTRA_HOSTS"`
|
||||
Binds []string `json:"binds" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER_BINDS"`
|
||||
}
|
||||
|
||||
type AgentSandboxConfig struct {
|
||||
|
|
@ -531,17 +531,17 @@ type AgentSandboxConfig struct {
|
|||
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"`
|
||||
Docker AgentSandboxDockerConfig `json:"docker"`
|
||||
Prune AgentSandboxPruneConfig `json:"prune"`
|
||||
Docker AgentSandboxDockerConfig `json:"docker" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_DOCKER"`
|
||||
Prune AgentSandboxPruneConfig `json:"prune" env:"PICOCLAW_AGENTS_DEFAULTS_SANDBOX_PRUNE"`
|
||||
}
|
||||
|
||||
type SandboxToolPolicyConfig struct {
|
||||
Allow []string `json:"allow"`
|
||||
Deny []string `json:"deny"`
|
||||
Allow []string `json:"allow" env:"PICOCLAW_TOOLS_SANDBOX_TOOLS_ALLOW"`
|
||||
Deny []string `json:"deny" env:"PICOCLAW_TOOLS_SANDBOX_TOOLS_DENY"`
|
||||
}
|
||||
|
||||
type SandboxToolsConfig struct {
|
||||
Tools SandboxToolPolicyConfig `json:"tools"`
|
||||
Tools SandboxToolPolicyConfig `json:"tools" env:"PICOCLAW_TOOLS_SANDBOX_TOOLS"`
|
||||
}
|
||||
|
||||
type ToolsConfig struct {
|
||||
|
|
@ -549,7 +549,7 @@ type ToolsConfig struct {
|
|||
Cron CronToolsConfig `json:"cron"`
|
||||
Exec ExecConfig `json:"exec"`
|
||||
Skills SkillsToolsConfig `json:"skills"`
|
||||
Sandbox SandboxToolsConfig `json:"sandbox"`
|
||||
Sandbox SandboxToolsConfig `json:"sandbox" env:"PICOCLAW_TOOLS_SANDBOX"`
|
||||
}
|
||||
|
||||
type SkillsToolsConfig struct {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func DefaultConfig() *Config {
|
|||
WorkspaceAccess: "none",
|
||||
WorkspaceRoot: "~/.picoclaw/sandboxes",
|
||||
Docker: AgentSandboxDockerConfig{
|
||||
Image: "openclaw-sandbox:bookworm-slim",
|
||||
Image: "picoclaw-sandbox:bookworm-slim",
|
||||
ContainerPrefix: "picoclaw-sbx-",
|
||||
Workdir: "/workspace",
|
||||
ReadOnlyRoot: true,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue