refactor: Consolidate sandbox context functions, update default image, and enhance container configuration with environment variable support.

This commit is contained in:
0x5487 2026-02-24 23:10:12 +08:00
parent 8762367ed3
commit 0186122a87
11 changed files with 65 additions and 84 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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