sensorium-mcp 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/dashboard/routes/data.d.ts.map +1 -1
  2. package/dist/dashboard/routes/data.js +2 -1
  3. package/dist/dashboard/routes/data.js.map +1 -1
  4. package/dist/dashboard/routes/threads.js +1 -1
  5. package/dist/dashboard/routes/threads.js.map +1 -1
  6. package/dist/dashboard/routes.d.ts.map +1 -1
  7. package/dist/dashboard/routes.js +1 -3
  8. package/dist/dashboard/routes.js.map +1 -1
  9. package/dist/data/memory/migration-runner.d.ts +1 -1
  10. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  11. package/dist/data/memory/migration-runner.js +59 -3
  12. package/dist/data/memory/migration-runner.js.map +1 -1
  13. package/dist/data/memory/schema-ddl.d.ts +1 -1
  14. package/dist/data/memory/schema-ddl.d.ts.map +1 -1
  15. package/dist/data/memory/schema-ddl.js +2 -1
  16. package/dist/data/memory/schema-ddl.js.map +1 -1
  17. package/dist/data/memory/thread-registry.js +1 -1
  18. package/dist/data/memory/thread-registry.js.map +1 -1
  19. package/dist/http-server.d.ts.map +1 -1
  20. package/dist/http-server.js +1 -9
  21. package/dist/http-server.js.map +1 -1
  22. package/dist/index.js +3 -6
  23. package/dist/index.js.map +1 -1
  24. package/dist/server/factory.js +1 -1
  25. package/dist/server/factory.js.map +1 -1
  26. package/dist/services/agent-spawn.service.d.ts +7 -1
  27. package/dist/services/agent-spawn.service.d.ts.map +1 -1
  28. package/dist/services/agent-spawn.service.js +69 -45
  29. package/dist/services/agent-spawn.service.js.map +1 -1
  30. package/dist/services/consolidation.service.d.ts.map +1 -1
  31. package/dist/services/consolidation.service.js +49 -35
  32. package/dist/services/consolidation.service.js.map +1 -1
  33. package/dist/services/keeper.service.d.ts +21 -0
  34. package/dist/services/keeper.service.d.ts.map +1 -0
  35. package/dist/services/keeper.service.js +195 -0
  36. package/dist/services/keeper.service.js.map +1 -0
  37. package/dist/services/maintenance-signal.d.ts +2 -0
  38. package/dist/services/maintenance-signal.d.ts.map +1 -1
  39. package/dist/services/maintenance-signal.js +7 -1
  40. package/dist/services/maintenance-signal.js.map +1 -1
  41. package/dist/services/process.service.d.ts +19 -2
  42. package/dist/services/process.service.d.ts.map +1 -1
  43. package/dist/services/process.service.js +104 -10
  44. package/dist/services/process.service.js.map +1 -1
  45. package/dist/services/reconnect-snapshot.service.d.ts.map +1 -1
  46. package/dist/services/reconnect-snapshot.service.js +20 -3
  47. package/dist/services/reconnect-snapshot.service.js.map +1 -1
  48. package/dist/services/thread-lifecycle.service.d.ts +5 -0
  49. package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
  50. package/dist/services/thread-lifecycle.service.js +33 -8
  51. package/dist/services/thread-lifecycle.service.js.map +1 -1
  52. package/dist/services/worker-cleanup.service.d.ts +14 -1
  53. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  54. package/dist/services/worker-cleanup.service.js +48 -27
  55. package/dist/services/worker-cleanup.service.js.map +1 -1
  56. package/dist/sessions.d.ts +0 -5
  57. package/dist/sessions.d.ts.map +1 -1
  58. package/dist/sessions.js +0 -7
  59. package/dist/sessions.js.map +1 -1
  60. package/dist/stdio-server.d.ts.map +1 -1
  61. package/dist/stdio-server.js +1 -7
  62. package/dist/stdio-server.js.map +1 -1
  63. package/dist/tools/delegate-tool.d.ts.map +1 -1
  64. package/dist/tools/delegate-tool.js +2 -2
  65. package/dist/tools/delegate-tool.js.map +1 -1
  66. package/dist/tools/session-tools.js +1 -1
  67. package/dist/tools/session-tools.js.map +1 -1
  68. package/dist/tools/start-session-tool.d.ts.map +1 -1
  69. package/dist/tools/start-session-tool.js +8 -9
  70. package/dist/tools/start-session-tool.js.map +1 -1
  71. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  72. package/dist/tools/wait/message-processing.js +28 -0
  73. package/dist/tools/wait/message-processing.js.map +1 -1
  74. package/dist/tools/wait/poll-loop.js +1 -1
  75. package/dist/tools/wait/poll-loop.js.map +1 -1
  76. package/package.json +1 -1
  77. package/dist/tools/thread-lifecycle.d.ts +0 -6
  78. package/dist/tools/thread-lifecycle.d.ts.map +0 -1
  79. package/dist/tools/thread-lifecycle.js +0 -6
  80. package/dist/tools/thread-lifecycle.js.map +0 -1
  81. package/supervisor/config.go +0 -253
  82. package/supervisor/config_test.go +0 -78
  83. package/supervisor/go.mod +0 -15
  84. package/supervisor/go.sum +0 -20
  85. package/supervisor/health.go +0 -433
  86. package/supervisor/health_test.go +0 -93
  87. package/supervisor/keeper.go +0 -309
  88. package/supervisor/keeper_test.go +0 -27
  89. package/supervisor/lock.go +0 -57
  90. package/supervisor/lock_test.go +0 -54
  91. package/supervisor/log.go +0 -195
  92. package/supervisor/log_test.go +0 -125
  93. package/supervisor/main.go +0 -461
  94. package/supervisor/main_test.go +0 -130
  95. package/supervisor/notify.go +0 -53
  96. package/supervisor/process.go +0 -294
  97. package/supervisor/process_test.go +0 -108
  98. package/supervisor/process_unix.go +0 -14
  99. package/supervisor/process_windows.go +0 -15
  100. package/supervisor/secrets.go +0 -95
  101. package/supervisor/secrets_securevault_test.go +0 -98
  102. package/supervisor/secrets_test.go +0 -119
  103. package/supervisor/self_update.go +0 -282
  104. package/supervisor/self_update_test.go +0 -177
  105. package/supervisor/service_restart_stub.go +0 -9
  106. package/supervisor/service_restart_windows.go +0 -63
  107. package/supervisor/service_stub.go +0 -15
  108. package/supervisor/service_windows.go +0 -194
  109. package/supervisor/update_state.go +0 -264
  110. package/supervisor/update_state_test.go +0 -306
  111. package/supervisor/updater.go +0 -613
  112. package/supervisor/updater_test.go +0 -64
@@ -1,53 +0,0 @@
1
- package main
2
-
3
- import (
4
- "context"
5
- "fmt"
6
- "net/http"
7
- "net/url"
8
- "strings"
9
- "time"
10
- )
11
-
12
- // NotifyOperator sends a message via Telegram to the operator.
13
- // Silently fails if credentials are not configured.
14
- func NotifyOperator(cfg Config, log *Logger, text string, threadID int) {
15
- if cfg.TelegramToken == "" || cfg.TelegramChatID == "" {
16
- log.Debug("NotifyOperator: skipped (no Telegram credentials)")
17
- return
18
- }
19
-
20
- log.Debug("NotifyOperator: sending to chat %s (threadID=%d)", cfg.TelegramChatID, threadID)
21
-
22
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
23
- defer cancel()
24
-
25
- apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfg.TelegramToken)
26
-
27
- form := url.Values{}
28
- form.Set("chat_id", cfg.TelegramChatID)
29
- form.Set("text", text)
30
- form.Set("parse_mode", "HTML")
31
- if threadID > 0 {
32
- form.Set("message_thread_id", fmt.Sprintf("%d", threadID))
33
- }
34
-
35
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(form.Encode()))
36
- if err != nil {
37
- log.Warn("Telegram notify: %v", err)
38
- return
39
- }
40
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
41
-
42
- resp, err := http.DefaultClient.Do(req)
43
- if err != nil {
44
- log.Warn("Telegram notify: %v", err)
45
- return
46
- }
47
- defer resp.Body.Close()
48
- if resp.StatusCode >= 400 {
49
- log.Warn("Telegram notify: HTTP %d", resp.StatusCode)
50
- } else {
51
- log.Debug("Telegram notify: sent OK (HTTP %d)", resp.StatusCode)
52
- }
53
- }
@@ -1,294 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "errors"
6
- "fmt"
7
- "os"
8
- "os/exec"
9
- "path/filepath"
10
- "runtime"
11
- "strconv"
12
- "strings"
13
- "syscall"
14
- "time"
15
- )
16
-
17
- // SpawnMCPServer starts the MCP server as a detached child process.
18
- // Returns the PID of the spawned process.
19
- func SpawnMCPServer(cfg Config, log *Logger) (int, error) {
20
- parts := strings.Fields(cfg.MCPStartCommand)
21
- if len(parts) == 0 {
22
- return 0, errors.New("empty MCP_START_COMMAND")
23
- }
24
-
25
- if err := os.MkdirAll(filepath.Dir(cfg.Paths.MCPStderrLog), 0755); err != nil {
26
- return 0, fmt.Errorf("create MCP stderr log directory: %w", err)
27
- }
28
- stderrFile, err := os.OpenFile(cfg.Paths.MCPStderrLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
29
- if err != nil {
30
- return 0, fmt.Errorf("open MCP stderr log: %w", err)
31
- }
32
-
33
- cmd := exec.Command(parts[0], parts[1:]...)
34
- cmd.Env = os.Environ()
35
- for k, v := range cfg.ResolvedProfileEnv {
36
- if v == "" {
37
- continue
38
- }
39
- cmd.Env = upsertEnv(cmd.Env, k, v)
40
- }
41
- if cfg.MCPHttpPort > 0 {
42
- cmd.Env = upsertEnv(cmd.Env, "MCP_HTTP_PORT", strconv.Itoa(cfg.MCPHttpPort))
43
- }
44
- if cfg.MCPHttpSecret != "" {
45
- cmd.Env = upsertEnv(cmd.Env, "MCP_HTTP_SECRET", cfg.MCPHttpSecret)
46
- }
47
- if cfg.TelegramToken != "" {
48
- cmd.Env = upsertEnv(cmd.Env, "TELEGRAM_TOKEN", cfg.TelegramToken)
49
- }
50
- if cfg.TelegramChatID != "" {
51
- cmd.Env = upsertEnv(cmd.Env, "TELEGRAM_CHAT_ID", cfg.TelegramChatID)
52
- }
53
- cmd.Stdin = nil
54
- cmd.Stdout = nil
55
- cmd.Stderr = stderrFile
56
- setSysProcAttr(cmd)
57
-
58
- log.Info("Starting MCP server: %s", cfg.MCPStartCommand)
59
- log.Info("Capturing MCP server stderr to %s", cfg.Paths.MCPStderrLog)
60
-
61
- if err := cmd.Start(); err != nil {
62
- _ = stderrFile.Close()
63
- return 0, fmt.Errorf("spawn MCP server: %w", err)
64
- }
65
-
66
- pid := cmd.Process.Pid
67
- log.Info("MCP server started with PID %d", pid)
68
-
69
- // Don't wait — detached process
70
- go func() {
71
- _ = cmd.Wait()
72
- _ = stderrFile.Close()
73
- }()
74
-
75
- if err := writePIDFile(cfg.Paths.ServerPID, pid); err != nil {
76
- log.Warn("Failed to write server PID file: %v", err)
77
- }
78
-
79
- return pid, nil
80
- }
81
-
82
- func upsertEnv(env []string, key, value string) []string {
83
- prefix := key + "="
84
- for i, kv := range env {
85
- if strings.HasPrefix(kv, prefix) {
86
- env[i] = prefix + value
87
- return env
88
- }
89
- }
90
- return append(env, prefix+value)
91
- }
92
-
93
- // KillProcess kills a process by PID. On Windows, uses taskkill /F /T for tree kill.
94
- func KillProcess(pid int, log *Logger) error {
95
- if !IsProcessAlive(pid) {
96
- log.Debug("KillProcess: PID %d already dead", pid)
97
- return nil
98
- }
99
-
100
- log.Debug("KillProcess: killing PID %d", pid)
101
-
102
- if runtime.GOOS == "windows" {
103
- // taskkill /F /T kills the tree
104
- out, err := exec.Command("taskkill", "/F", "/T", "/PID", strconv.Itoa(pid)).CombinedOutput()
105
- if err != nil {
106
- return fmt.Errorf("taskkill PID %d: %w (%s)", pid, err, strings.TrimSpace(string(out)))
107
- }
108
- log.Info("Killed process tree PID %d", pid)
109
- return nil
110
- }
111
-
112
- // Unix: SIGTERM, wait 2s, then SIGKILL
113
- proc, err := os.FindProcess(pid)
114
- if err != nil {
115
- log.Debug("KillProcess: FindProcess(%d) failed: %v", pid, err)
116
- return err
117
- }
118
- if err := proc.Signal(syscall.SIGTERM); err != nil {
119
- // Already dead
120
- return nil
121
- }
122
- time.Sleep(2 * time.Second)
123
- if IsProcessAlive(pid) {
124
- _ = proc.Kill()
125
- log.Info("Force-killed PID %d", pid)
126
- } else {
127
- log.Info("Process PID %d terminated gracefully", pid)
128
- }
129
- return nil
130
- }
131
-
132
- // KillByPort finds a process listening on the given port and kills it (Windows-only orphan cleanup).
133
- func KillByPort(port int, log *Logger) {
134
- if runtime.GOOS != "windows" || port <= 0 || port > 65535 {
135
- return
136
- }
137
- log.Debug("KillByPort: checking for processes on port %d", port)
138
- out, err := exec.Command("cmd", "/c", fmt.Sprintf("netstat -aon | findstr \":%d.*LISTENING\"", port)).CombinedOutput()
139
- if err != nil {
140
- log.Debug("KillByPort: no listeners on port %d", port)
141
- return
142
- }
143
- for _, line := range strings.Split(string(out), "\n") {
144
- fields := strings.Fields(strings.TrimSpace(line))
145
- if len(fields) >= 5 {
146
- pid, err := strconv.Atoi(fields[len(fields)-1])
147
- if err == nil && pid > 0 {
148
- log.Info("Found orphan PID %d on port %d — killing", pid, port)
149
- _ = KillProcess(pid, log)
150
- }
151
- }
152
- }
153
- }
154
-
155
- // IsProcessAlive checks whether a process with the given PID exists.
156
- func IsProcessAlive(pid int) bool {
157
- if pid <= 0 {
158
- return false
159
- }
160
- if runtime.GOOS == "windows" {
161
- out, err := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH").CombinedOutput()
162
- if err != nil {
163
- return false
164
- }
165
- return strings.Contains(string(out), strconv.Itoa(pid))
166
- }
167
- proc, err := os.FindProcess(pid)
168
- if err != nil {
169
- return false
170
- }
171
- return proc.Signal(syscall.Signal(0)) == nil // signal 0 = existence check on Unix
172
- }
173
-
174
- // --- PID File Helpers ---
175
-
176
- type pidJSON struct {
177
- PID int `json:"pid"`
178
- }
179
-
180
- // ReadPIDFile reads a PID from a file. Supports both JSON {"pid":123} and raw integer formats.
181
- func ReadPIDFile(path string) (int, error) {
182
- data, err := os.ReadFile(path)
183
- if err != nil {
184
- return 0, err
185
- }
186
- raw := strings.TrimSpace(string(data))
187
-
188
- // Try JSON first
189
- var pj pidJSON
190
- if json.Unmarshal([]byte(raw), &pj) == nil && pj.PID > 0 {
191
- return pj.PID, nil
192
- }
193
-
194
- // Fallback: raw integer
195
- pid, err := strconv.Atoi(raw)
196
- if err != nil {
197
- return 0, fmt.Errorf("invalid PID file content: %q", raw)
198
- }
199
- if pid <= 0 {
200
- return 0, fmt.Errorf("invalid PID: %d", pid)
201
- }
202
- return pid, nil
203
- }
204
-
205
- func writePIDFile(path string, pid int) error {
206
- dir := filepath.Dir(path)
207
- if err := os.MkdirAll(dir, 0755); err != nil {
208
- return err
209
- }
210
- data, _ := json.Marshal(pidJSON{PID: pid})
211
- return atomicWrite(path, data)
212
- }
213
-
214
- // atomicWrite writes data to a temp file then renames — prevents partial reads.
215
- func atomicWrite(path string, data []byte) error {
216
- tmp := fmt.Sprintf("%s.tmp.%d", path, os.Getpid())
217
- if err := os.WriteFile(tmp, data, 0644); err != nil {
218
- return err
219
- }
220
- return os.Rename(tmp, path)
221
- }
222
-
223
- // ListThreadPIDs returns a map of threadId → PID from the pids directory.
224
- func ListThreadPIDs(pidsDir string) map[string]int {
225
- result := make(map[string]int)
226
- entries, err := os.ReadDir(pidsDir)
227
- if err != nil {
228
- // directory may not exist yet
229
- return result
230
- }
231
- for _, e := range entries {
232
- if e.IsDir() || !strings.HasSuffix(e.Name(), ".pid") {
233
- continue
234
- }
235
- threadID := strings.TrimSuffix(e.Name(), ".pid")
236
- pid, err := ReadPIDFile(filepath.Join(pidsDir, e.Name()))
237
- if err != nil {
238
- continue
239
- }
240
- result[threadID] = pid
241
- }
242
- return result
243
- }
244
-
245
- // KillOrphanThreads kills any alive processes listed in PID files (leftovers from
246
- // a previous supervisor run) and then removes the PID files. Called on startup
247
- // before the MCP server is launched, so all thread PIDs are guaranteed to be stale.
248
- func KillOrphanThreads(pidsDir string, log *Logger) {
249
- pids := ListThreadPIDs(pidsDir)
250
- if len(pids) == 0 {
251
- log.Debug("KillOrphanThreads: no PID files found")
252
- return
253
- }
254
- log.Info("KillOrphanThreads: checking %d PID files for orphan processes", len(pids))
255
- killed := 0
256
- for threadID, pid := range pids {
257
- path := filepath.Join(pidsDir, threadID+".pid")
258
- if IsProcessAlive(pid) {
259
- log.Info("Killing orphan thread %s (PID %d)", threadID, pid)
260
- if err := KillProcess(pid, log); err != nil {
261
- log.Warn("Failed to kill orphan PID %d: %v", pid, err)
262
- }
263
- killed++
264
- }
265
- _ = os.Remove(path)
266
- }
267
- if killed > 0 {
268
- log.Info("KillOrphanThreads: killed %d orphan processes, cleaned %d PID files", killed, len(pids))
269
- } else {
270
- log.Info("KillOrphanThreads: no orphans found, cleaned %d stale PID files", len(pids))
271
- }
272
- }
273
-
274
- // CleanStalePIDs removes PID files for processes that are no longer running.
275
- func CleanStalePIDs(pidsDir string, log *Logger) {
276
- pids := ListThreadPIDs(pidsDir)
277
- if len(pids) == 0 {
278
- log.Debug("CleanStalePIDs: no PID files found in %s", pidsDir)
279
- return
280
- }
281
- log.Debug("CleanStalePIDs: checking %d PID files", len(pids))
282
- cleaned := 0
283
- for threadID, pid := range pids {
284
- if !IsProcessAlive(pid) {
285
- path := filepath.Join(pidsDir, threadID+".pid")
286
- log.Info("Removing stale PID file for thread %s (PID %d)", threadID, pid)
287
- _ = os.Remove(path)
288
- cleaned++
289
- }
290
- }
291
- if cleaned > 0 {
292
- log.Info("CleanStalePIDs: removed %d stale PID files", cleaned)
293
- }
294
- }
@@ -1,108 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "path/filepath"
6
- "testing"
7
- )
8
-
9
- func TestReadPIDFile_JSON(t *testing.T) {
10
- dir := t.TempDir()
11
- path := filepath.Join(dir, "test.pid")
12
- os.WriteFile(path, []byte(`{"pid":12345}`), 0644)
13
-
14
- pid, err := ReadPIDFile(path)
15
- if err != nil {
16
- t.Fatalf("unexpected error: %v", err)
17
- }
18
- if pid != 12345 {
19
- t.Errorf("got %d, want 12345", pid)
20
- }
21
- }
22
-
23
- func TestReadPIDFile_RawInt(t *testing.T) {
24
- dir := t.TempDir()
25
- path := filepath.Join(dir, "test.pid")
26
- os.WriteFile(path, []byte("54321\n"), 0644)
27
-
28
- pid, err := ReadPIDFile(path)
29
- if err != nil {
30
- t.Fatalf("unexpected error: %v", err)
31
- }
32
- if pid != 54321 {
33
- t.Errorf("got %d, want 54321", pid)
34
- }
35
- }
36
-
37
- func TestReadPIDFile_Invalid(t *testing.T) {
38
- dir := t.TempDir()
39
- path := filepath.Join(dir, "test.pid")
40
- os.WriteFile(path, []byte("not-a-pid"), 0644)
41
-
42
- _, err := ReadPIDFile(path)
43
- if err == nil {
44
- t.Fatal("expected error for invalid PID content")
45
- }
46
- }
47
-
48
- func TestReadPIDFile_Missing(t *testing.T) {
49
- _, err := ReadPIDFile(filepath.Join(t.TempDir(), "missing.pid"))
50
- if err == nil {
51
- t.Fatal("expected error for missing file")
52
- }
53
- }
54
-
55
- func TestAtomicWrite(t *testing.T) {
56
- dir := t.TempDir()
57
- path := filepath.Join(dir, "data.txt")
58
-
59
- if err := atomicWrite(path, []byte("hello")); err != nil {
60
- t.Fatalf("atomicWrite failed: %v", err)
61
- }
62
- data, err := os.ReadFile(path)
63
- if err != nil {
64
- t.Fatalf("ReadFile failed: %v", err)
65
- }
66
- if string(data) != "hello" {
67
- t.Errorf("got %q, want %q", string(data), "hello")
68
- }
69
- }
70
-
71
- func TestListThreadPIDs(t *testing.T) {
72
- dir := t.TempDir()
73
- os.WriteFile(filepath.Join(dir, "1234.pid"), []byte(`{"pid":100}`), 0644)
74
- os.WriteFile(filepath.Join(dir, "5678.pid"), []byte("200"), 0644)
75
- os.WriteFile(filepath.Join(dir, "not-a-pid.txt"), []byte("300"), 0644)
76
-
77
- result := ListThreadPIDs(dir)
78
- if len(result) != 2 {
79
- t.Fatalf("got %d entries, want 2", len(result))
80
- }
81
- if result["1234"] != 100 {
82
- t.Errorf("result[1234] = %d, want 100", result["1234"])
83
- }
84
- if result["5678"] != 200 {
85
- t.Errorf("result[5678] = %d, want 200", result["5678"])
86
- }
87
- }
88
-
89
- func TestListThreadPIDs_MissingDir(t *testing.T) {
90
- result := ListThreadPIDs(filepath.Join(t.TempDir(), "no-such-dir"))
91
- if len(result) != 0 {
92
- t.Errorf("expected empty map for missing directory, got %d entries", len(result))
93
- }
94
- }
95
-
96
- func TestUpsertEnv_ReplacesExistingAndAppendsMissing(t *testing.T) {
97
- env := []string{"A=1", "B=2"}
98
-
99
- env = upsertEnv(env, "B", "updated")
100
- if env[1] != "B=updated" {
101
- t.Fatalf("expected existing key to be replaced, got %q", env[1])
102
- }
103
-
104
- env = upsertEnv(env, "C", "3")
105
- if env[len(env)-1] != "C=3" {
106
- t.Fatalf("expected missing key to be appended, got %q", env[len(env)-1])
107
- }
108
- }
@@ -1,14 +0,0 @@
1
- //go:build !windows
2
-
3
- package main
4
-
5
- import (
6
- "os/exec"
7
- "syscall"
8
- )
9
-
10
- func setSysProcAttr(cmd *exec.Cmd) {
11
- cmd.SysProcAttr = &syscall.SysProcAttr{
12
- Setsid: true, // detach from parent session
13
- }
14
- }
@@ -1,15 +0,0 @@
1
- //go:build windows
2
-
3
- package main
4
-
5
- import (
6
- "os/exec"
7
- "syscall"
8
- )
9
-
10
- func setSysProcAttr(cmd *exec.Cmd) {
11
- cmd.SysProcAttr = &syscall.SysProcAttr{
12
- CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
13
- HideWindow: true,
14
- }
15
- }
@@ -1,95 +0,0 @@
1
- package main
2
-
3
- import (
4
- "errors"
5
- "fmt"
6
- "os"
7
- "strconv"
8
-
9
- "github.com/zalando/go-keyring"
10
-
11
- sv "github.com/andriyshevchenko/SecureVault/securevault-go"
12
- )
13
-
14
- const defaultKeyringService = "sensorium-supervisor"
15
-
16
- var keyringGet = keyring.Get
17
-
18
- // resolveSecretWithKeyring returns environment value first, then keyring fallback.
19
- // If both sources are unavailable, it returns an empty string.
20
- func resolveSecretWithKeyring(envKey, keyringService string) string {
21
- if v := os.Getenv(envKey); v != "" {
22
- return v
23
- }
24
-
25
- if keyringService == "" {
26
- return ""
27
- }
28
-
29
- secret, err := keyringGet(keyringService, envKey)
30
- if err != nil {
31
- if errors.Is(err, keyring.ErrNotFound) {
32
- return ""
33
- }
34
- fmt.Fprintf(os.Stderr, "WARN: keyring lookup failed for %s (service=%s): %v\n", envKey, keyringService, err)
35
- return ""
36
- }
37
- return secret
38
- }
39
-
40
- // resolveIntWithKeyring parses an integer value from env first, then keyring fallback.
41
- // If parsing fails or no value exists, it returns fallback.
42
- func resolveIntWithKeyring(envKey, keyringService string, fallback int) int {
43
- v := resolveSecretWithKeyring(envKey, keyringService)
44
- if v == "" {
45
- return fallback
46
- }
47
- parsed, err := strconv.Atoi(v)
48
- if err != nil {
49
- fmt.Fprintf(os.Stderr, "WARN: invalid integer for %s: %q\n", envKey, v)
50
- return fallback
51
- }
52
- return parsed
53
- }
54
-
55
- // resolveFromSecureVault looks up envKey in the named SecureVault profile.
56
- // baseDir may be empty (defaults to %LOCALAPPDATA%\SecureVault).
57
- // Returns empty string on any error, with a warning for unexpected failures.
58
- func resolveFromSecureVault(profileName, envKey, baseDir string) string {
59
- store := sv.NewStore(baseDir)
60
- val, err := store.ResolveKey(profileName, envKey)
61
- if err != nil {
62
- if errors.Is(err, sv.ErrNotFound) || errors.Is(err, sv.ErrUnsupportedPlatform) {
63
- return ""
64
- }
65
- fmt.Fprintf(os.Stderr, "WARN: securevault lookup failed for %s (profile=%s): %v\n", envKey, profileName, err)
66
- return ""
67
- }
68
- return val
69
- }
70
-
71
- // resolveStringChain resolves a string config key using the chain:
72
- // environment variable → SecureVault profile → OS keyring → empty string.
73
- func resolveStringChain(envKey, svProfile, svBaseDir, keyringService string) string {
74
- if v := os.Getenv(envKey); v != "" {
75
- return v
76
- }
77
- if v := resolveFromSecureVault(svProfile, envKey, svBaseDir); v != "" {
78
- return v
79
- }
80
- return resolveSecretWithKeyring(envKey, keyringService)
81
- }
82
-
83
- // resolveIntChain resolves an integer config key using the same chain as resolveStringChain.
84
- func resolveIntChain(envKey, svProfile, svBaseDir, keyringService string, fallback int) int {
85
- v := resolveStringChain(envKey, svProfile, svBaseDir, keyringService)
86
- if v == "" {
87
- return fallback
88
- }
89
- parsed, err := strconv.Atoi(v)
90
- if err != nil {
91
- fmt.Fprintf(os.Stderr, "WARN: invalid integer for %s: %q\n", envKey, v)
92
- return fallback
93
- }
94
- return parsed
95
- }
@@ -1,98 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "os"
6
- "path/filepath"
7
- "testing"
8
-
9
- sv "github.com/andriyshevchenko/SecureVault/securevault-go"
10
- "github.com/zalando/go-keyring"
11
- )
12
-
13
- // writeProfileFixture writes a minimal profiles.json to dir and returns the dir.
14
- func writeProfileFixture(t *testing.T, dir string, profiles []sv.Profile) {
15
- t.Helper()
16
- data, err := json.Marshal(profiles)
17
- if err != nil {
18
- t.Fatalf("marshal profiles: %v", err)
19
- }
20
- if err := os.WriteFile(filepath.Join(dir, "profiles.json"), data, 0600); err != nil {
21
- t.Fatalf("write profiles.json: %v", err)
22
- }
23
- }
24
-
25
- func TestResolveStringChain_EnvWins(t *testing.T) {
26
- t.Setenv("TELEGRAM_TOKEN", "env-value")
27
- dir := t.TempDir()
28
- writeProfileFixture(t, dir, []sv.Profile{
29
- {ID: "p1", Name: "TEST", Mappings: []sv.ProfileMapping{
30
- {EnvVar: "TELEGRAM_TOKEN", SecretID: "no-cred"},
31
- }},
32
- })
33
-
34
- got := resolveStringChain("TELEGRAM_TOKEN", "TEST", dir, "")
35
- if got != "env-value" {
36
- t.Errorf("resolveStringChain = %q, want env-value", got)
37
- }
38
- }
39
-
40
- func TestResolveStringChain_SecureVaultFallback_MissingCred_UsesKeyring(t *testing.T) {
41
- t.Setenv("TELEGRAM_TOKEN", "")
42
- dir := t.TempDir()
43
- writeProfileFixture(t, dir, []sv.Profile{
44
- {ID: "p1", Name: "TEST", Mappings: []sv.ProfileMapping{
45
- {EnvVar: "TELEGRAM_TOKEN", SecretID: "non-existent-cred"},
46
- }},
47
- })
48
-
49
- orig := keyringGet
50
- keyringGet = func(service, user string) (string, error) {
51
- if service != "sensorium-supervisor" {
52
- t.Fatalf("service = %q, want %q", service, "sensorium-supervisor")
53
- }
54
- if user != "TELEGRAM_TOKEN" {
55
- t.Fatalf("user = %q, want %q", user, "TELEGRAM_TOKEN")
56
- }
57
- return "from-keyring", nil
58
- }
59
- t.Cleanup(func() { keyringGet = orig })
60
-
61
- // Credential not in store → falls through to keyring.
62
- got := resolveStringChain("TELEGRAM_TOKEN", "TEST", dir, "sensorium-supervisor")
63
- if got != "from-keyring" {
64
- t.Errorf("resolveStringChain = %q, want from-keyring", got)
65
- }
66
- }
67
-
68
- func TestResolveStringChain_NoProfile_FallsToKeyring(t *testing.T) {
69
- t.Setenv("TELEGRAM_TOKEN", "")
70
-
71
- orig := keyringGet
72
- keyringGet = func(service, user string) (string, error) {
73
- return "", keyring.ErrNotFound
74
- }
75
- t.Cleanup(func() { keyringGet = orig })
76
-
77
- // No profiles file in dir → securevault returns empty → keyring path taken.
78
- got := resolveStringChain("TELEGRAM_TOKEN", "NONEXISTENT_PROFILE", t.TempDir(), "sensorium-supervisor")
79
- if got != "" {
80
- t.Errorf("resolveStringChain = %q, want empty", got)
81
- }
82
- }
83
-
84
- func TestResolveIntChain_EnvWins(t *testing.T) {
85
- t.Setenv("MCP_HTTP_PORT", "4567")
86
- got := resolveIntChain("MCP_HTTP_PORT", "TEST", t.TempDir(), "", 0)
87
- if got != 4567 {
88
- t.Errorf("resolveIntChain = %d, want 4567", got)
89
- }
90
- }
91
-
92
- func TestResolveIntChain_InvalidFallback(t *testing.T) {
93
- t.Setenv("MCP_HTTP_PORT", "not-a-number")
94
- got := resolveIntChain("MCP_HTTP_PORT", "TEST", t.TempDir(), "", 9999)
95
- if got != 9999 {
96
- t.Errorf("resolveIntChain invalid = %d, want fallback 9999", got)
97
- }
98
- }