sensorium-mcp 2.17.28 → 3.0.0
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.
- package/Install-Sensorium.ps1 +327 -0
- package/README.md +14 -0
- package/dist/config.d.ts +16 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -2
- package/dist/config.js.map +1 -1
- package/dist/daily-session.d.ts +2 -1
- package/dist/daily-session.d.ts.map +1 -1
- package/dist/daily-session.js +23 -26
- package/dist/daily-session.js.map +1 -1
- package/dist/dashboard/routes/settings.d.ts +4 -0
- package/dist/dashboard/routes/settings.d.ts.map +1 -1
- package/dist/dashboard/routes/settings.js +57 -1
- package/dist/dashboard/routes/settings.js.map +1 -1
- package/dist/dashboard/routes/threads.d.ts +1 -0
- package/dist/dashboard/routes/threads.d.ts.map +1 -1
- package/dist/dashboard/routes/threads.js +23 -25
- package/dist/dashboard/routes/threads.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +7 -2
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/spa.html +11 -11
- package/dist/data/interfaces.d.ts +36 -0
- package/dist/data/interfaces.d.ts.map +1 -0
- package/dist/data/interfaces.js +2 -0
- package/dist/data/interfaces.js.map +1 -0
- package/dist/data/memory/bootstrap.d.ts +36 -16
- package/dist/data/memory/bootstrap.d.ts.map +1 -1
- package/dist/data/memory/bootstrap.js +71 -217
- package/dist/data/memory/bootstrap.js.map +1 -1
- package/dist/data/memory/consolidation.d.ts +35 -34
- package/dist/data/memory/consolidation.d.ts.map +1 -1
- package/dist/data/memory/consolidation.js +43 -554
- package/dist/data/memory/consolidation.js.map +1 -1
- package/dist/data/memory/migration-runner.d.ts +5 -0
- package/dist/data/memory/migration-runner.d.ts.map +1 -0
- package/dist/data/memory/migration-runner.js +403 -0
- package/dist/data/memory/migration-runner.js.map +1 -0
- package/dist/data/memory/reflection.js +1 -1
- package/dist/data/memory/schema-ddl.d.ts +4 -0
- package/dist/data/memory/schema-ddl.d.ts.map +1 -0
- package/dist/data/memory/schema-ddl.js +194 -0
- package/dist/data/memory/schema-ddl.js.map +1 -0
- package/dist/data/memory/schema-guard.d.ts +3 -0
- package/dist/data/memory/schema-guard.d.ts.map +1 -0
- package/dist/data/memory/schema-guard.js +184 -0
- package/dist/data/memory/schema-guard.js.map +1 -0
- package/dist/data/memory/schema.d.ts +2 -5
- package/dist/data/memory/schema.d.ts.map +1 -1
- package/dist/data/memory/schema.js +6 -834
- package/dist/data/memory/schema.js.map +1 -1
- package/dist/data/memory/synthesis.js +2 -2
- package/dist/data/memory/synthesis.js.map +1 -1
- package/dist/data/memory/thread-registry.d.ts +18 -4
- package/dist/data/memory/thread-registry.d.ts.map +1 -1
- package/dist/data/memory/thread-registry.js +25 -0
- package/dist/data/memory/thread-registry.js.map +1 -1
- package/dist/data/sent-message.repository.d.ts +12 -0
- package/dist/data/sent-message.repository.d.ts.map +1 -0
- package/dist/data/sent-message.repository.js +31 -0
- package/dist/data/sent-message.repository.js.map +1 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +23 -2
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +27 -48
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +7 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +89 -12
- package/dist/logger.js.map +1 -1
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +15 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/server/factory.d.ts +2 -1
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +11 -4
- package/dist/server/factory.js.map +1 -1
- package/dist/services/agent-spawn.service.d.ts +39 -0
- package/dist/services/agent-spawn.service.d.ts.map +1 -0
- package/dist/services/agent-spawn.service.js +348 -0
- package/dist/services/agent-spawn.service.js.map +1 -0
- package/dist/services/background-runner.d.ts +26 -0
- package/dist/services/background-runner.d.ts.map +1 -0
- package/dist/services/background-runner.js +71 -0
- package/dist/services/background-runner.js.map +1 -0
- package/dist/services/consolidation.service.d.ts +16 -0
- package/dist/services/consolidation.service.d.ts.map +1 -0
- package/dist/services/consolidation.service.js +508 -0
- package/dist/services/consolidation.service.js.map +1 -0
- package/dist/services/dispatcher/broker.d.ts +2 -0
- package/dist/services/dispatcher/broker.d.ts.map +1 -1
- package/dist/services/dispatcher/broker.js +5 -10
- package/dist/services/dispatcher/broker.js.map +1 -1
- package/dist/services/dispatcher/index.d.ts +1 -1
- package/dist/services/dispatcher/index.d.ts.map +1 -1
- package/dist/services/dispatcher/index.js +1 -1
- package/dist/services/dispatcher/index.js.map +1 -1
- package/dist/services/dispatcher/lock.d.ts.map +1 -1
- package/dist/services/dispatcher/lock.js +7 -11
- package/dist/services/dispatcher/lock.js.map +1 -1
- package/dist/services/maintenance-signal.d.ts +18 -0
- package/dist/services/maintenance-signal.d.ts.map +1 -0
- package/dist/services/maintenance-signal.js +48 -0
- package/dist/services/maintenance-signal.js.map +1 -0
- package/dist/services/memory-briefing.service.d.ts +4 -0
- package/dist/services/memory-briefing.service.d.ts.map +1 -0
- package/dist/services/memory-briefing.service.js +143 -0
- package/dist/services/memory-briefing.service.js.map +1 -0
- package/dist/services/process.service.d.ts +31 -0
- package/dist/services/process.service.d.ts.map +1 -0
- package/dist/services/process.service.js +100 -0
- package/dist/services/process.service.js.map +1 -0
- package/dist/services/thread-health.service.d.ts +18 -0
- package/dist/services/thread-health.service.d.ts.map +1 -0
- package/dist/services/thread-health.service.js +118 -0
- package/dist/services/thread-health.service.js.map +1 -0
- package/dist/services/thread-lifecycle.service.d.ts +52 -0
- package/dist/services/thread-lifecycle.service.d.ts.map +1 -0
- package/dist/services/thread-lifecycle.service.js +174 -0
- package/dist/services/thread-lifecycle.service.js.map +1 -0
- package/dist/services/topic.service.d.ts +25 -0
- package/dist/services/topic.service.d.ts.map +1 -0
- package/dist/services/topic.service.js +65 -0
- package/dist/services/topic.service.js.map +1 -0
- package/dist/services/worker-cleanup.service.d.ts +8 -0
- package/dist/services/worker-cleanup.service.d.ts.map +1 -0
- package/dist/services/worker-cleanup.service.js +82 -0
- package/dist/services/worker-cleanup.service.js.map +1 -0
- package/dist/sessions.d.ts +14 -0
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +55 -0
- package/dist/sessions.js.map +1 -1
- package/dist/telegram.d.ts +13 -6
- package/dist/telegram.d.ts.map +1 -1
- package/dist/telegram.js +43 -14
- package/dist/telegram.js.map +1 -1
- package/dist/tools/delegate-tool.d.ts +4 -0
- package/dist/tools/delegate-tool.d.ts.map +1 -1
- package/dist/tools/delegate-tool.js +48 -109
- package/dist/tools/delegate-tool.js.map +1 -1
- package/dist/tools/memory-tools.d.ts.map +1 -1
- package/dist/tools/memory-tools.js +1 -1
- package/dist/tools/memory-tools.js.map +1 -1
- package/dist/tools/shared-agent-utils.d.ts +9 -1
- package/dist/tools/shared-agent-utils.d.ts.map +1 -1
- package/dist/tools/shared-agent-utils.js +21 -38
- package/dist/tools/shared-agent-utils.js.map +1 -1
- package/dist/tools/start-session-tool.d.ts +2 -0
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +66 -106
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/thread-lifecycle.d.ts +5 -127
- package/dist/tools/thread-lifecycle.d.ts.map +1 -1
- package/dist/tools/thread-lifecycle.js +5 -1167
- package/dist/tools/thread-lifecycle.js.map +1 -1
- package/dist/tools/utility-tools.js +5 -2
- package/dist/tools/utility-tools.js.map +1 -1
- package/dist/tools/wait/drive-handler.d.ts +0 -1
- package/dist/tools/wait/drive-handler.d.ts.map +1 -1
- package/dist/tools/wait/drive-handler.js +5 -22
- package/dist/tools/wait/drive-handler.js.map +1 -1
- package/dist/tools/wait/message-delivery.js +1 -1
- package/dist/tools/wait/message-delivery.js.map +1 -1
- package/dist/tools/wait/message-processing.d.ts.map +1 -1
- package/dist/tools/wait/message-processing.js +9 -8
- package/dist/tools/wait/message-processing.js.map +1 -1
- package/dist/tools/wait/poll-loop.d.ts +2 -0
- package/dist/tools/wait/poll-loop.d.ts.map +1 -1
- package/dist/tools/wait/poll-loop.js +27 -29
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/dist/tools/wait/task-handler.d.ts +0 -3
- package/dist/tools/wait/task-handler.d.ts.map +1 -1
- package/dist/tools/wait/task-handler.js +3 -2
- package/dist/tools/wait/task-handler.js.map +1 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -8
- package/supervisor/config.go +182 -69
- package/supervisor/config_test.go +78 -0
- package/supervisor/go.mod +12 -0
- package/supervisor/go.sum +20 -0
- package/supervisor/health.go +56 -6
- package/supervisor/health_test.go +29 -0
- package/supervisor/keeper.go +15 -10
- package/supervisor/log.go +109 -28
- package/supervisor/log_test.go +86 -6
- package/supervisor/main.go +150 -19
- package/supervisor/main_test.go +130 -0
- package/supervisor/process.go +47 -4
- package/supervisor/process_test.go +14 -0
- package/supervisor/secrets.go +95 -0
- package/supervisor/secrets_securevault_test.go +98 -0
- package/supervisor/secrets_test.go +119 -0
- package/supervisor/self_update.go +282 -0
- package/supervisor/self_update_test.go +177 -0
- package/supervisor/service_restart_stub.go +9 -0
- package/supervisor/service_restart_windows.go +63 -0
- package/supervisor/service_stub.go +15 -0
- package/supervisor/service_windows.go +216 -0
- package/supervisor/update_state.go +264 -0
- package/supervisor/update_state_test.go +306 -0
- package/supervisor/updater.go +311 -10
- package/supervisor/updater_test.go +64 -0
- package/scripts/install-supervisor.ps1 +0 -67
- package/scripts/install-supervisor.sh +0 -43
- package/scripts/start-supervisor.ps1 +0 -46
- package/scripts/start-supervisor.sh +0 -20
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestResolveRunSupervisorMode(t *testing.T) {
|
|
11
|
+
tests := []struct {
|
|
12
|
+
name string
|
|
13
|
+
processIsService bool
|
|
14
|
+
hostMode string
|
|
15
|
+
want bool
|
|
16
|
+
}{
|
|
17
|
+
{
|
|
18
|
+
name: "service process always forces service mode",
|
|
19
|
+
processIsService: true,
|
|
20
|
+
hostMode: "task",
|
|
21
|
+
want: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "non-service defaults to task mode",
|
|
25
|
+
processIsService: false,
|
|
26
|
+
hostMode: "",
|
|
27
|
+
want: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "non-service task remains task mode",
|
|
31
|
+
processIsService: false,
|
|
32
|
+
hostMode: "task",
|
|
33
|
+
want: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "non-service service mode is honored",
|
|
37
|
+
processIsService: false,
|
|
38
|
+
hostMode: "service",
|
|
39
|
+
want: true,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for _, tc := range tests {
|
|
44
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
45
|
+
got := resolveRunSupervisorMode(tc.processIsService, tc.hostMode)
|
|
46
|
+
if got != tc.want {
|
|
47
|
+
t.Fatalf("resolveRunSupervisorMode(processIsService=%v, hostMode=%q) = %v, want %v", tc.processIsService, tc.hostMode, got, tc.want)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func TestRunSupervisor_DoesNotRecoverPersistedStateWhenWatcherLockNotAcquired(t *testing.T) {
|
|
54
|
+
tempHome := t.TempDir()
|
|
55
|
+
t.Setenv("USERPROFILE", tempHome)
|
|
56
|
+
t.Setenv("HOME", tempHome)
|
|
57
|
+
t.Setenv("MCP_HTTP_PORT", "7777")
|
|
58
|
+
t.Setenv("MCP_HTTP_SECRET", "test-secret")
|
|
59
|
+
t.Setenv("TELEGRAM_TOKEN", "test-token")
|
|
60
|
+
t.Setenv("TELEGRAM_CHAT_ID", "test-chat")
|
|
61
|
+
|
|
62
|
+
dataDir := filepath.Join(tempHome, ".remote-copilot-mcp")
|
|
63
|
+
log := NewLogger(filepath.Join(dataDir, "test.log"))
|
|
64
|
+
defer log.Close()
|
|
65
|
+
|
|
66
|
+
store := NewUpdateStateStore(filepath.Join(dataDir, "update-state.json"), log)
|
|
67
|
+
store.Transition(updateScopeMCP, updatePhaseApplying, "2.0.0", "1.0.0", "")
|
|
68
|
+
|
|
69
|
+
watcherLock := filepath.Join(dataDir, "watcher.lock")
|
|
70
|
+
if !AcquireLock(watcherLock, log) {
|
|
71
|
+
t.Fatal("failed to pre-acquire watcher lock")
|
|
72
|
+
}
|
|
73
|
+
defer ReleaseLock(watcherLock)
|
|
74
|
+
|
|
75
|
+
err := runSupervisor(false)
|
|
76
|
+
if err == nil {
|
|
77
|
+
t.Fatal("expected runSupervisor to fail when watcher lock is already held")
|
|
78
|
+
}
|
|
79
|
+
if !strings.Contains(err.Error(), "another supervisor instance") {
|
|
80
|
+
t.Fatalf("unexpected error: %v", err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
state, loadErr := store.Load()
|
|
84
|
+
if loadErr != nil {
|
|
85
|
+
t.Fatalf("failed to load update state: %v", loadErr)
|
|
86
|
+
}
|
|
87
|
+
if state.Phase != updatePhaseApplying {
|
|
88
|
+
t.Fatalf("phase = %q, want %q", state.Phase, updatePhaseApplying)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func TestRunSupervisor_DoesNotApplyPendingSupervisorUpdateWhenWatcherLockNotAcquired(t *testing.T) {
|
|
93
|
+
tempHome := t.TempDir()
|
|
94
|
+
t.Setenv("USERPROFILE", tempHome)
|
|
95
|
+
t.Setenv("HOME", tempHome)
|
|
96
|
+
t.Setenv("MCP_HTTP_PORT", "7777")
|
|
97
|
+
t.Setenv("MCP_HTTP_SECRET", "test-secret")
|
|
98
|
+
t.Setenv("TELEGRAM_TOKEN", "test-token")
|
|
99
|
+
t.Setenv("TELEGRAM_CHAT_ID", "test-chat")
|
|
100
|
+
|
|
101
|
+
dataDir := filepath.Join(tempHome, ".remote-copilot-mcp")
|
|
102
|
+
log := NewLogger(filepath.Join(dataDir, "test.log"))
|
|
103
|
+
defer log.Close()
|
|
104
|
+
|
|
105
|
+
pendingVersion := filepath.Join(dataDir, "bin", "sensorium-supervisor.new.exe.version")
|
|
106
|
+
if err := os.MkdirAll(filepath.Dir(pendingVersion), 0755); err != nil {
|
|
107
|
+
t.Fatalf("failed to create pending version directory: %v", err)
|
|
108
|
+
}
|
|
109
|
+
if err := os.WriteFile(pendingVersion, []byte("2.0.0"), 0644); err != nil {
|
|
110
|
+
t.Fatalf("failed to create stale pending version file: %v", err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
watcherLock := filepath.Join(dataDir, "watcher.lock")
|
|
114
|
+
if !AcquireLock(watcherLock, log) {
|
|
115
|
+
t.Fatal("failed to pre-acquire watcher lock")
|
|
116
|
+
}
|
|
117
|
+
defer ReleaseLock(watcherLock)
|
|
118
|
+
|
|
119
|
+
err := runSupervisor(false)
|
|
120
|
+
if err == nil {
|
|
121
|
+
t.Fatal("expected runSupervisor to fail when watcher lock is already held")
|
|
122
|
+
}
|
|
123
|
+
if !strings.Contains(err.Error(), "another supervisor instance") {
|
|
124
|
+
t.Fatalf("unexpected error: %v", err)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if _, statErr := os.Stat(pendingVersion); statErr != nil {
|
|
128
|
+
t.Fatalf("pending supervisor version file was unexpectedly modified: %v", statErr)
|
|
129
|
+
}
|
|
130
|
+
}
|
package/supervisor/process.go
CHANGED
|
@@ -10,6 +10,7 @@ import (
|
|
|
10
10
|
"runtime"
|
|
11
11
|
"strconv"
|
|
12
12
|
"strings"
|
|
13
|
+
"syscall"
|
|
13
14
|
"time"
|
|
14
15
|
)
|
|
15
16
|
|
|
@@ -21,16 +22,44 @@ func SpawnMCPServer(cfg Config, log *Logger) (int, error) {
|
|
|
21
22
|
return 0, errors.New("empty MCP_START_COMMAND")
|
|
22
23
|
}
|
|
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
|
+
|
|
24
33
|
cmd := exec.Command(parts[0], parts[1:]...)
|
|
25
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
|
+
}
|
|
26
53
|
cmd.Stdin = nil
|
|
27
54
|
cmd.Stdout = nil
|
|
28
|
-
cmd.Stderr =
|
|
55
|
+
cmd.Stderr = stderrFile
|
|
29
56
|
setSysProcAttr(cmd)
|
|
30
57
|
|
|
31
58
|
log.Info("Starting MCP server: %s", cfg.MCPStartCommand)
|
|
59
|
+
log.Info("Capturing MCP server stderr to %s", cfg.Paths.MCPStderrLog)
|
|
32
60
|
|
|
33
61
|
if err := cmd.Start(); err != nil {
|
|
62
|
+
_ = stderrFile.Close()
|
|
34
63
|
return 0, fmt.Errorf("spawn MCP server: %w", err)
|
|
35
64
|
}
|
|
36
65
|
|
|
@@ -38,7 +67,10 @@ func SpawnMCPServer(cfg Config, log *Logger) (int, error) {
|
|
|
38
67
|
log.Info("MCP server started with PID %d", pid)
|
|
39
68
|
|
|
40
69
|
// Don't wait — detached process
|
|
41
|
-
go func() {
|
|
70
|
+
go func() {
|
|
71
|
+
_ = cmd.Wait()
|
|
72
|
+
_ = stderrFile.Close()
|
|
73
|
+
}()
|
|
42
74
|
|
|
43
75
|
if err := writePIDFile(cfg.Paths.ServerPID, pid); err != nil {
|
|
44
76
|
log.Warn("Failed to write server PID file: %v", err)
|
|
@@ -47,6 +79,17 @@ func SpawnMCPServer(cfg Config, log *Logger) (int, error) {
|
|
|
47
79
|
return pid, nil
|
|
48
80
|
}
|
|
49
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
|
+
|
|
50
93
|
// KillProcess kills a process by PID. On Windows, uses taskkill /F /T for tree kill.
|
|
51
94
|
func KillProcess(pid int, log *Logger) error {
|
|
52
95
|
if !IsProcessAlive(pid) {
|
|
@@ -72,7 +115,7 @@ func KillProcess(pid int, log *Logger) error {
|
|
|
72
115
|
log.Debug("KillProcess: FindProcess(%d) failed: %v", pid, err)
|
|
73
116
|
return err
|
|
74
117
|
}
|
|
75
|
-
if err := proc.Signal(
|
|
118
|
+
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
|
76
119
|
// Already dead
|
|
77
120
|
return nil
|
|
78
121
|
}
|
|
@@ -125,7 +168,7 @@ func IsProcessAlive(pid int) bool {
|
|
|
125
168
|
if err != nil {
|
|
126
169
|
return false
|
|
127
170
|
}
|
|
128
|
-
return proc.Signal(
|
|
171
|
+
return proc.Signal(syscall.Signal(0)) == nil // signal 0 = existence check on Unix
|
|
129
172
|
}
|
|
130
173
|
|
|
131
174
|
// --- PID File Helpers ---
|
|
@@ -92,3 +92,17 @@ func TestListThreadPIDs_MissingDir(t *testing.T) {
|
|
|
92
92
|
t.Errorf("expected empty map for missing directory, got %d entries", len(result))
|
|
93
93
|
}
|
|
94
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
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"errors"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/zalando/go-keyring"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestResolveSecretWithKeyring_EnvWins(t *testing.T) {
|
|
11
|
+
t.Setenv("MCP_HTTP_SECRET", "env-secret")
|
|
12
|
+
|
|
13
|
+
orig := keyringGet
|
|
14
|
+
keyringGet = func(service, user string) (string, error) {
|
|
15
|
+
return "keyring-secret", nil
|
|
16
|
+
}
|
|
17
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
18
|
+
|
|
19
|
+
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
20
|
+
if got != "env-secret" {
|
|
21
|
+
t.Fatalf("resolveSecretWithKeyring() = %q, want %q", got, "env-secret")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestResolveSecretWithKeyring_FallbackToKeyring(t *testing.T) {
|
|
26
|
+
t.Setenv("MCP_HTTP_SECRET", "")
|
|
27
|
+
|
|
28
|
+
orig := keyringGet
|
|
29
|
+
keyringGet = func(service, user string) (string, error) {
|
|
30
|
+
if service != "sensorium-supervisor" {
|
|
31
|
+
t.Fatalf("service = %q, want %q", service, "sensorium-supervisor")
|
|
32
|
+
}
|
|
33
|
+
if user != "MCP_HTTP_SECRET" {
|
|
34
|
+
t.Fatalf("user = %q, want %q", user, "MCP_HTTP_SECRET")
|
|
35
|
+
}
|
|
36
|
+
return "keyring-secret", nil
|
|
37
|
+
}
|
|
38
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
39
|
+
|
|
40
|
+
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
41
|
+
if got != "keyring-secret" {
|
|
42
|
+
t.Fatalf("resolveSecretWithKeyring() = %q, want %q", got, "keyring-secret")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestResolveSecretWithKeyring_NotFound(t *testing.T) {
|
|
47
|
+
t.Setenv("MCP_HTTP_SECRET", "")
|
|
48
|
+
|
|
49
|
+
orig := keyringGet
|
|
50
|
+
keyringGet = func(service, user string) (string, error) {
|
|
51
|
+
return "", keyring.ErrNotFound
|
|
52
|
+
}
|
|
53
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
54
|
+
|
|
55
|
+
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
56
|
+
if got != "" {
|
|
57
|
+
t.Fatalf("resolveSecretWithKeyring() = %q, want empty", got)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func TestResolveSecretWithKeyring_OtherError(t *testing.T) {
|
|
62
|
+
t.Setenv("MCP_HTTP_SECRET", "")
|
|
63
|
+
|
|
64
|
+
orig := keyringGet
|
|
65
|
+
keyringGet = func(service, user string) (string, error) {
|
|
66
|
+
return "", errors.New("keyring backend unavailable")
|
|
67
|
+
}
|
|
68
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
69
|
+
|
|
70
|
+
got := resolveSecretWithKeyring("MCP_HTTP_SECRET", "sensorium-supervisor")
|
|
71
|
+
if got != "" {
|
|
72
|
+
t.Fatalf("resolveSecretWithKeyring() = %q, want empty", got)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestResolveIntWithKeyring_EnvWins(t *testing.T) {
|
|
77
|
+
t.Setenv("MCP_HTTP_PORT", "3847")
|
|
78
|
+
|
|
79
|
+
orig := keyringGet
|
|
80
|
+
keyringGet = func(service, user string) (string, error) {
|
|
81
|
+
return "9999", nil
|
|
82
|
+
}
|
|
83
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
84
|
+
|
|
85
|
+
got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 0)
|
|
86
|
+
if got != 3847 {
|
|
87
|
+
t.Fatalf("resolveIntWithKeyring() = %d, want %d", got, 3847)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func TestResolveIntWithKeyring_FallbackToKeyring(t *testing.T) {
|
|
92
|
+
t.Setenv("MCP_HTTP_PORT", "")
|
|
93
|
+
|
|
94
|
+
orig := keyringGet
|
|
95
|
+
keyringGet = func(service, user string) (string, error) {
|
|
96
|
+
return "5001", nil
|
|
97
|
+
}
|
|
98
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
99
|
+
|
|
100
|
+
got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 0)
|
|
101
|
+
if got != 5001 {
|
|
102
|
+
t.Fatalf("resolveIntWithKeyring() = %d, want %d", got, 5001)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func TestResolveIntWithKeyring_InvalidFallback(t *testing.T) {
|
|
107
|
+
t.Setenv("MCP_HTTP_PORT", "")
|
|
108
|
+
|
|
109
|
+
orig := keyringGet
|
|
110
|
+
keyringGet = func(service, user string) (string, error) {
|
|
111
|
+
return "not-a-number", nil
|
|
112
|
+
}
|
|
113
|
+
t.Cleanup(func() { keyringGet = orig })
|
|
114
|
+
|
|
115
|
+
got := resolveIntWithKeyring("MCP_HTTP_PORT", "sensorium-supervisor", 3847)
|
|
116
|
+
if got != 3847 {
|
|
117
|
+
t.Fatalf("resolveIntWithKeyring() = %d, want fallback %d", got, 3847)
|
|
118
|
+
}
|
|
119
|
+
}
|