diff --git a/internal/daemon/autostart_darwin.go b/internal/daemon/autostart_darwin.go
index 1367cc42..32be89e5 100644
--- a/internal/daemon/autostart_darwin.go
+++ b/internal/daemon/autostart_darwin.go
@@ -4,10 +4,12 @@ package daemon
import (
"fmt"
+ "html"
"os"
"os/exec"
"path/filepath"
"strconv"
+ "strings"
"text/template"
)
@@ -53,6 +55,10 @@ const plistTemplate = `
PATH
{{.EnvPath}}
+ {{- range $key, $val := .ExtraEnv}}
+ {{$key}}
+ {{xmlEscape $val}}
+ {{- end}}
@@ -66,6 +72,7 @@ type plistData struct {
Port int
LogFile string
EnvPath string
+ ExtraEnv map[string]string
}
// EnableAutostart creates the launchd plist and loads it.
@@ -89,6 +96,17 @@ func EnableAutostart(configPath string, port int) error {
envPath = "/usr/local/bin:/usr/bin:/bin"
}
+ extraEnv := make(map[string]string)
+ for _, e := range os.Environ() {
+ k, v, ok := strings.Cut(e, "=")
+ if !ok {
+ continue
+ }
+ if strings.HasPrefix(k, "ROUTATIC_PROXY_") {
+ extraEnv[k] = v
+ }
+ }
+
data := plistData{
Label: LaunchAgent,
BinaryPath: paths.BinaryPath,
@@ -96,9 +114,12 @@ func EnableAutostart(configPath string, port int) error {
Port: port,
LogFile: paths.LogFile,
EnvPath: envPath,
+ ExtraEnv: extraEnv,
}
- tmpl, err := template.New("plist").Parse(plistTemplate)
+ tmpl, err := template.New("plist").Funcs(template.FuncMap{
+ "xmlEscape": html.EscapeString,
+ }).Parse(plistTemplate)
if err != nil {
return fmt.Errorf("cannot parse plist template: %w", err)
}
diff --git a/internal/daemon/autostart_darwin_test.go b/internal/daemon/autostart_darwin_test.go
index 0d08677b..812a6209 100644
--- a/internal/daemon/autostart_darwin_test.go
+++ b/internal/daemon/autostart_darwin_test.go
@@ -13,6 +13,8 @@ func TestEnableDisableAutostart_Darwin(t *testing.T) {
// Setup temporary home directory
tempHome := t.TempDir()
t.Setenv("HOME", tempHome)
+ // Set a ROUTATIC_PROXY_* var with characters that need XML escaping.
+ t.Setenv("ROUTATIC_PROXY_API_KEY", "sk-test&key")
configPath := "/tmp/mock-config.json"
port := 9999
@@ -40,8 +42,11 @@ func TestEnableDisableAutostart_Darwin(t *testing.T) {
if !strings.Contains(content, "Label\n com.routatic.proxy") {
t.Errorf("Plist missing correct Label string")
}
- if !strings.Contains(content, "serve") {
- t.Errorf("Plist missing serve command")
+ if !strings.Contains(content, "start") {
+ t.Errorf("Plist missing start command")
+ }
+ if !strings.Contains(content, "--background") {
+ t.Errorf("Plist missing --background flag")
}
if !strings.Contains(content, "--config\n /tmp/mock-config.json") {
t.Errorf("Plist missing config path arguments")
@@ -50,6 +55,15 @@ func TestEnableDisableAutostart_Darwin(t *testing.T) {
t.Errorf("Plist missing port arguments")
}
+ // Verify ROUTATIC_PROXY_* env var is present and XML-escaped.
+ if !strings.Contains(content, "ROUTATIC_PROXY_API_KEY") {
+ t.Errorf("Plist missing ROUTATIC_PROXY_API_KEY env var")
+ }
+ // The value should be XML-escaped: & -> &, < -> <, > -> >
+ if !strings.Contains(content, "sk-test&key<value>") {
+ t.Errorf("Plist missing XML-escaped API key value. Content:\n%s", content)
+ }
+
// Disable autostart
err = DisableAutostart()
if err != nil {