mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
51eecde01e
* * completed * * optimzie * * fix format * * fix pr check * try to fix ci * * Indicates that Windows does not support expos_paths, adding more mount paths for the Linux platform. * fix isolation startup lifecycle and MCP transport wrapping * fix isolation startup cleanup and optional Linux mounts * fix isolation path handling for relative hooks Preserve relative command and working-directory semantics when Linux isolation wraps subprocesses, and restore absolute argv path exposure to avoid startup regressions. Add hook coverage and docs updates so isolation-enabled process hooks keep working as configured. * * fix ci
265 lines
7.6 KiB
Go
265 lines
7.6 KiB
Go
//go:build linux
|
|
|
|
package isolation
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
)
|
|
|
|
func applyPlatformIsolation(cmd *exec.Cmd, isolation config.IsolationConfig, root string) error {
|
|
if !isolation.Enabled {
|
|
return nil
|
|
}
|
|
// Bubblewrap is the only supported Linux backend right now. Fail closed when
|
|
// it is unavailable instead of silently running the child process unisolated.
|
|
bwrapPath, err := exec.LookPath("bwrap")
|
|
if err != nil {
|
|
hint := bwrapInstallHint()
|
|
disableHint := `set "isolation.enabled": false in config.json`
|
|
logger.WarnCF("isolation", "bubblewrap is required for Linux isolation",
|
|
map[string]any{
|
|
"binary": "bwrap",
|
|
"install": hint,
|
|
"disable_isolation": disableHint,
|
|
"risk": "disabling isolation lets child processes run without Linux filesystem isolation",
|
|
})
|
|
return fmt.Errorf(
|
|
"linux isolation requires bwrap and does not fall back automatically: %w; install bubblewrap with one of: %s; or disable isolation by setting %s; disabling isolation means child processes can run without Linux filesystem isolation and may access or modify more host files",
|
|
err,
|
|
hint,
|
|
disableHint,
|
|
)
|
|
}
|
|
if cmd == nil || cmd.Path == "" || len(cmd.Args) == 0 {
|
|
return nil
|
|
}
|
|
|
|
originalPath := cmd.Path
|
|
originalArgs := append([]string{}, cmd.Args...)
|
|
_, execDir, err := resolveLinuxWorkingDir(cmd.Dir, originalPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resolvedPath, err := resolveLinuxCommandPath(originalPath, execDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start from the configured mount plan, then add only the executable, its
|
|
// resolved path, the effective working directory, and any absolute path
|
|
// arguments needed to preserve the original command semantics.
|
|
plan := BuildLinuxMountPlan(root, isolation.ExposePaths)
|
|
plan = ensureLinuxMountRule(plan, resolvedPath, resolvedPath, "ro")
|
|
plan = ensureLinuxMountRule(plan, filepath.Dir(resolvedPath), filepath.Dir(resolvedPath), "ro")
|
|
if resolved, resolveErr := filepath.EvalSymlinks(resolvedPath); resolveErr == nil && resolved != resolvedPath {
|
|
plan = ensureLinuxMountRule(plan, resolved, resolved, "ro")
|
|
plan = ensureLinuxMountRule(plan, filepath.Dir(resolved), filepath.Dir(resolved), "ro")
|
|
}
|
|
if execDir != "" {
|
|
plan = ensureLinuxMountRule(plan, execDir, execDir, "rw")
|
|
if resolved, resolveErr := filepath.EvalSymlinks(execDir); resolveErr == nil && resolved != execDir {
|
|
plan = ensureLinuxMountRule(plan, resolved, resolved, "rw")
|
|
}
|
|
}
|
|
plan = appendLinuxArgumentMounts(plan, originalArgs[1:])
|
|
logger.DebugCF("isolation", "linux isolation mount plan",
|
|
map[string]any{
|
|
"root": root,
|
|
"command": resolvedPath,
|
|
"working_dir": execDir,
|
|
"mounts": formatLinuxMountPlan(plan),
|
|
})
|
|
bwrapArgs, err := buildLinuxBwrapArgs(originalPath, resolvedPath, originalArgs, execDir, plan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Path = bwrapPath
|
|
cmd.Args = bwrapArgs
|
|
cmd.Dir = ""
|
|
return nil
|
|
}
|
|
|
|
func bwrapInstallHint() string {
|
|
return "apt install bubblewrap; dnf install bubblewrap; yum install bubblewrap; pacman -S bubblewrap; apk add bubblewrap"
|
|
}
|
|
|
|
// formatLinuxMountPlan reshapes the internal plan for structured logging.
|
|
func formatLinuxMountPlan(plan []MountRule) []map[string]string {
|
|
formatted := make([]map[string]string, 0, len(plan))
|
|
for _, rule := range plan {
|
|
formatted = append(formatted, map[string]string{
|
|
"source": rule.Source,
|
|
"target": rule.Target,
|
|
"mode": rule.Mode,
|
|
})
|
|
}
|
|
return formatted
|
|
}
|
|
|
|
func postStartPlatformIsolation(cmd *exec.Cmd, isolation config.IsolationConfig, root string) error {
|
|
return nil
|
|
}
|
|
|
|
func cleanupPendingPlatformResources(cmd *exec.Cmd) {
|
|
}
|
|
|
|
// buildLinuxBwrapArgs translates the mount plan into the bubblewrap command
|
|
// line that re-executes the original process inside the isolated mount view.
|
|
func buildLinuxBwrapArgs(
|
|
originalPath string,
|
|
resolvedPath string,
|
|
originalArgs []string,
|
|
execDir string,
|
|
plan []MountRule,
|
|
) ([]string, error) {
|
|
bwrapArgs := []string{
|
|
"bwrap",
|
|
"--die-with-parent",
|
|
"--unshare-ipc",
|
|
"--proc", "/proc",
|
|
"--dev", "/dev",
|
|
}
|
|
for _, rule := range plan {
|
|
flag, err := linuxBindFlag(rule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bwrapArgs = append(bwrapArgs, flag, rule.Source, rule.Target)
|
|
}
|
|
if execDir != "" {
|
|
bwrapArgs = append(bwrapArgs, "--chdir", execDir)
|
|
}
|
|
execPath := originalPath
|
|
if isRelativeCommandPath(originalPath) {
|
|
execPath = resolvedPath
|
|
}
|
|
bwrapArgs = append(bwrapArgs, "--", execPath)
|
|
if len(originalArgs) > 1 {
|
|
bwrapArgs = append(bwrapArgs, originalArgs[1:]...)
|
|
}
|
|
return bwrapArgs, nil
|
|
}
|
|
|
|
func resolveLinuxWorkingDir(originalDir, originalPath string) (string, string, error) {
|
|
if originalDir != "" {
|
|
resolved, err := filepath.Abs(originalDir)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("resolve command dir %s: %w", originalDir, err)
|
|
}
|
|
return resolved, resolved, nil
|
|
}
|
|
if !isRelativeCommandPath(originalPath) {
|
|
return "", "", nil
|
|
}
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("resolve current working dir: %w", err)
|
|
}
|
|
return "", wd, nil
|
|
}
|
|
|
|
func resolveLinuxCommandPath(originalPath, execDir string) (string, error) {
|
|
if filepath.IsAbs(originalPath) || !isRelativeCommandPath(originalPath) {
|
|
return filepath.Clean(originalPath), nil
|
|
}
|
|
base := execDir
|
|
if base == "" {
|
|
var err error
|
|
base, err = os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve current working dir: %w", err)
|
|
}
|
|
}
|
|
return filepath.Clean(filepath.Join(base, originalPath)), nil
|
|
}
|
|
|
|
func appendLinuxArgumentMounts(plan []MountRule, args []string) []MountRule {
|
|
for _, arg := range args {
|
|
path, ok := linuxArgumentPath(arg)
|
|
if !ok {
|
|
continue
|
|
}
|
|
clean := filepath.Clean(path)
|
|
if info, err := os.Stat(clean); err == nil {
|
|
mode := "ro"
|
|
if info.IsDir() {
|
|
mode = "rw"
|
|
}
|
|
plan = ensureLinuxMountRule(plan, clean, clean, mode)
|
|
if resolved, resolveErr := filepath.EvalSymlinks(clean); resolveErr == nil && resolved != clean {
|
|
plan = ensureLinuxMountRule(plan, resolved, resolved, mode)
|
|
}
|
|
continue
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
continue
|
|
}
|
|
parent := filepath.Dir(clean)
|
|
if parent == clean {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(parent); err == nil {
|
|
plan = ensureLinuxMountRule(plan, parent, parent, "rw")
|
|
}
|
|
}
|
|
return plan
|
|
}
|
|
|
|
func linuxArgumentPath(arg string) (string, bool) {
|
|
if filepath.IsAbs(arg) {
|
|
return arg, true
|
|
}
|
|
idx := strings.IndexRune(arg, '=')
|
|
if idx <= 0 || idx == len(arg)-1 {
|
|
return "", false
|
|
}
|
|
value := arg[idx+1:]
|
|
if !filepath.IsAbs(value) {
|
|
return "", false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
func isRelativeCommandPath(path string) bool {
|
|
return !filepath.IsAbs(path) && strings.ContainsRune(path, filepath.Separator)
|
|
}
|
|
|
|
// ensureLinuxMountRule appends a mount rule unless another rule already owns
|
|
// the same target path.
|
|
func ensureLinuxMountRule(plan []MountRule, source, target, mode string) []MountRule {
|
|
cleanSource := filepath.Clean(source)
|
|
cleanTarget := filepath.Clean(target)
|
|
for _, rule := range plan {
|
|
if filepath.Clean(rule.Target) == cleanTarget {
|
|
return plan
|
|
}
|
|
}
|
|
return append(plan, MountRule{Source: cleanSource, Target: cleanTarget, Mode: mode})
|
|
}
|
|
|
|
// linuxBindFlag selects the correct bubblewrap bind flag based on mount mode.
|
|
func linuxBindFlag(rule MountRule) (string, error) {
|
|
info, err := os.Stat(rule.Source)
|
|
if err != nil {
|
|
return "", fmt.Errorf("stat linux mount source %s: %w", rule.Source, err)
|
|
}
|
|
if !info.IsDir() {
|
|
if rule.Mode == "rw" {
|
|
return "--bind", nil
|
|
}
|
|
return "--ro-bind", nil
|
|
}
|
|
if rule.Mode == "rw" {
|
|
return "--bind", nil
|
|
}
|
|
return "--ro-bind", nil
|
|
}
|