feat: Improve sandbox image fallback, add Podman-compatible workspace binds, and centralize home directory resolution.
This commit is contained in:
parent
0186122a87
commit
7cdaf2cfa1
7 changed files with 107 additions and 41 deletions
4
Makefile
4
Makefile
|
|
@ -96,8 +96,8 @@ build-all: generate
|
||||||
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
|
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
|
||||||
@echo "All builds complete"
|
@echo "All builds complete"
|
||||||
|
|
||||||
build-docker-images:
|
build-sandbox:
|
||||||
docker build -t picoclaw-sandbox:bookworm-slim .
|
./scripts/build-sandbox.sh
|
||||||
|
|
||||||
## install: Install picoclaw to system and copy builtin skills
|
## install: Install picoclaw to system and copy builtin skills
|
||||||
install: build
|
install: build
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/internal/infra"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/skills"
|
"github.com/sipeed/picoclaw/pkg/skills"
|
||||||
)
|
)
|
||||||
|
|
@ -190,8 +191,7 @@ func printHelp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigPath() string {
|
func getConfigPath() string {
|
||||||
home, _ := os.UserHomeDir()
|
return filepath.Join(infra.ResolveHomeDir(), "config.json")
|
||||||
return filepath.Join(home, ".picoclaw", "config.json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() (*config.Config, error) {
|
func loadConfig() (*config.Config, error) {
|
||||||
|
|
|
||||||
22
internal/infra/homedir.go
Normal file
22
internal/infra/homedir.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveHomeDir returns the effective home directory for PicoClaw.
|
||||||
|
// It checks the PICOCLAW_HOME environment variable first,
|
||||||
|
// falls back to ~/.picoclaw if not set or empty.
|
||||||
|
func ResolveHomeDir() string {
|
||||||
|
if envHome := strings.TrimSpace(os.Getenv("PICOCLAW_HOME")); envHome != "" {
|
||||||
|
return envHome
|
||||||
|
}
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil || strings.TrimSpace(home) == "" {
|
||||||
|
// Extreme fallback
|
||||||
|
return filepath.Join(os.TempDir(), ".picoclaw")
|
||||||
|
}
|
||||||
|
return filepath.Join(home, ".picoclaw")
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"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/internal/infra"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -68,12 +69,16 @@ type ContainerSandbox struct {
|
||||||
hash string
|
hash string
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSandboxRegistryFile = "containers.json"
|
const (
|
||||||
|
defaultSandboxRegistryFile = "containers.json"
|
||||||
|
DefaultSandboxImage = "picoclaw-sandbox:bookworm-slim"
|
||||||
|
FallbackSandboxImage = "debian:bookworm-slim"
|
||||||
|
)
|
||||||
|
|
||||||
// NewContainerSandbox creates a container sandbox with normalized defaults and precomputed config hash.
|
// NewContainerSandbox creates a container sandbox with normalized defaults and precomputed config hash.
|
||||||
func NewContainerSandbox(cfg ContainerSandboxConfig) *ContainerSandbox {
|
func NewContainerSandbox(cfg ContainerSandboxConfig) *ContainerSandbox {
|
||||||
if strings.TrimSpace(cfg.Image) == "" {
|
if strings.TrimSpace(cfg.Image) == "" {
|
||||||
cfg.Image = "picoclaw-sandbox:bookworm-slim"
|
cfg.Image = DefaultSandboxImage
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.ContainerPrefix) == "" {
|
if strings.TrimSpace(cfg.ContainerPrefix) == "" {
|
||||||
cfg.ContainerPrefix = "picoclaw-sandbox-"
|
cfg.ContainerPrefix = "picoclaw-sandbox-"
|
||||||
|
|
@ -148,13 +153,31 @@ func (c *ContainerSandbox) Start(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := c.cli.ImageInspect(ctx, c.cfg.Image); err != nil {
|
if _, err := c.cli.ImageInspect(ctx, c.cfg.Image); err != nil {
|
||||||
rc, pullErr := c.cli.ImagePull(ctx, c.cfg.Image, image.PullOptions{})
|
if c.cfg.Image == DefaultSandboxImage {
|
||||||
if pullErr != nil {
|
// If default image is missing, try to pull fallback and tag it
|
||||||
c.startErr = fmt.Errorf("docker image unavailable (%s): %w", c.cfg.Image, pullErr)
|
rc, pullErr := c.cli.ImagePull(ctx, FallbackSandboxImage, image.PullOptions{})
|
||||||
return c.startErr
|
if pullErr != nil {
|
||||||
|
c.startErr = fmt.Errorf("docker fallback image unavailable (%s): %w", FallbackSandboxImage, pullErr)
|
||||||
|
return c.startErr
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, rc)
|
||||||
|
|
||||||
|
// Tag debian:bookworm-slim as picoclaw-sandbox:bookworm-slim
|
||||||
|
if err := c.cli.ImageTag(ctx, FallbackSandboxImage, DefaultSandboxImage); err != nil {
|
||||||
|
c.startErr = fmt.Errorf("failed to tag fallback image: %w", err)
|
||||||
|
return c.startErr
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-default images, just try to pull directly
|
||||||
|
rc, pullErr := c.cli.ImagePull(ctx, c.cfg.Image, image.PullOptions{})
|
||||||
|
if pullErr != nil {
|
||||||
|
c.startErr = fmt.Errorf("docker image unavailable (%s): %w", c.cfg.Image, pullErr)
|
||||||
|
return c.startErr
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
_, _ = io.Copy(io.Discard, rc)
|
||||||
}
|
}
|
||||||
defer rc.Close()
|
|
||||||
_, _ = io.Copy(io.Discard, rc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -371,17 +394,29 @@ func (c *ContainerSandbox) createAndStart(ctx context.Context) error {
|
||||||
func (c *ContainerSandbox) binds() []string {
|
func (c *ContainerSandbox) binds() []string {
|
||||||
binds := make([]string, 0, 1+len(c.cfg.Binds))
|
binds := make([]string, 0, 1+len(c.cfg.Binds))
|
||||||
workspace := strings.TrimSpace(c.cfg.Workspace)
|
workspace := strings.TrimSpace(c.cfg.Workspace)
|
||||||
|
|
||||||
|
// Determine the effective host directory to mount
|
||||||
|
var hostDir string
|
||||||
if workspace != "" {
|
if workspace != "" {
|
||||||
abs, err := filepath.Abs(workspace)
|
if abs, err := filepath.Abs(workspace); err == nil {
|
||||||
if err == nil {
|
hostDir = abs
|
||||||
switch c.cfg.WorkspaceAccess {
|
}
|
||||||
case "ro":
|
}
|
||||||
binds = append(binds, fmt.Sprintf("%s:%s:ro", abs, c.cfg.Workdir))
|
|
||||||
case "rw":
|
if hostDir != "" {
|
||||||
binds = append(binds, fmt.Sprintf("%s:%s:rw", abs, c.cfg.Workdir))
|
if c.cfg.WorkspaceAccess == "none" {
|
||||||
default:
|
// Ensure the isolated directory exists on the host so Docker doesn't create it as root
|
||||||
binds = append(binds, fmt.Sprintf("%s:%s", abs, c.cfg.Workdir))
|
_ = os.MkdirAll(hostDir, 0755)
|
||||||
}
|
}
|
||||||
|
// Add :Z flag for SELinux (Podman) to label the content with a private unshared label.
|
||||||
|
// This fixes errors like: "crun: getcwd: Operation not permitted: OCI permission denied"
|
||||||
|
switch c.cfg.WorkspaceAccess {
|
||||||
|
case "ro":
|
||||||
|
binds = append(binds, fmt.Sprintf("%s:%s:ro,Z", hostDir, c.cfg.Workdir))
|
||||||
|
case "rw", "none":
|
||||||
|
binds = append(binds, fmt.Sprintf("%s:%s:rw,Z", hostDir, c.cfg.Workdir))
|
||||||
|
default:
|
||||||
|
// Default to no mount for unknown access types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, bind := range c.cfg.Binds {
|
for _, bind := range c.cfg.Binds {
|
||||||
|
|
@ -397,19 +432,7 @@ func (c *ContainerSandbox) registryPath() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContainerSandbox) sandboxStateDir() string {
|
func (c *ContainerSandbox) sandboxStateDir() string {
|
||||||
return filepath.Join(resolvePicoClawHomeDir(), "sandboxes")
|
return filepath.Join(infra.ResolveHomeDir(), "sandboxes")
|
||||||
}
|
|
||||||
|
|
||||||
func resolvePicoClawHomeDir() string {
|
|
||||||
if envHome := strings.TrimSpace(os.Getenv("PICOCLAW_HOME")); envHome != "" {
|
|
||||||
if abs := resolveAbsPath(expandHomePath(envHome)); strings.TrimSpace(abs) != "" {
|
|
||||||
return abs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
|
|
||||||
return filepath.Join(home, ".picoclaw")
|
|
||||||
}
|
|
||||||
return filepath.Join(osTempDir(), ".picoclaw")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContainerSandbox) stopAndRemoveContainer(ctx context.Context, containerName string) error {
|
func (c *ContainerSandbox) stopAndRemoveContainer(ctx context.Context, containerName string) error {
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ func TestContainerSandbox_Binds_WorkspaceAccessModes(t *testing.T) {
|
||||||
Workdir: "/workspace",
|
Workdir: "/workspace",
|
||||||
})
|
})
|
||||||
roBinds := ro.binds()
|
roBinds := ro.binds()
|
||||||
if len(roBinds) == 0 || !strings.HasSuffix(roBinds[0], ":/workspace:ro") {
|
if len(roBinds) == 0 || !strings.HasSuffix(roBinds[0], ":/workspace:ro,Z") {
|
||||||
t.Fatalf("unexpected ro bind: %#v", roBinds)
|
t.Fatalf("unexpected ro bind: %#v", roBinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +248,7 @@ func TestContainerSandbox_Binds_WorkspaceAccessModes(t *testing.T) {
|
||||||
Workdir: "/workspace",
|
Workdir: "/workspace",
|
||||||
})
|
})
|
||||||
rwBinds := rw.binds()
|
rwBinds := rw.binds()
|
||||||
if len(rwBinds) == 0 || !strings.HasSuffix(rwBinds[0], ":/workspace:rw") {
|
if len(rwBinds) == 0 || !strings.HasSuffix(rwBinds[0], ":/workspace:rw,Z") {
|
||||||
t.Fatalf("unexpected rw bind: %#v", rwBinds)
|
t.Fatalf("unexpected rw bind: %#v", rwBinds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,10 +256,11 @@ func TestContainerSandbox_Binds_WorkspaceAccessModes(t *testing.T) {
|
||||||
Workspace: filepath.Join(root, "ws-none"),
|
Workspace: filepath.Join(root, "ws-none"),
|
||||||
WorkspaceAccess: "none",
|
WorkspaceAccess: "none",
|
||||||
Workdir: "/workspace",
|
Workdir: "/workspace",
|
||||||
|
ContainerName: "test-none",
|
||||||
})
|
})
|
||||||
noneBinds := none.binds()
|
noneBinds := none.binds()
|
||||||
if len(noneBinds) == 0 || !strings.HasSuffix(noneBinds[0], ":/workspace") {
|
if len(noneBinds) == 0 || !strings.HasSuffix(noneBinds[0], ":/workspace:rw,Z") {
|
||||||
t.Fatalf("unexpected none bind: %#v", noneBinds)
|
t.Fatalf("expected none bind to isolated workspace, got: %#v", noneBinds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/internal/infra"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/routing"
|
"github.com/sipeed/picoclaw/pkg/routing"
|
||||||
)
|
)
|
||||||
|
|
@ -275,8 +276,7 @@ func (m *scopedSandboxManager) pruneOnce(ctx context.Context) error {
|
||||||
if m.pruneIdleHours <= 0 && m.pruneMaxAgeDays <= 0 {
|
if m.pruneIdleHours <= 0 && m.pruneMaxAgeDays <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
regPath := filepath.Join(infra.ResolveHomeDir(), "sandbox", defaultSandboxRegistryFile)
|
||||||
regPath := filepath.Join(resolvePicoClawHomeDir(), "sandbox", defaultSandboxRegistryFile)
|
|
||||||
registryMu.Lock()
|
registryMu.Lock()
|
||||||
data, err := loadRegistry(regPath)
|
data, err := loadRegistry(regPath)
|
||||||
registryMu.Unlock()
|
registryMu.Unlock()
|
||||||
|
|
|
||||||
20
scripts/build-sandbox.sh
Executable file
20
scripts/build-sandbox.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# PicoClaw Sandbox Build Script
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Base directory
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
DOCKERFILE="${REPO_ROOT}/Dockerfile.sandbox"
|
||||||
|
IMAGE_NAME="picoclaw-sandbox:bookworm-slim"
|
||||||
|
|
||||||
|
echo "Building PicoClaw sandbox image: ${IMAGE_NAME}..."
|
||||||
|
|
||||||
|
if [ ! -f "${DOCKERFILE}" ]; then
|
||||||
|
echo "Error: Dockerfile.sandbox not found at ${DOCKERFILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker build -t "${IMAGE_NAME}" -f "${DOCKERFILE}" "${REPO_ROOT}"
|
||||||
|
|
||||||
|
echo "Successfully built ${IMAGE_NAME}"
|
||||||
Loading…
Add table
Reference in a new issue