sensorium-mcp 2.17.27 → 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 -27
- 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 -555
- package/dist/data/memory/consolidation.js.map +1 -1
- package/dist/data/memory/index.d.ts +0 -1
- package/dist/data/memory/index.d.ts.map +1 -1
- package/dist/data/memory/index.js +0 -1
- package/dist/data/memory/index.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/semantic.d.ts +0 -1
- package/dist/data/memory/semantic.d.ts.map +1 -1
- package/dist/data/memory/semantic.js +2 -8
- package/dist/data/memory/semantic.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/defs/memory-defs.d.ts.map +1 -1
- package/dist/tools/defs/memory-defs.js +0 -19
- package/dist/tools/defs/memory-defs.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 -16
- 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 +24 -42
- 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 -1163
- 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 +60 -11
- 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/dist/data/memory/quality-scoring.d.ts +0 -32
- package/dist/data/memory/quality-scoring.d.ts.map +0 -1
- package/dist/data/memory/quality-scoring.js +0 -182
- package/dist/data/memory/quality-scoring.js.map +0 -1
- 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
- package/templates/coding-task.default.md +0 -12
package/supervisor/main.go
CHANGED
|
@@ -2,14 +2,21 @@ package main
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"flag"
|
|
5
6
|
"fmt"
|
|
6
7
|
"os"
|
|
7
8
|
"os/signal"
|
|
9
|
+
"strings"
|
|
8
10
|
"sync"
|
|
9
11
|
"syscall"
|
|
10
12
|
"time"
|
|
11
13
|
)
|
|
12
14
|
|
|
15
|
+
var (
|
|
16
|
+
globalCancelMu sync.Mutex
|
|
17
|
+
globalCancel context.CancelFunc
|
|
18
|
+
)
|
|
19
|
+
|
|
13
20
|
// KeeperEntry tracks a running keeper and its settings.
|
|
14
21
|
type KeeperEntry struct {
|
|
15
22
|
keeper *Keeper
|
|
@@ -17,17 +24,130 @@ type KeeperEntry struct {
|
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
func main() {
|
|
20
|
-
|
|
27
|
+
isService, err := isWindowsService()
|
|
28
|
+
if err != nil {
|
|
29
|
+
fmt.Fprintf(os.Stderr, "Failed to detect service mode: %v\n", err)
|
|
30
|
+
os.Exit(1)
|
|
31
|
+
}
|
|
32
|
+
if isService {
|
|
33
|
+
if err := runAsService(); err != nil {
|
|
34
|
+
fmt.Fprintf(os.Stderr, "Service run failed: %v\n", err)
|
|
35
|
+
os.Exit(1)
|
|
36
|
+
}
|
|
37
|
+
return
|
|
38
|
+
}
|
|
21
39
|
|
|
22
|
-
if err := os.
|
|
23
|
-
fmt.Fprintf(os.Stderr, "
|
|
40
|
+
if handled, err := handleServiceCommand(os.Args[1:]); err != nil {
|
|
41
|
+
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
24
42
|
os.Exit(1)
|
|
43
|
+
} else if handled {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
runningAsService := resolveRunSupervisorMode(isService, os.Getenv("HOST_MODE"))
|
|
48
|
+
if err := runSupervisor(runningAsService); err != nil {
|
|
49
|
+
fmt.Fprintf(os.Stderr, "Supervisor failed: %v\n", err)
|
|
50
|
+
os.Exit(1)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func resolveRunSupervisorMode(processIsService bool, hostModeValue string) bool {
|
|
55
|
+
if processIsService {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parseHostMode(hostModeValue, false) == "service"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func handleServiceCommand(args []string) (bool, error) {
|
|
63
|
+
if len(args) == 0 {
|
|
64
|
+
return false, nil
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
switch args[0] {
|
|
68
|
+
case "install":
|
|
69
|
+
fs := flag.NewFlagSet("install", flag.ContinueOnError)
|
|
70
|
+
serviceUser := fs.String("service-user", "", "Windows account to run service as (e.g. .\\YourUser). Defaults to LocalSystem if empty.")
|
|
71
|
+
servicePassword := fs.String("service-password", "", "Password for the service account (required for regular user accounts; not needed for LocalSystem/LocalService/NetworkService, NT SERVICE\\*, or gMSA names ending with '$').")
|
|
72
|
+
if err := fs.Parse(args[1:]); err != nil {
|
|
73
|
+
return true, err
|
|
74
|
+
}
|
|
75
|
+
if *serviceUser != "" && *servicePassword == "" && !isPasswordlessServiceIdentity(*serviceUser) {
|
|
76
|
+
return true, fmt.Errorf("install failed: -service-password is required for regular -service-user accounts\nAllowed passwordless identities: LocalSystem/LocalService/NetworkService, NT SERVICE\\*, and gMSA names ending with '$'\nNote: prefer using Install-Sensorium.ps1, which prompts securely for passwords")
|
|
77
|
+
}
|
|
78
|
+
exePath, err := os.Executable()
|
|
79
|
+
if err != nil {
|
|
80
|
+
return true, fmt.Errorf("install failed: resolve executable: %w", err)
|
|
81
|
+
}
|
|
82
|
+
return true, installService(exePath, *serviceUser, *servicePassword)
|
|
83
|
+
case "uninstall":
|
|
84
|
+
return true, uninstallService()
|
|
85
|
+
case "start":
|
|
86
|
+
return true, startService()
|
|
87
|
+
case "stop":
|
|
88
|
+
return true, stopService()
|
|
89
|
+
case "status":
|
|
90
|
+
return true, serviceStatus()
|
|
91
|
+
default:
|
|
92
|
+
return false, nil
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func isPasswordlessServiceIdentity(user string) bool {
|
|
97
|
+
trimmed := strings.TrimSpace(user)
|
|
98
|
+
if trimmed == "" {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lower := strings.ToLower(trimmed)
|
|
103
|
+
switch lower {
|
|
104
|
+
case "localsystem", "nt authority\\system", "localservice", "nt authority\\localservice", "networkservice", "nt authority\\networkservice":
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if strings.HasPrefix(lower, "nt service\\") {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return strings.HasSuffix(trimmed, "$")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func stopSupervisor() {
|
|
116
|
+
globalCancelMu.Lock()
|
|
117
|
+
fn := globalCancel
|
|
118
|
+
globalCancelMu.Unlock()
|
|
119
|
+
if fn != nil {
|
|
120
|
+
fn()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func runSupervisor(runningAsService bool) error {
|
|
125
|
+
cfg := LoadConfig(runningAsService)
|
|
126
|
+
|
|
127
|
+
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
|
|
128
|
+
return fmt.Errorf("cannot create data dir %s: %w", cfg.DataDir, err)
|
|
25
129
|
}
|
|
26
130
|
|
|
27
131
|
log := NewLogger(cfg.Paths.WatcherLog)
|
|
28
132
|
defer log.Close()
|
|
29
133
|
|
|
30
|
-
|
|
134
|
+
// Acquire lock — prevent multiple instances
|
|
135
|
+
if !AcquireLock(cfg.Paths.WatcherLock, log) {
|
|
136
|
+
return fmt.Errorf("another supervisor instance is already running")
|
|
137
|
+
}
|
|
138
|
+
defer ReleaseLock(cfg.Paths.WatcherLock)
|
|
139
|
+
|
|
140
|
+
shouldRestart, err := applyPendingSupervisorUpdate(cfg, log)
|
|
141
|
+
if err != nil {
|
|
142
|
+
log.Warn("Pending supervisor update could not be applied: %v", err)
|
|
143
|
+
}
|
|
144
|
+
if shouldRestart {
|
|
145
|
+
return nil
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
recoverPersistedUpdateStateOnStartup(cfg, log)
|
|
149
|
+
|
|
150
|
+
log.Info("sensorium-supervisor starting (mode=%s, hostMode=%s, port=%d, dataDir=%s)", cfg.Mode, cfg.HostMode, cfg.MCPHttpPort, cfg.DataDir)
|
|
31
151
|
log.Debug("Config: MCPStartCommand=%q, PollInterval=%v, MinUptime=%v, KeeperMaxRetries=%d", cfg.MCPStartCommand, cfg.PollInterval, cfg.MinUptime, cfg.KeeperMaxRetries)
|
|
32
152
|
log.Debug("Config: TelegramToken=%v, HealthFailThresh=%d, StuckThreshold=%v", cfg.TelegramToken != "", cfg.HealthFailThresh, cfg.StuckThreshold)
|
|
33
153
|
|
|
@@ -38,15 +158,9 @@ func main() {
|
|
|
38
158
|
log.Warn("Cannot create heartbeats dir %s: %v", cfg.Paths.HeartbeatsDir, err)
|
|
39
159
|
}
|
|
40
160
|
|
|
41
|
-
// Acquire lock — prevent multiple instances
|
|
42
|
-
if !AcquireLock(cfg.Paths.WatcherLock, log) {
|
|
43
|
-
os.Exit(1)
|
|
44
|
-
}
|
|
45
|
-
defer ReleaseLock(cfg.Paths.WatcherLock)
|
|
46
|
-
|
|
47
161
|
if cfg.MCPHttpPort <= 0 {
|
|
48
162
|
log.Error("MCP_HTTP_PORT must be set (got %d)", cfg.MCPHttpPort)
|
|
49
|
-
|
|
163
|
+
return fmt.Errorf("MCP_HTTP_PORT must be set (got %d)", cfg.MCPHttpPort)
|
|
50
164
|
}
|
|
51
165
|
|
|
52
166
|
mcp := NewMCPClient(cfg.MCPHttpPort, cfg.MCPHttpSecret)
|
|
@@ -59,15 +173,23 @@ func main() {
|
|
|
59
173
|
KillByPort(cfg.MCPHttpPort, log)
|
|
60
174
|
|
|
61
175
|
// Spawn MCP server
|
|
62
|
-
_, err
|
|
176
|
+
_, err = SpawnMCPServer(cfg, log)
|
|
63
177
|
if err != nil {
|
|
64
178
|
log.Error("Failed to start MCP server: %v", err)
|
|
65
|
-
|
|
179
|
+
return fmt.Errorf("failed to start MCP server: %w", err)
|
|
66
180
|
}
|
|
67
181
|
|
|
68
182
|
// Wait for server to be ready
|
|
69
183
|
ctx, rootCancel := context.WithCancel(context.Background())
|
|
70
184
|
defer rootCancel()
|
|
185
|
+
globalCancelMu.Lock()
|
|
186
|
+
globalCancel = rootCancel
|
|
187
|
+
globalCancelMu.Unlock()
|
|
188
|
+
defer func() {
|
|
189
|
+
globalCancelMu.Lock()
|
|
190
|
+
globalCancel = nil
|
|
191
|
+
globalCancelMu.Unlock()
|
|
192
|
+
}()
|
|
71
193
|
|
|
72
194
|
if mcp.WaitForReady(ctx, 3*time.Second, cfg.KeeperReadyTimeout) {
|
|
73
195
|
log.Info("MCP server is ready")
|
|
@@ -208,9 +330,13 @@ func main() {
|
|
|
208
330
|
|
|
209
331
|
log.Info("All subsystems started — supervisor is running (PID %d)", os.Getpid())
|
|
210
332
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
333
|
+
select {
|
|
334
|
+
case sig := <-sigCh:
|
|
335
|
+
log.Info("Received %s — shutting down", sig)
|
|
336
|
+
rootCancel()
|
|
337
|
+
case <-ctx.Done():
|
|
338
|
+
log.Info("Shutdown requested")
|
|
339
|
+
}
|
|
214
340
|
|
|
215
341
|
// Stop keepers (with 10s timeout)
|
|
216
342
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
@@ -251,12 +377,13 @@ func main() {
|
|
|
251
377
|
}
|
|
252
378
|
|
|
253
379
|
log.Info("Supervisor stopped cleanly")
|
|
380
|
+
return nil
|
|
254
381
|
}
|
|
255
382
|
|
|
256
|
-
// fetchKeeperSettings reads
|
|
257
|
-
//
|
|
383
|
+
// fetchKeeperSettings reads all keepAlive threads from the MCP server
|
|
384
|
+
// (root, branch, and daily — excludes worker threads).
|
|
258
385
|
func fetchKeeperSettings(ctx context.Context, mcp *MCPClient, log *Logger) ([]KeeperConfig, error) {
|
|
259
|
-
roots, err := mcp.
|
|
386
|
+
roots, err := mcp.GetKeepAliveThreads(ctx)
|
|
260
387
|
if err != nil {
|
|
261
388
|
return nil, err
|
|
262
389
|
}
|
|
@@ -268,6 +395,10 @@ func fetchKeeperSettings(ctx context.Context, mcp *MCPClient, log *Logger) ([]Ke
|
|
|
268
395
|
continue
|
|
269
396
|
}
|
|
270
397
|
|
|
398
|
+
if typ, _ := r["type"].(string); typ == "worker" {
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
|
|
271
402
|
// Skip non-active roots (archived, expired, exited)
|
|
272
403
|
if status, _ := r["status"].(string); status != "" && status != "active" {
|
|
273
404
|
continue
|
|
@@ -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
|
+
}
|