Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions internal/core/local_runtime/subprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"slices"
"time"

"github.com/langgenius/dify-plugin-daemon/internal/types/app"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/constants"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
routinepkg "github.com/langgenius/dify-plugin-daemon/pkg/routine"
Expand All @@ -32,19 +33,66 @@ func (r *LocalPluginRuntime) getInstanceCmd() (*exec.Cmd, error) {
return nil, fmt.Errorf("unsupported language: %s", r.Config.Meta.Runner.Language)
}

cmd.Env = cmd.Environ()
if r.appConfig.HttpsProxy != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", r.appConfig.HttpsProxy))
cmd.Env = BuildPluginCommandEnv(r.appConfig)
cmd.Dir = r.State.WorkingPath
return cmd, nil
}

// pluginCommandEnvAllowlist lists the only process environment variables a
// plugin subprocess may inherit. Daemon credentials such as DB_PASSWORD,
// SERVER_KEY or DIFY_INNER_API_KEY must never reach plugin code.
var pluginCommandEnvAllowlist = []string{
"PATH",
"HOME",
"LANG",
"LC_ALL",
"LC_CTYPE",
"TMPDIR",
"TEMP",
"TMP",
"TZ",
"SSL_CERT_FILE",
"REQUESTS_CA_BUNDLE",
"HTTP_PROXY",
"HTTPS_PROXY",
"NO_PROXY",
"http_proxy",
"https_proxy",
"no_proxy",
}

// BuildPluginCommandEnv builds the environment of a plugin subprocess from an
// explicit allowlist instead of inheriting the daemon process environment,
// mirroring buildUVCommandEnv for the dependency installer. Proxy settings
// from the daemon config take precedence over inherited proxy variables.
func BuildPluginCommandEnv(appConfig *app.Config) []string {
envByKey := make(map[string]string, len(pluginCommandEnvAllowlist)+4)
for _, key := range pluginCommandEnvAllowlist {
if value, ok := os.LookupEnv(key); ok {
envByKey[key] = value
}
}
if r.appConfig.HttpProxy != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", r.appConfig.HttpProxy))

if appConfig != nil {
if appConfig.HttpProxy != "" {
envByKey["HTTP_PROXY"] = appConfig.HttpProxy
}
if appConfig.HttpsProxy != "" {
envByKey["HTTPS_PROXY"] = appConfig.HttpsProxy
}
if appConfig.NoProxy != "" {
envByKey["NO_PROXY"] = appConfig.NoProxy
}
}
if r.appConfig.NoProxy != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("NO_PROXY=%s", r.appConfig.NoProxy))

envByKey["INSTALL_METHOD"] = "local"

env := make([]string, 0, len(envByKey))
for key, value := range envByKey {
env = append(env, key+"="+value)
}
cmd.Env = append(cmd.Env, "INSTALL_METHOD=local", "PATH="+os.Getenv("PATH"))
cmd.Dir = r.State.WorkingPath
return cmd, nil
slices.Sort(env)
return env
}

// getInstanceStdio gets the stdin, stdout, and stderr pipes for the plugin instance
Expand Down
98 changes: 98 additions & 0 deletions internal/core/local_runtime/subprocess_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package local_runtime

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/langgenius/dify-plugin-daemon/internal/types/app"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/constants"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
"github.com/stretchr/testify/require"
)

func pluginEnvSliceToMap(t *testing.T, env []string) map[string]string {
t.Helper()
envByKey := make(map[string]string, len(env))
for _, item := range env {
key, value, found := strings.Cut(item, "=")
require.True(t, found)
envByKey[key] = value
}
return envByKey
}

func TestBuildPluginCommandEnv(t *testing.T) {
t.Setenv("PATH", "/test/bin")
t.Setenv("HOME", "/test/home")
t.Setenv("LANG", "en_US.UTF-8")
t.Setenv("HTTP_PROXY", "http://env-proxy:8080")
t.Setenv("DB_PASSWORD", "must-not-be-inherited")
t.Setenv("SERVER_KEY", "must-not-be-inherited")
t.Setenv("DIFY_INNER_API_KEY", "must-not-be-inherited")
t.Setenv("AWS_ACCESS_KEY_ID", "must-not-be-inherited")
t.Setenv("AWS_SECRET_ACCESS_KEY", "must-not-be-inherited")
t.Setenv("REDIS_PASSWORD", "must-not-be-inherited")
t.Setenv("ADMIN_API_KEY", "must-not-be-inherited")
t.Setenv("UNRELATED_SECRET", "must-not-be-inherited")

env := BuildPluginCommandEnv(&app.Config{
HttpsProxy: "https://config-proxy:8443",
NoProxy: "localhost,127.0.0.1",
})
envByKey := pluginEnvSliceToMap(t, env)

// allowlisted variables are passed through
require.Equal(t, "/test/bin", envByKey["PATH"])
require.Equal(t, "/test/home", envByKey["HOME"])
require.Equal(t, "en_US.UTF-8", envByKey["LANG"])
require.Equal(t, "http://env-proxy:8080", envByKey["HTTP_PROXY"])
require.Equal(t, "local", envByKey["INSTALL_METHOD"])

// proxy settings from the daemon config take precedence
require.Equal(t, "https://config-proxy:8443", envByKey["HTTPS_PROXY"])
require.Equal(t, "localhost,127.0.0.1", envByKey["NO_PROXY"])

// daemon secrets never reach the plugin process
for _, key := range []string{
"DB_PASSWORD",
"SERVER_KEY",
"DIFY_INNER_API_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"REDIS_PASSWORD",
"ADMIN_API_KEY",
"UNRELATED_SECRET",
} {
require.NotContains(t, envByKey, key)
}
}

func TestGetInstanceCmdDoesNotInheritDaemonEnv(t *testing.T) {
t.Setenv("PATH", "/test/bin")
t.Setenv("DB_PASSWORD", "must-not-be-inherited")
t.Setenv("SERVER_KEY", "must-not-be-inherited")
t.Setenv("DIFY_INNER_API_KEY", "must-not-be-inherited")

// minimal fake venv so getVirtualEnvironmentPythonPath succeeds
workDir := t.TempDir()
venvBin := filepath.Join(workDir, ".venv", "bin")
require.NoError(t, os.MkdirAll(venvBin, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(venvBin, "python"), []byte("#!/bin/sh\n"), 0o755))

r := &LocalPluginRuntime{appConfig: &app.Config{}}
r.State = plugin_entities.PluginRuntimeState{WorkingPath: workDir}
r.Config.Meta.Runner.Language = constants.Python
r.Config.Meta.Runner.Entrypoint = "main"

cmd, err := r.getInstanceCmd()
require.NoError(t, err)

envByKey := pluginEnvSliceToMap(t, cmd.Env)
require.Equal(t, "/test/bin", envByKey["PATH"])
require.Equal(t, "local", envByKey["INSTALL_METHOD"])
require.NotContains(t, envByKey, "DB_PASSWORD")
require.NotContains(t, envByKey, "SERVER_KEY")
require.NotContains(t, envByKey, "DIFY_INNER_API_KEY")
}
2 changes: 1 addition & 1 deletion pkg/slim/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func execPlugin(

cmd := exec.Command(pythonPath, "-m", rt.Config.Meta.Runner.Entrypoint)
cmd.Dir = rt.State.WorkingPath
cmd.Env = append(os.Environ(), "INSTALL_METHOD=local", "PATH="+os.Getenv("PATH"))
cmd.Env = local_runtime.BuildPluginCommandEnv(appConfig)

stdin, err := cmd.StdinPipe()
if err != nil {
Expand Down
Loading