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.
This commit is contained in:
parent
d38ca161ec
commit
6ac314b15d
5 changed files with 341 additions and 19 deletions
|
|
@ -7,10 +7,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/isolation"
|
||||||
"github.com/sipeed/picoclaw/pkg/providers"
|
"github.com/sipeed/picoclaw/pkg/providers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -178,6 +181,75 @@ func TestAgentLoop_MountProcessHook_ApprovalDeny(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAgentLoop_MountProcessHook_IsolationSupportsRelativeDirAndCommand(t *testing.T) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skip("linux-only isolation path handling")
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &llmHookTestProvider{}
|
||||||
|
al, agent, cleanup := newHookTestLoop(t, provider)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
t.Setenv(config.EnvHome, filepath.Join(root, "picoclaw-home"))
|
||||||
|
binDir := filepath.Join(root, "bin")
|
||||||
|
hookDir := filepath.Join(root, "hooks")
|
||||||
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(hookDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeFakeBwrap(t, filepath.Join(binDir, "bwrap"))
|
||||||
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
linkTestBinary(t, os.Args[0], filepath.Join(hookDir, "hook-helper"))
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Isolation.Enabled = true
|
||||||
|
isolation.Configure(cfg)
|
||||||
|
t.Cleanup(func() { isolation.Configure(config.DefaultConfig()) })
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
relHookDir, err := filepath.Rel(cwd, hookDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := al.MountProcessHook(context.Background(), "ipc-relative", ProcessHookOptions{
|
||||||
|
Command: []string{"./hook-helper", "-test.run=TestProcessHook_HelperProcess", "--"},
|
||||||
|
Dir: relHookDir,
|
||||||
|
Env: processHookHelperEnv("rewrite", ""),
|
||||||
|
InterceptLLM: true,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("MountProcessHook failed with relative dir/command under isolation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := al.runAgentLoop(context.Background(), agent, processOptions{
|
||||||
|
SessionKey: "session-relative",
|
||||||
|
Channel: "cli",
|
||||||
|
ChatID: "direct",
|
||||||
|
UserMessage: "hello",
|
||||||
|
DefaultResponse: defaultResponse,
|
||||||
|
EnableSummary: false,
|
||||||
|
SendResponse: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runAgentLoop failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp != "provider content|ipc" {
|
||||||
|
t.Fatalf("expected process-hooked llm content, got %q", resp)
|
||||||
|
}
|
||||||
|
provider.mu.Lock()
|
||||||
|
lastModel := provider.lastModel
|
||||||
|
provider.mu.Unlock()
|
||||||
|
if lastModel != "process-model" {
|
||||||
|
t.Fatalf("expected process model, got %q", lastModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func processHookHelperCommand() []string {
|
func processHookHelperCommand() []string {
|
||||||
return []string{os.Args[0], "-test.run=TestProcessHook_HelperProcess", "--"}
|
return []string{os.Args[0], "-test.run=TestProcessHook_HelperProcess", "--"}
|
||||||
}
|
}
|
||||||
|
|
@ -193,6 +265,59 @@ func processHookHelperEnv(mode, eventLog string) []string {
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeFakeBwrap(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
script := `#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
workdir=
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
--chdir)
|
||||||
|
workdir="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bind|--ro-bind)
|
||||||
|
shift 3
|
||||||
|
;;
|
||||||
|
--proc|--dev)
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--die-with-parent|--unshare-ipc)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ -n "$workdir" ]; then
|
||||||
|
cd "$workdir"
|
||||||
|
fi
|
||||||
|
exec "$@"
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatalf("write fake bwrap: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkTestBinary(t *testing.T, source, target string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.Symlink(source, target); err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read test binary: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(target, data, 0o755); err != nil {
|
||||||
|
t.Fatalf("create hook helper binary: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func waitForFileContains(t *testing.T, path, substring string) {
|
func waitForFileContains(t *testing.T, path, substring string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ Capabilities:
|
||||||
|
|
||||||
Default mounts include the instance root plus the minimum runtime system paths such as `/usr`, `/bin`, `/lib`, `/lib64`, and `/etc/resolv.conf`.
|
Default mounts include the instance root plus the minimum runtime system paths such as `/usr`, `/bin`, `/lib`, `/lib64`, and `/etc/resolv.conf`.
|
||||||
|
|
||||||
At runtime, PicoClaw also adds the executable path, its directory, the working directory, and absolute path arguments when needed.
|
At runtime, PicoClaw also adds the executable path, its directory, the effective working directory, and absolute path arguments when needed.
|
||||||
|
|
||||||
There is no automatic fallback when `bwrap` is missing.
|
There is no automatic fallback when `bwrap` is missing.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ Linux 后端当前依赖 `bwrap`(`bubblewrap`)。
|
||||||
|
|
||||||
默认映射包括实例根,以及 `/usr`、`/bin`、`/lib`、`/lib64`、`/etc/resolv.conf` 等最小运行时系统路径。
|
默认映射包括实例根,以及 `/usr`、`/bin`、`/lib`、`/lib64`、`/etc/resolv.conf` 等最小运行时系统路径。
|
||||||
|
|
||||||
运行时还会按需补充可执行文件本身、其所在目录、当前工作目录,以及命令行中的绝对路径参数。
|
运行时还会按需补充可执行文件本身、其所在目录、生效后的工作目录,以及命令行中的绝对路径参数。
|
||||||
|
|
||||||
缺少 `bwrap` 时不会自动回退。
|
缺少 `bwrap` 时不会自动回退。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
package isolation
|
package isolation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
|
@ -42,32 +44,40 @@ func applyPlatformIsolation(cmd *exec.Cmd, isolation config.IsolationConfig, roo
|
||||||
|
|
||||||
originalPath := cmd.Path
|
originalPath := cmd.Path
|
||||||
originalArgs := append([]string{}, cmd.Args...)
|
originalArgs := append([]string{}, cmd.Args...)
|
||||||
originalDir := cmd.Dir
|
_, 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
|
// Start from the configured mount plan, then add only the executable, its
|
||||||
// resolved path, and the working directory. Any additional host paths must be
|
// resolved path, the effective working directory, and any absolute path
|
||||||
// exposed explicitly via config instead of being inferred from argv.
|
// arguments needed to preserve the original command semantics.
|
||||||
plan := BuildLinuxMountPlan(root, isolation.ExposePaths)
|
plan := BuildLinuxMountPlan(root, isolation.ExposePaths)
|
||||||
plan = ensureLinuxMountRule(plan, originalPath, originalPath, "ro")
|
plan = ensureLinuxMountRule(plan, resolvedPath, resolvedPath, "ro")
|
||||||
plan = ensureLinuxMountRule(plan, filepath.Dir(originalPath), filepath.Dir(originalPath), "ro")
|
plan = ensureLinuxMountRule(plan, filepath.Dir(resolvedPath), filepath.Dir(resolvedPath), "ro")
|
||||||
if resolved, resolveErr := filepath.EvalSymlinks(originalPath); resolveErr == nil && resolved != originalPath {
|
if resolved, resolveErr := filepath.EvalSymlinks(resolvedPath); resolveErr == nil && resolved != resolvedPath {
|
||||||
plan = ensureLinuxMountRule(plan, resolved, resolved, "ro")
|
plan = ensureLinuxMountRule(plan, resolved, resolved, "ro")
|
||||||
plan = ensureLinuxMountRule(plan, filepath.Dir(resolved), filepath.Dir(resolved), "ro")
|
plan = ensureLinuxMountRule(plan, filepath.Dir(resolved), filepath.Dir(resolved), "ro")
|
||||||
}
|
}
|
||||||
if originalDir != "" {
|
if execDir != "" {
|
||||||
plan = ensureLinuxMountRule(plan, originalDir, originalDir, "rw")
|
plan = ensureLinuxMountRule(plan, execDir, execDir, "rw")
|
||||||
if resolved, resolveErr := filepath.EvalSymlinks(originalDir); resolveErr == nil && resolved != originalDir {
|
if resolved, resolveErr := filepath.EvalSymlinks(execDir); resolveErr == nil && resolved != execDir {
|
||||||
plan = ensureLinuxMountRule(plan, resolved, resolved, "rw")
|
plan = ensureLinuxMountRule(plan, resolved, resolved, "rw")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
plan = appendLinuxArgumentMounts(plan, originalArgs[1:])
|
||||||
logger.DebugCF("isolation", "linux isolation mount plan",
|
logger.DebugCF("isolation", "linux isolation mount plan",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"root": root,
|
"root": root,
|
||||||
"command": originalPath,
|
"command": resolvedPath,
|
||||||
"working_dir": originalDir,
|
"working_dir": execDir,
|
||||||
"mounts": formatLinuxMountPlan(plan),
|
"mounts": formatLinuxMountPlan(plan),
|
||||||
})
|
})
|
||||||
bwrapArgs, err := buildLinuxBwrapArgs(originalPath, originalArgs, originalDir, plan)
|
bwrapArgs, err := buildLinuxBwrapArgs(originalPath, resolvedPath, originalArgs, execDir, plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -106,8 +116,9 @@ func cleanupPendingPlatformResources(cmd *exec.Cmd) {
|
||||||
// line that re-executes the original process inside the isolated mount view.
|
// line that re-executes the original process inside the isolated mount view.
|
||||||
func buildLinuxBwrapArgs(
|
func buildLinuxBwrapArgs(
|
||||||
originalPath string,
|
originalPath string,
|
||||||
|
resolvedPath string,
|
||||||
originalArgs []string,
|
originalArgs []string,
|
||||||
originalDir string,
|
execDir string,
|
||||||
plan []MountRule,
|
plan []MountRule,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
bwrapArgs := []string{
|
bwrapArgs := []string{
|
||||||
|
|
@ -124,16 +135,103 @@ func buildLinuxBwrapArgs(
|
||||||
}
|
}
|
||||||
bwrapArgs = append(bwrapArgs, flag, rule.Source, rule.Target)
|
bwrapArgs = append(bwrapArgs, flag, rule.Source, rule.Target)
|
||||||
}
|
}
|
||||||
if originalDir != "" {
|
if execDir != "" {
|
||||||
bwrapArgs = append(bwrapArgs, "--chdir", originalDir)
|
bwrapArgs = append(bwrapArgs, "--chdir", execDir)
|
||||||
}
|
}
|
||||||
bwrapArgs = append(bwrapArgs, "--", originalPath)
|
execPath := originalPath
|
||||||
|
if isRelativeCommandPath(originalPath) {
|
||||||
|
execPath = resolvedPath
|
||||||
|
}
|
||||||
|
bwrapArgs = append(bwrapArgs, "--", execPath)
|
||||||
if len(originalArgs) > 1 {
|
if len(originalArgs) > 1 {
|
||||||
bwrapArgs = append(bwrapArgs, originalArgs[1:]...)
|
bwrapArgs = append(bwrapArgs, originalArgs[1:]...)
|
||||||
}
|
}
|
||||||
return bwrapArgs, nil
|
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
|
// ensureLinuxMountRule appends a mount rule unless another rule already owns
|
||||||
// the same target path.
|
// the same target path.
|
||||||
func ensureLinuxMountRule(plan []MountRule, source, target, mode string) []MountRule {
|
func ensureLinuxMountRule(plan []MountRule, source, target, mode string) []MountRule {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func TestBuildLinuxBwrapArgs_IncludesNamespaceFlagsAndExec(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
plan := BuildLinuxMountPlan(root, []config.ExposePath{{Source: binaryDir, Target: binaryDir, Mode: "ro"}})
|
plan := BuildLinuxMountPlan(root, []config.ExposePath{{Source: binaryDir, Target: binaryDir, Mode: "ro"}})
|
||||||
args, err := buildLinuxBwrapArgs(binaryPath, []string{binaryPath, "--flag"}, root, plan)
|
args, err := buildLinuxBwrapArgs(binaryPath, binaryPath, []string{binaryPath, "--flag"}, root, plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("buildLinuxBwrapArgs() error = %v", err)
|
t.Fatalf("buildLinuxBwrapArgs() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -47,3 +47,102 @@ func TestBuildLinuxBwrapArgs_IncludesNamespaceFlagsAndExec(t *testing.T) {
|
||||||
t.Fatalf("bwrap args missing required items: %v", args)
|
t.Fatalf("bwrap args missing required items: %v", args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveLinuxWorkingDir_ResolvesRelativeDir(t *testing.T) {
|
||||||
|
cwd := t.TempDir()
|
||||||
|
previous, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if chdirErr := os.Chdir(previous); chdirErr != nil {
|
||||||
|
t.Fatalf("restore cwd: %v", chdirErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := os.Chdir(cwd); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedDir, execDir, err := resolveLinuxWorkingDir("./hooks", "./hook.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveLinuxWorkingDir() error = %v", err)
|
||||||
|
}
|
||||||
|
want := filepath.Join(cwd, "hooks")
|
||||||
|
if resolvedDir != want || execDir != want {
|
||||||
|
t.Fatalf("resolveLinuxWorkingDir() = (%q, %q), want (%q, %q)", resolvedDir, execDir, want, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveLinuxCommandPath_UsesExecDirForRelativeCommand(t *testing.T) {
|
||||||
|
execDir := filepath.Join(t.TempDir(), "hooks")
|
||||||
|
got, err := resolveLinuxCommandPath("./hook.sh", execDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveLinuxCommandPath() error = %v", err)
|
||||||
|
}
|
||||||
|
want := filepath.Join(execDir, "hook.sh")
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("resolveLinuxCommandPath() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildLinuxBwrapArgs_UsesResolvedPathForRelativeCommand(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
execDir := filepath.Join(root, "hooks")
|
||||||
|
if err := os.MkdirAll(execDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
resolvedPath := filepath.Join(execDir, "hook.sh")
|
||||||
|
if err := os.WriteFile(resolvedPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plan := []MountRule{
|
||||||
|
{Source: execDir, Target: execDir, Mode: "rw"},
|
||||||
|
{Source: resolvedPath, Target: resolvedPath, Mode: "ro"},
|
||||||
|
}
|
||||||
|
args, err := buildLinuxBwrapArgs("./hook.sh", resolvedPath, []string{"./hook.sh"}, execDir, plan)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildLinuxBwrapArgs() error = %v", err)
|
||||||
|
}
|
||||||
|
hasExecDir := false
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == execDir {
|
||||||
|
hasExecDir = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasExecDir {
|
||||||
|
t.Fatalf("buildLinuxBwrapArgs() missing resolved chdir: %v", args)
|
||||||
|
}
|
||||||
|
for i := range args {
|
||||||
|
if args[i] == "--" {
|
||||||
|
if i+1 >= len(args) || args[i+1] != resolvedPath {
|
||||||
|
t.Fatalf("buildLinuxBwrapArgs() exec path = %v, want %q after --", args, resolvedPath)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("buildLinuxBwrapArgs() missing exec delimiter: %v", args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendLinuxArgumentMounts_AddsAbsoluteArgumentPaths(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
input := filepath.Join(root, "input.txt")
|
||||||
|
if err := os.WriteFile(input, []byte("data"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output := filepath.Join(root, "out", "result.txt")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := appendLinuxArgumentMounts(nil, []string{input, "--output=" + output})
|
||||||
|
if len(plan) != 2 {
|
||||||
|
t.Fatalf("appendLinuxArgumentMounts() len = %d, want 2", len(plan))
|
||||||
|
}
|
||||||
|
if plan[0].Source != input || plan[0].Mode != "ro" {
|
||||||
|
t.Fatalf("appendLinuxArgumentMounts()[0] = %+v, want source=%q mode=ro", plan[0], input)
|
||||||
|
}
|
||||||
|
if plan[1].Source != filepath.Dir(output) || plan[1].Mode != "rw" {
|
||||||
|
t.Fatalf("appendLinuxArgumentMounts()[1] = %+v, want source=%q mode=rw", plan[1], filepath.Dir(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue