From b55d548c5fb590fa3a8bab6bd351944cfab2ced8 Mon Sep 17 00:00:00 2001 From: mram Date: Mon, 21 Sep 2026 22:19:08 +0200 Subject: [PATCH] install-service: grant venv base interpreter (pyvenv.cfg home) outside COMFY_DIR --- internal/service/service_linux.go | 7 ++++- internal/service/service_windows.go | 9 +++++- internal/service/venv.go | 38 ++++++++++++++++++++++++ internal/service/venv_test.go | 45 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 internal/service/venv.go create mode 100644 internal/service/venv_test.go diff --git a/internal/service/service_linux.go b/internal/service/service_linux.go index 1d1ca46..6c7255e 100644 --- a/internal/service/service_linux.go +++ b/internal/service/service_linux.go @@ -64,11 +64,16 @@ func Run(run func(ctx context.Context) error) error { // directives apply. The proxy needs nothing but outbound TCP/UDP and the // notify socket, so it loses nothing. A managed ComfyUI (comfyDir) gets a // BindPaths hole through ProtectHome/ProtectSystem: it reads its venv and -// writes output/temp/user data under COMFY_DIR. +// writes output/temp/user data under COMFY_DIR. A venv whose base +// interpreter (pyvenv.cfg home) lives outside COMFY_DIR gets an additional +// read-only bind. func renderUnit(exePath, configPath, comfyDir string) string { bind := "" if comfyDir != "" { bind = "BindPaths=" + comfyDir + "\n" + if home := comfyVenvHome(comfyDir); home != "" { + bind += "BindReadOnlyPaths=" + home + "\n" + } } return fmt.Sprintf(`[Unit] Description=gpu-turnstile GPU arbitration proxy for Ollama and ComfyUI diff --git a/internal/service/service_windows.go b/internal/service/service_windows.go index 5827fab..c3cfe60 100644 --- a/internal/service/service_windows.go +++ b/internal/service/service_windows.go @@ -375,6 +375,13 @@ func grantAll(exe, configPath string) error { if err := grantAccessTree(comfyDir, "(OI)(CI)(M)"); err != nil { return err } + // uv venvs (Comfy-Desktop) redirect to a base interpreter that + // can live outside COMFY_DIR; read+execute suffices for it. + if home := comfyVenvHome(comfyDir); home != "" { + if err := grantAccessTree(home, "(OI)(CI)(RX)"); err != nil { + return err + } + } } } return nil @@ -496,7 +503,7 @@ func grantAccessTree(path, perms string) error { func runIcacls(path, perms string, recursive bool) error { args := []string{path, "/grant", virtualAccount + ":" + perms} if recursive { - fmt.Printf("granting %s modify access to %s (large trees can take minutes)\n", virtualAccount, path) + fmt.Printf("granting %s %s access to %s (large trees can take minutes)\n", virtualAccount, perms, path) args = append(args, "/T") } start := time.Now() diff --git a/internal/service/venv.go b/internal/service/venv.go new file mode 100644 index 0000000..de69db6 --- /dev/null +++ b/internal/service/venv.go @@ -0,0 +1,38 @@ +package service + +import ( + "os" + "path/filepath" + "strings" +) + +// comfyVenvHome returns the base interpreter directory of the Python venv at +// /.venv when that directory lives outside comfyDir, "" otherwise. +// uv-created venvs (Comfy-Desktop) ship a redirector python.exe whose real +// interpreter is the pyvenv.cfg "home" tree — typically a sibling of +// COMFY_DIR, which a sandbox/ACL covering COMFY_DIR alone does not reach. +func comfyVenvHome(comfyDir string) string { + data, err := os.ReadFile(filepath.Join(comfyDir, ".venv", "pyvenv.cfg")) + if err != nil { + return "" + } + for _, line := range strings.Split(string(data), "\n") { + k, v, ok := strings.Cut(line, "=") + if !ok || strings.TrimSpace(k) != "home" { + continue + } + home := strings.TrimSpace(v) + if home == "" { + return "" + } + if st, err := os.Stat(home); err != nil || !st.IsDir() { + return "" + } + rel, err := filepath.Rel(comfyDir, home) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + return home + } + return "" // inside comfyDir: already covered by the COMFY_DIR grant + } + return "" +} diff --git a/internal/service/venv_test.go b/internal/service/venv_test.go new file mode 100644 index 0000000..366e665 --- /dev/null +++ b/internal/service/venv_test.go @@ -0,0 +1,45 @@ +package service + +import ( + "os" + "path/filepath" + "testing" +) + +func TestComfyVenvHome(t *testing.T) { + root := t.TempDir() + comfy := filepath.Join(root, "ComfyUI") + outside := filepath.Join(root, "standalone-env") + inside := filepath.Join(comfy, "runtime") + for _, d := range []string{filepath.Join(comfy, ".venv"), outside, inside} { + if err := os.MkdirAll(d, 0o755); err != nil { + t.Fatal(err) + } + } + cfg := filepath.Join(comfy, ".venv", "pyvenv.cfg") + + write := func(home string) { + if err := os.WriteFile(cfg, []byte("home = "+home+"\nversion_info = 3.13.0\n"), 0o644); err != nil { + t.Fatal(err) + } + } + + write(outside) + if got := comfyVenvHome(comfy); got != outside { + t.Fatalf("outside home: got %q, want %q", got, outside) + } + + write(inside) + if got := comfyVenvHome(comfy); got != "" { + t.Fatalf("inside home: got %q, want empty", got) + } + + write(filepath.Join(root, "does-not-exist")) + if got := comfyVenvHome(comfy); got != "" { + t.Fatalf("missing home: got %q, want empty", got) + } + + if got := comfyVenvHome(filepath.Join(root, "no-venv")); got != "" { + t.Fatalf("no pyvenv.cfg: got %q, want empty", got) + } +}