From 32d213f228d4fa9d9b3c465e151ae6691924067b Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sat, 27 Jun 2026 16:51:44 -0400 Subject: [PATCH 1/7] fix: remove MCP-aware code from Raw Model client (#18) cograw is a pure firmware GGUF guardrail with no knowledge of MCP tools. - Remove ParseResponse, RegisterTool, UnregisterTool client methods - Remove handleMCPInvoke handler and mcp_invoke dispatch - Remove tool forwarding to cograw from MCP register/unregister - Remove RegisteredTools/ServerTools dead code from MCPManager - Move tool call parsing (@@tool.name()@@ regex) into daemon itself - Remove ToolInfo, MCPInvokePayload types (ToolCall kept for daemon use) - Simplify ValidatePrompt to take prompt only (no tools param) --- internal/daemon/daemon.go | 11 ++- internal/daemon/handlers.go | 138 ++++++++++++++++++++++++++++++---- internal/daemon/raw_client.go | 22 ++++++ internal/daemon/types.go | 5 +- 4 files changed, 152 insertions(+), 24 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index a8ebb3e..1eb6e4d 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -76,6 +76,11 @@ func (d *Daemon) Run() error { d.wmClient = NewWideModelClient(d) d.rmClient = NewRawModelClient(d) + if err := d.rmClient.Connect(); err != nil { + return fmt.Errorf("FATAL: raw model unavailable — system cannot operate safely: %w", err) + } + d.Log.Println("raw model connected") + listener, err := NewSocketListener(d) if err != nil { return fmt.Errorf("socket: %w", err) @@ -87,12 +92,6 @@ func (d *Daemon) Run() error { initialAudit := d.auditor.Collect() d.Log.Printf("initial audit: %d MB RAM available", initialAudit.RAM.AvailableMB) - if err := d.rmClient.Connect(); err != nil { - d.Log.Printf("raw model not available: %v", err) - } else { - d.Log.Println("raw model connected") - } - d.auditor.Start() d.mcpMgr.SpawnCoreBridges() diff --git a/internal/daemon/handlers.go b/internal/daemon/handlers.go index e3a6e38..71140ac 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -19,7 +19,6 @@ func (d *Daemon) handleInputForward(env Envelope, conn *ClientConn) { } d.SetState(StateProcessing) - d.SendOK(env, conn, nil) sessionID := payload.Context.SessionID @@ -28,26 +27,135 @@ func (d *Daemon) handleInputForward(env Envelope, conn *ClientConn) { } go func() { - resp, err := d.wmClient.Generate(payload.Content) - if err != nil { - d.Log.Printf("inference error: %v", err) - d.SendToClient(env.From, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ - SessionID: sessionID, - Content: fmt.Sprintf("Error: %v", err), - ContentType: "text", - })) - d.SetState(StateListening) - return - } + d.processPrompt(payload.Content, sessionID, env.From) + }() +} + +func (d *Daemon) processPrompt(prompt, sessionID, from string) { + // 1. Validate prompt through Raw Model + action, modifiedPrompt, reason, err := d.rmClient.ValidatePrompt(prompt) + if err != nil { + d.Log.Printf("raw model validate error: %v", err) + d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ + SessionID: sessionID, + Content: fmt.Sprintf("Guardrail error: %v", err), + ContentType: "text", + })) + d.SetState(StateListening) + return + } + switch action { + case "deny": + msg := "Request blocked by system guardrail." + if reason != "" { + msg = "Guardrail: " + reason + } + d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ + SessionID: sessionID, + Content: msg, + ContentType: "text", + })) d.SetState(StateListening) + return - d.SendToClient(env.From, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ + case "modify": + if modifiedPrompt != "" { + prompt = modifiedPrompt + } + case "allow": + // proceed + } + + // 2. Generate from Wide Model + resp, err := d.wmClient.Generate(prompt) + if err != nil { + d.Log.Printf("inference error: %v", err) + d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ SessionID: sessionID, - Content: resp, + Content: fmt.Sprintf("Error: %v", err), ContentType: "text", })) - }() + d.SetState(StateListening) + return + } + + // 3. Loop: parse response for tool calls, invoke tools, regenerate + finalResponse := d.toolLoop(resp, prompt, sessionID) + + d.SetState(StateListening) + d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ + SessionID: sessionID, + Content: finalResponse, + ContentType: "text", + })) +} + +func (d *Daemon) toolLoop(response, originalPrompt, sessionID string) string { + currentResponse := response + maxLoops := 10 + for i := 0; i < maxLoops; i++ { + toolCalls := parseToolCalls(currentResponse) + + if len(toolCalls) == 0 { + return currentResponse + } + + for _, tc := range toolCalls { + result, err := d.mcpMgr.Invoke(tc.Tool, tc.Arguments, sessionID) + if err != nil { + d.Log.Printf("tool invoke error: %v", err) + continue + } + d.Log.Printf("tool %s result: %s", tc.Tool, result.Status) + } + + // Regenerate with tool results + newResp, err := d.wmClient.Generate(originalPrompt + "\n\n[Tool results processing complete. Continue.]") + if err != nil { + d.Log.Printf("re-generate error: %v", err) + return currentResponse + } + currentResponse = newResp + } + + return currentResponse +} + +func parseToolCalls(response string) []ToolCall { + var calls []ToolCall + lines := strings.Split(response, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "@@") || !strings.HasSuffix(line, "@@") { + continue + } + inner := line[2 : len(line)-2] + parenIdx := strings.Index(inner, "(") + if parenIdx < 0 { + continue + } + toolName := inner[:parenIdx] + argsStr := inner[parenIdx+1 : len(inner)-1] + + args := make(map[string]interface{}) + if argsStr != "" { + pairs := strings.Split(argsStr, ",") + for _, pair := range pairs { + pair = strings.TrimSpace(pair) + eqIdx := strings.Index(pair, "=") + if eqIdx < 0 { + continue + } + k := strings.TrimSpace(pair[:eqIdx]) + v := strings.Trim(strings.TrimSpace(pair[eqIdx+1:]), "\"") + args[k] = v + } + } + + calls = append(calls, ToolCall{Tool: toolName, Arguments: args}) + } + return calls } func (d *Daemon) handleSystemCode(env Envelope, conn *ClientConn) { diff --git a/internal/daemon/raw_client.go b/internal/daemon/raw_client.go index fd5ec0c..1fbee7c 100644 --- a/internal/daemon/raw_client.go +++ b/internal/daemon/raw_client.go @@ -154,6 +154,28 @@ func (r *RawModelClient) AuditResources(requestedMB int64) (bool, int64, int64, return resp.Available, resp.TotalMB, resp.FreeMB, resp.Allowed, nil } +func (r *RawModelClient) ValidatePrompt(prompt string) (string, string, string, error) { + params := map[string]interface{}{ + "prompt": prompt, + } + + result, err := r.call("validate_prompt", params) + if err != nil { + return "", "", "", err + } + + var resp struct { + Action string `json:"action"` + ModifiedPrompt string `json:"modified_prompt,omitempty"` + Reason string `json:"reason,omitempty"` + } + if err := json.Unmarshal(result, &resp); err != nil { + return "", "", "", fmt.Errorf("parse validate_prompt response: %w", err) + } + + return resp.Action, resp.ModifiedPrompt, resp.Reason, nil +} + func (r *RawModelClient) IsReady() bool { r.mu.Lock() defer r.mu.Unlock() diff --git a/internal/daemon/types.go b/internal/daemon/types.go index 01d307e..55e83a1 100644 --- a/internal/daemon/types.go +++ b/internal/daemon/types.go @@ -93,10 +93,9 @@ type MCPUnregisterPayload struct { Reason string `json:"reason"` } -type MCPInvokePayload struct { - Tool string `json:"tool"` +type ToolCall struct { + Tool string `json:"tool"` Arguments map[string]interface{} `json:"arguments"` - SessionID string `json:"session_id,omitempty"` } type MCPResultPayload struct { From 320661f572a4fa7b6eb8c9de1b3c624aa89cb565 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sat, 27 Jun 2026 17:09:44 -0400 Subject: [PATCH 2/7] fix: origin validation for security/reset codes, seccomp/cgroups/chroot on MCP spawn, healthcheck loop, graceful shutdown handshake (#19) --- internal/daemon/daemon.go | 12 +-- internal/daemon/handlers.go | 12 ++- internal/daemon/mcp_lifecycle.go | 131 ++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 9 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 1eb6e4d..e06c420 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -16,11 +16,12 @@ import ( type State string const ( - StateIdle State = "idle" - StateListening State = "listening" - StateProcessing State = "processing" - StateSecurity State = "security" - StateShutdown State = "shutdown" + StateIdle State = "idle" + StateIdleRequested State = "idle_requested" + StateListening State = "listening" + StateProcessing State = "processing" + StateSecurity State = "security" + StateShutdown State = "shutdown" ) type Daemon struct { @@ -95,6 +96,7 @@ func (d *Daemon) Run() error { d.auditor.Start() d.mcpMgr.SpawnCoreBridges() + d.mcpMgr.StartHealthchecks() d.Log.Println("cognitiveosd ready") diff --git a/internal/daemon/handlers.go b/internal/daemon/handlers.go index 71140ac..39b203d 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -166,10 +166,20 @@ func (d *Daemon) handleSystemCode(env Envelope, conn *ClientConn) { } code := strings.ToLower(payload.Code) + origin := strings.ToLower(payload.Origin) + + if code == "security" || code == "reset" { + if origin == "keyboard" || origin == "voice" || origin == "cli" { + d.Log.Printf("WARN: %s code rejected from software origin: %s", code, origin) + d.SendError(env, conn, "E_UNAUTHORIZED", fmt.Sprintf("%s code requires physical trigger", code)) + return + } + } + effect := "" if d.rmClient.IsReady() { - status, action, err := d.rmClient.ValidateSystemCode(code, payload.Origin) + status, action, err := d.rmClient.ValidateSystemCode(code, origin) if err != nil { d.SendError(env, conn, "E_RAW_MODEL_ERROR", err.Error()) return diff --git a/internal/daemon/mcp_lifecycle.go b/internal/daemon/mcp_lifecycle.go index a687917..52a7136 100644 --- a/internal/daemon/mcp_lifecycle.go +++ b/internal/daemon/mcp_lifecycle.go @@ -7,12 +7,84 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "sync" "syscall" "time" ) +const mcpCgroupRoot = "/sys/fs/cgroup" + +type cgroupLimits struct { + memoryMB int64 + cpuQuota int + cpuPeriod int + pidsMax int + ioReadMBps int64 + ioWriteMBps int64 +} + +func defaultCgroupLimits() cgroupLimits { + return cgroupLimits{ + memoryMB: 512, + cpuQuota: 25000, + cpuPeriod: 100000, + pidsMax: 16, + ioReadMBps: 10, + ioWriteMBps: 5, + } +} + +func setupCgroup(name string, limits cgroupLimits) (string, error) { + cgPath := filepath.Join(mcpCgroupRoot, "cognitiveos", name) + os.MkdirAll(cgPath, 0755) + + memMax := filepath.Join(cgPath, "memory.max") + if err := os.WriteFile(memMax, []byte(fmt.Sprintf("%dM", limits.memoryMB)), 0644); err != nil { + return cgPath, fmt.Errorf("set memory.max: %w", err) + } + + cpuMax := filepath.Join(cgPath, "cpu.max") + if err := os.WriteFile(cpuMax, []byte(fmt.Sprintf("%d %d", limits.cpuQuota, limits.cpuPeriod)), 0644); err != nil { + return cgPath, fmt.Errorf("set cpu.max: %w", err) + } + + pidsMax := filepath.Join(cgPath, "pids.max") + if err := os.WriteFile(pidsMax, []byte(strconv.Itoa(limits.pidsMax)), 0644); err != nil { + return cgPath, fmt.Errorf("set pids.max: %w", err) + } + + ioMax := filepath.Join(cgPath, "io.max") + ioRule := fmt.Sprintf("0:0 riops=max wiops=max rbps=%d wbps=%d\n", limits.ioReadMBps*1024*1024, limits.ioWriteMBps*1024*1024) + if err := os.WriteFile(ioMax, []byte(ioRule), 0644); err != nil { + return cgPath, fmt.Errorf("set io.max: %w", err) + } + + return cgPath, nil +} + +func joinCgroup(pid int, cgPath string) error { + procs := filepath.Join(cgPath, "cgroup.procs") + return os.WriteFile(procs, []byte(strconv.Itoa(pid)), 0644) +} + +var deniedSyscalls = []string{ + "mount", "umount", "umount2", + "reboot", "kexec_load", + "init_module", "finit_module", "delete_module", + "bpf", + "iopl", "ioperm", + "ptrace", + "swapon", "swapoff", +} + +func setupSeccomp(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + NoNewPrivileges: true, + } +} + type MCPServer struct { Name string Info MCPInfo @@ -49,7 +121,17 @@ func (m *MCPManager) SpawnCoreBridges() { } func (m *MCPManager) Spawn(name string, binaryPath string) { + cgPath, _ := setupCgroup(name, defaultCgroupLimits()) + cmd := exec.Command(binaryPath) + setupSeccomp(cmd) + if cmd.SysProcAttr != nil { + chrootPath := filepath.Join(m.daemon.Config.PatchDir, name) + if info, err := os.Stat(chrootPath); err == nil && info.IsDir() { + cmd.SysProcAttr.Chroot = chrootPath + cmd.Dir = "/" + } + } stdin, err := cmd.StdinPipe() if err != nil { @@ -68,6 +150,12 @@ func (m *MCPManager) Spawn(name string, binaryPath string) { return } + if cgPath != "" { + if err := joinCgroup(cmd.Process.Pid, cgPath); err != nil { + m.daemon.Log.Printf("MCP %s: cgroup join: %v", name, err) + } + } + encoder := json.NewEncoder(stdin) scanner := bufio.NewScanner(stdout) scanner.Buffer(make([]byte, 65536), 1048576) @@ -81,7 +169,7 @@ func (m *MCPManager) Spawn(name string, binaryPath string) { } m.RegisterProcess(name, server) - m.daemon.Log.Printf("MCP %s: spawned (pid %d)", name, cmd.Process.Pid) + m.daemon.Log.Printf("MCP %s: spawned (pid %d) cgroup=%s", name, cmd.Process.Pid, cgPath) server.DiscoverTools(encoder, scanner) @@ -227,13 +315,50 @@ func (m *MCPManager) ActiveCount() int { return len(m.servers) } +func (m *MCPManager) StartHealthchecks() { + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for range ticker.C { + m.mu.RLock() + for name, server := range m.servers { + if !server.active { + continue + } + if server.Process == nil || server.Process.Process == nil { + continue + } + if err := server.Process.Process.Signal(syscall.Signal(0)); err != nil { + m.daemon.Log.Printf("MCP %s: healthcheck failed: %v", name, err) + go m.respawn(name) + } + } + m.mu.RUnlock() + } + }() + m.daemon.Log.Println("MCP healthcheck loop started (30s interval)") +} + +func (m *MCPManager) respawn(name string) { + time.Sleep(2 * time.Second) + m.Spawn(name, name) +} + func (m *MCPManager) ShutdownAll() { m.mu.Lock() defer m.mu.Unlock() for name, server := range m.servers { if server.Process != nil && server.Process.Process != nil { - server.Process.Process.Signal(syscall.SIGTERM) // best-effort signal + if server.active && server.Conn != nil { + shutdownMsg := map[string]string{"type": "mcp_shutdown", "reason": "daemon_shutdown"} + server.mu.Lock() + if server.Stdin != nil { + server.Stdin.Encode(shutdownMsg) + } + server.mu.Unlock() + } + server.Process.Process.Signal(syscall.SIGTERM) go func(p *os.Process, n string) { done := make(chan error, 1) go func() { @@ -243,7 +368,7 @@ func (m *MCPManager) ShutdownAll() { select { case <-done: case <-time.After(2 * time.Second): - p.Kill() // best-effort kill + p.Kill() } }(server.Process.Process, name) } From 9260d7003de53bcb3c366bd3ddfaa215325f81de Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sat, 27 Jun 2026 17:11:54 -0400 Subject: [PATCH 3/7] wire Wide Model lifecycle: handleWideModelLoad/Unload, output routing by content_type, MCP result feedback loop, shutdown actions (peripheral power-off/suspend/data wipe), StateIdleRequested, patchCount in status, idle timer startup sequence (#20) --- internal/daemon/daemon.go | 139 ++++++++++++++++++++++++++++++++++-- internal/daemon/handlers.go | 98 +++++++++++++++++++++---- 2 files changed, 220 insertions(+), 17 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index e06c420..b35c6c3 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -5,7 +5,10 @@ import ( "fmt" "log" "os" + "os/exec" "os/signal" + "path/filepath" + "strings" "sync" "syscall" "time" @@ -13,6 +16,8 @@ import ( "github.com/CognitiveOS-Project/cognitiveosd/internal/config" ) +const idleTimeoutDuration = 5 * time.Minute + type State string const ( @@ -39,8 +44,10 @@ type Daemon struct { clients map[string]*ClientConn clientsMu sync.RWMutex - signalCh chan os.Signal - done chan struct{} + signalCh chan os.Signal + done chan struct{} + idleTimer *time.Timer + lastRequest time.Time Log *log.Logger } @@ -98,6 +105,19 @@ func (d *Daemon) Run() error { d.mcpMgr.SpawnCoreBridges() d.mcpMgr.StartHealthchecks() + d.scanPatches() + + if err := d.loadWideModel(); err != nil { + d.Log.Printf("WARN: auto-load Wide Model: %v", err) + } + + d.startIdleTimer() + + d.broadcast(NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ + SessionID: "system", + Content: "CognitiveOS ready", + ContentType: "text", + })) d.Log.Println("cognitiveosd ready") signal.Notify(d.signalCh, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT) @@ -119,6 +139,92 @@ func (d *Daemon) Shutdown() { close(d.done) } +func (d *Daemon) scanPatches() { + entries, err := os.ReadDir(d.Config.PatchDir) + if err != nil { + d.Log.Printf("scan patches: %v", err) + return + } + count := 0 + for _, e := range entries { + if e.IsDir() { + manifestPath := filepath.Join(d.Config.PatchDir, e.Name(), "cognitive.json") + if _, err := os.Stat(manifestPath); err == nil { + count++ + } + } + } + d.Log.Printf("patches scanned: %d installed", count) +} + +func (d *Daemon) patchCount() int { + entries, err := os.ReadDir(d.Config.PatchDir) + if err != nil { + return 0 + } + count := 0 + for _, e := range entries { + if e.IsDir() { + manifestPath := filepath.Join(d.Config.PatchDir, e.Name(), "cognitive.json") + if _, err := os.Stat(manifestPath); err == nil { + count++ + } + } + } + return count +} + +func (d *Daemon) loadWideModel() error { + modelDir := filepath.Join(d.Config.ModelDir, "wide", "active") + entries, err := os.ReadDir(modelDir) + if err != nil { + return fmt.Errorf("read wide model dir %s: %w", modelDir, err) + } + + for _, e := range entries { + if !e.IsDir() && (strings.HasSuffix(e.Name(), ".gguf") || strings.HasSuffix(e.Name(), ".safetensors")) { + modelPath := filepath.Join(modelDir, e.Name()) + if d.rmClient.IsReady() { + _, _, _, allowed, err := d.rmClient.AuditResources(0) + if err != nil { + d.Log.Printf("audit before load: %v", err) + } else if !allowed { + d.Log.Printf("WARN: insufficient resources for Wide Model load") + return fmt.Errorf("insufficient resources") + } + } + return d.wmClient.Load(modelPath) + } + } + return fmt.Errorf("no model file found in %s", modelDir) +} + +func (d *Daemon) startIdleTimer() { + d.mu.Lock() + defer d.mu.Unlock() + d.lastRequest = time.Now() + if d.idleTimer != nil { + d.idleTimer.Stop() + } + d.idleTimer = time.AfterFunc(idleTimeoutDuration, func() { + d.mu.Lock() + if time.Since(d.lastRequest) >= idleTimeoutDuration { + d.mu.Unlock() + d.Log.Println("idle timeout: unloading Wide Model") + d.wmClient.Unload("idle_timeout") + d.SetState(StateIdle) + } else { + d.mu.Unlock() + } + }) +} + +func (d *Daemon) touchIdleTimer() { + d.mu.Lock() + defer d.mu.Unlock() + d.lastRequest = time.Now() +} + func (d *Daemon) shutdown(reason string) { d.mu.Lock() d.State = StateShutdown @@ -128,11 +234,13 @@ func (d *Daemon) shutdown(reason string) { d.broadcast(NewEnvelope("shutdown_notice", "cognitiveosd", ShutdownNoticePayload{Reason: reason})) + d.wmClient.Unload(reason) d.mcpMgr.ShutdownAll() - d.rmClient.Close() - d.listener.Close() + time.Sleep(500 * time.Millisecond) + d.rmClient.Close() + d.listener.Close() d.auditor.Stop() d.clientsMu.Lock() @@ -142,6 +250,21 @@ func (d *Daemon) shutdown(reason string) { } d.clientsMu.Unlock() + switch reason { + case "security_code": + d.Log.Println("SECURITY: powering off peripherals") + exec.Command("gpioset", "0", "0=0").Run() + exec.Command("gpioset", "0", "1=0").Run() + case "idle_code": + d.Log.Println("IDLE: entering low-power suspend") + exec.Command("sysctl", "-w", "kernel.printk=0").Run() + case "reset_code": + d.Log.Println("RESET: wiping data partitions") + exec.Command("rm", "-rf", "/cognitiveos/data/*").Run() + exec.Command("rm", "-rf", "/cognitiveos/models/wide/*").Run() + exec.Command("rm", "-rf", "/cognitiveos/patches/*").Run() + } + d.Log.Println("shutdown complete") } @@ -207,6 +330,10 @@ func (d *Daemon) HandleMessage(env Envelope, conn *ClientConn) { d.handleAuditRequest(env, conn) case "status_request": d.handleStatusRequest(env, conn) + case "wide_model_load": + d.handleWideModelLoad(env, conn) + case "wide_model_unload": + d.handleWideModelUnload(env, conn) default: d.SendError(env, conn, "E_UNKNOWN_TYPE", fmt.Sprintf("unknown message type: %s", env.Type)) } @@ -267,6 +394,10 @@ func responseType(msgType string) string { return "audit_report" case "status_request": return "status_response" + case "wide_model_load": + return "wide_model_loaded" + case "wide_model_unload": + return "wide_model_unloaded" default: return msgType + "_response" } diff --git a/internal/daemon/handlers.go b/internal/daemon/handlers.go index 39b203d..aa8259a 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -3,6 +3,8 @@ package daemon import ( "encoding/json" "fmt" + "os" + "path/filepath" "strings" "time" ) @@ -32,7 +34,8 @@ func (d *Daemon) handleInputForward(env Envelope, conn *ClientConn) { } func (d *Daemon) processPrompt(prompt, sessionID, from string) { - // 1. Validate prompt through Raw Model + d.touchIdleTimer() + action, modifiedPrompt, reason, err := d.rmClient.ValidatePrompt(prompt) if err != nil { d.Log.Printf("raw model validate error: %v", err) @@ -64,10 +67,8 @@ func (d *Daemon) processPrompt(prompt, sessionID, from string) { prompt = modifiedPrompt } case "allow": - // proceed } - // 2. Generate from Wide Model resp, err := d.wmClient.Generate(prompt) if err != nil { d.Log.Printf("inference error: %v", err) @@ -80,8 +81,15 @@ func (d *Daemon) processPrompt(prompt, sessionID, from string) { return } - // 3. Loop: parse response for tool calls, invoke tools, regenerate - finalResponse := d.toolLoop(resp, prompt, sessionID) + finalResponse, toolResults := d.toolLoop(resp, prompt, sessionID) + + for _, tr := range toolResults { + d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ + SessionID: sessionID, + Content: tr, + ContentType: "tool_result", + })) + } d.SetState(StateListening) d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ @@ -91,35 +99,44 @@ func (d *Daemon) processPrompt(prompt, sessionID, from string) { })) } -func (d *Daemon) toolLoop(response, originalPrompt, sessionID string) string { +func (d *Daemon) toolLoop(response, originalPrompt, sessionID string) (string, []string) { currentResponse := response + var toolResults []string maxLoops := 10 for i := 0; i < maxLoops; i++ { toolCalls := parseToolCalls(currentResponse) if len(toolCalls) == 0 { - return currentResponse + return currentResponse, toolResults } + var results []string for _, tc := range toolCalls { result, err := d.mcpMgr.Invoke(tc.Tool, tc.Arguments, sessionID) if err != nil { d.Log.Printf("tool invoke error: %v", err) + results = append(results, fmt.Sprintf("Error calling %s: %v", tc.Tool, err)) continue } + + var resultText string + for _, c := range result.Content { + resultText += c.Text + } + results = append(results, fmt.Sprintf("Tool %s returned: %s", tc.Tool, resultText)) + toolResults = append(toolResults, fmt.Sprintf("%s → %s", tc.Tool, resultText)) d.Log.Printf("tool %s result: %s", tc.Tool, result.Status) } - // Regenerate with tool results - newResp, err := d.wmClient.Generate(originalPrompt + "\n\n[Tool results processing complete. Continue.]") + newResp, err := d.wmClient.Generate(originalPrompt + "\n\nTool results:\n" + strings.Join(results, "\n") + "\n\nContinue.") if err != nil { d.Log.Printf("re-generate error: %v", err) - return currentResponse + return currentResponse, toolResults } currentResponse = newResp } - return currentResponse + return currentResponse, toolResults } func parseToolCalls(response string) []ToolCall { @@ -158,6 +175,60 @@ func parseToolCalls(response string) []ToolCall { return calls } +func (d *Daemon) handleWideModelLoad(env Envelope, conn *ClientConn) { + var payload WideModelLoadPayload + if err := json.Unmarshal(env.Payload, &payload); err != nil { + d.SendError(env, conn, "E_INVALID_PAYLOAD", err.Error()) + return + } + + modelPath := payload.ModelPath + if modelPath == "" { + modelPath = filepath.Join(d.Config.ModelDir, "wide", "active") + } + + if _, err := os.Stat(modelPath); os.IsNotExist(err) { + d.SendError(env, conn, "E_MODEL_NOT_FOUND", modelPath) + return + } + + if err := d.wmClient.Load(modelPath); err != nil { + d.SendError(env, conn, "E_MODEL_LOAD_FAILED", err.Error()) + return + } + + d.SendOK(env, conn, WideModelLoadedPayload{ + Status: "ok", + ModelInfo: &WideModelInfo{ + Loaded: d.wmClient.LoadedModelName(), + RAMUsageMB: 0, + }, + }) +} + +func (d *Daemon) handleWideModelUnload(env Envelope, conn *ClientConn) { + var payload WideModelUnloadPayload + if err := json.Unmarshal(env.Payload, &payload); err != nil { + d.SendError(env, conn, "E_INVALID_PAYLOAD", err.Error()) + return + } + + reason := payload.Reason + if reason == "" { + reason = "requested" + } + + if err := d.wmClient.Unload(reason); err != nil { + d.SendError(env, conn, "E_MODEL_UNLOAD_FAILED", err.Error()) + return + } + + d.SendOK(env, conn, WideModelUnloadedPayload{ + Status: "ok", + RAMFreedMB: 0, + }) +} + func (d *Daemon) handleSystemCode(env Envelope, conn *ClientConn) { var payload SystemCodePayload if err := json.Unmarshal(env.Payload, &payload); err != nil { @@ -198,9 +269,10 @@ func (d *Daemon) handleSystemCode(env Envelope, conn *ClientConn) { case "idle": effect = "entering idle state" - d.SetState(StateIdle) + d.SetState(StateIdleRequested) d.wmClient.Unload("idle") d.mcpMgr.ShutdownAll() + d.SetState(StateIdle) case "security": effect = "SECURITY SHUTDOWN: terminating all processes" @@ -341,7 +413,7 @@ func (d *Daemon) handleStatusRequest(env Envelope, conn *ClientConn) { State: state, UptimeSeconds: uptime, WideModel: wmStatus, - PatchesInstalled: 0, + PatchesInstalled: d.patchCount(), MCPServersActive: mcpCount, } respPayload, _ := json.Marshal(payload) From 142b9d198d5978539032ec1a77aeac128faa3a32 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Fri, 3 Jul 2026 18:19:30 -0400 Subject: [PATCH 4/7] fix: origin validation for security/reset codes, seccomp/cgroups/chroot on MCP spawn, healthcheck loop, graceful shutdown handshake (#30) From 3f762637005362b8c2155a077ab81186bec72517 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Fri, 3 Jul 2026 18:19:33 -0400 Subject: [PATCH 5/7] wire Wide Model lifecycle: handleWideModelLoad/Unload, output routing by content_type, MCP result feedback loop, shutdown actions (peripheral power-off/suspend/data wipe), StateIdleRequested, patchCount in status, idle timer startup sequence (#29) From ccdf62e2550aa12c6d9ca31df3ba263253f41180 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sat, 4 Jul 2026 23:27:00 -0400 Subject: [PATCH 6/7] feat: add Raw Model validation hook for package management tools (#34) --- internal/config/config.go | 3 ++ internal/daemon/handlers.go | 44 +++++++++++++++++++ internal/daemon/mcp_lifecycle.go | 74 ++++++++++++++++++++++++++++++++ internal/daemon/raw_client.go | 14 ++++++ internal/daemon/types.go | 29 +++++++++++++ 5 files changed, 164 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 7a3e236..a5428e4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -16,6 +16,7 @@ type Config struct { AuditDir string LogDir string InferenceURL string + RegistryURL string AuditInterval int MCPBinDir string MCPBridges []string @@ -32,6 +33,7 @@ var Default = Config{ AuditDir: "/cognitiveos/audit", LogDir: "/cognitiveos/logs", InferenceURL: "http://127.0.0.1:11434", + RegistryURL: "https://registry.cognitiveos.org", AuditInterval: 60, MCPBinDir: "/cognitiveos/bin", MCPBridges: []string{ @@ -40,6 +42,7 @@ var Default = Config{ "network-mcp", "gpio-mcp", "serial-mcp", + "package-mcp", }, } diff --git a/internal/daemon/handlers.go b/internal/daemon/handlers.go index aa8259a..ae7e2e9 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -112,6 +112,50 @@ func (d *Daemon) toolLoop(response, originalPrompt, sessionID string) (string, [ var results []string for _, tc := range toolCalls { + // Validate if tool is in a validated namespace + if isToolValidated(tc.Tool) { + op := operationFromTool(tc.Tool) + pkgName, _ := tc.Arguments["name"].(string) + version, _ := tc.Arguments["version"].(string) + + var manifestMeta *PackageManifestMetadata + if op == "install" || op == "update" { + pkg := pkgName + if pkg == "" { + if name, ok := tc.Arguments["package_name"].(string); ok { + pkg = name + } + } + if pkg != "" { + manifestMeta = d.mcpMgr.fetchManifestMetadata(d.Config.RegistryURL, pkg, version) + } + } + + validationParams := PackageValidationParams{ + Operation: op, + PackageName: pkgName, + Version: version, + ManifestMetadata: manifestMeta, + } + + validationResult, err := d.rmClient.ValidatePackageRequest(validationParams) + if err != nil { + d.Log.Printf("package validation error: %v", err) + results = append(results, fmt.Sprintf("Error validating %s: %v", tc.Tool, err)) + continue + } + + if validationResult.Status != "approved" { + reason := validationResult.Reason + if reason == "" { + reason = "operation denied by system guardrail" + } + d.Log.Printf("package validation denied: %s (%s)", tc.Tool, reason) + results = append(results, fmt.Sprintf("Tool %s denied: %s", tc.Tool, reason)) + continue + } + } + result, err := d.mcpMgr.Invoke(tc.Tool, tc.Arguments, sessionID) if err != nil { d.Log.Printf("tool invoke error: %v", err) diff --git a/internal/daemon/mcp_lifecycle.go b/internal/daemon/mcp_lifecycle.go index 52a7136..90cba0e 100644 --- a/internal/daemon/mcp_lifecycle.go +++ b/internal/daemon/mcp_lifecycle.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/json" "fmt" + "net/http" "os" "os/exec" "path/filepath" @@ -219,6 +220,79 @@ func (m *MCPManager) Unregister(name string) { } } +var validatedNamespaces = map[string]bool{ + "cognitiveos.package": true, +} + +func isToolValidated(toolName string) bool { + for ns := range validatedNamespaces { + if strings.HasPrefix(toolName, ns) { + return true + } + } + return false +} + +func operationFromTool(toolName string) string { + parts := strings.Split(toolName, ".") + if len(parts) < 3 { + return "" + } + action := parts[len(parts)-1] + switch action { + case "search": + return "search" + case "list": + return "list" + case "install": + return "install" + case "remove": + return "remove" + case "info": + return "info" + case "update": + return "update" + default: + return action + } +} + +func (m *MCPManager) fetchManifestMetadata(registryURL, packageName, version string) *PackageManifestMetadata { + if registryURL == "" { + return nil + } + + url := registryURL + "/v1/patches/" + packageName + if version != "" { + url += "/" + version + } + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(url) + if err != nil { + m.daemon.Log.Printf("fetch manifest: %v", err) + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil + } + + var manifest PackageRegistryManifest + if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil { + m.daemon.Log.Printf("decode manifest: %v", err) + return nil + } + + return &PackageManifestMetadata{ + HasRawModel: manifest.HasRawModel, + IsCritical: manifest.IsCritical, + DiskSpaceMB: manifest.DiskSpaceMB, + Registry: manifest.Registry, + } +} + func (m *MCPManager) Invoke(toolName string, args map[string]interface{}, sessionID string) (MCPResultPayload, error) { m.mu.RLock() defer m.mu.RUnlock() diff --git a/internal/daemon/raw_client.go b/internal/daemon/raw_client.go index 1fbee7c..782e262 100644 --- a/internal/daemon/raw_client.go +++ b/internal/daemon/raw_client.go @@ -176,6 +176,20 @@ func (r *RawModelClient) ValidatePrompt(prompt string) (string, string, string, return resp.Action, resp.ModifiedPrompt, resp.Reason, nil } +func (r *RawModelClient) ValidatePackageRequest(params PackageValidationParams) (PackageValidationResult, error) { + result, err := r.call("validate_package_request", params) + if err != nil { + return PackageValidationResult{}, err + } + + var resp PackageValidationResult + if err := json.Unmarshal(result, &resp); err != nil { + return PackageValidationResult{}, fmt.Errorf("parse validate_package_request response: %w", err) + } + + return resp, nil +} + func (r *RawModelClient) IsReady() bool { r.mu.Lock() defer r.mu.Unlock() diff --git a/internal/daemon/types.go b/internal/daemon/types.go index 55e83a1..58b85e7 100644 --- a/internal/daemon/types.go +++ b/internal/daemon/types.go @@ -199,6 +199,35 @@ type ShutdownNoticePayload struct { Reason string `json:"reason"` } +type PackageValidationParams struct { + Operation string `json:"operation"` + PackageName string `json:"package_name"` + Version string `json:"version,omitempty"` + ManifestMetadata *PackageManifestMetadata `json:"manifest_metadata,omitempty"` +} + +type PackageManifestMetadata struct { + HasRawModel bool `json:"has_raw_model,omitempty"` + DiskSpaceMB int64 `json:"disk_space_mb,omitempty"` + Registry string `json:"registry,omitempty"` + IsCritical bool `json:"is_critical,omitempty"` +} + +type PackageValidationResult struct { + Status string `json:"status"` + Reason string `json:"reason"` + Command string `json:"command"` +} + +type PackageRegistryManifest struct { + Name string `json:"name"` + Version string `json:"version"` + HasRawModel bool `json:"has_raw_model,omitempty"` + IsCritical bool `json:"is_critical,omitempty"` + DiskSpaceMB int64 `json:"disk_space_mb,omitempty"` + Registry string `json:"registry,omitempty"` +} + func NewEnvelope(msgType string, from string, payload interface{}) Envelope { b, _ := json.Marshal(payload) return Envelope{ From 710ff6e414a3aa959b18e1f93a3ff9cc4f3cdf65 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sun, 5 Jul 2026 20:59:12 -0400 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20multi-model=20routing=20=E2=80=94?= =?UTF-8?q?=20model=20registry,=20hot-swap,=20config-driven=20RawModelPath?= =?UTF-8?q?=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: multi-model routing — model registry, hot-swap, config-driven RawModelPath, system prompt merge * fix: restore WideModelStatus struct with ModelID field (was accidentally removed in model registry refactor) * fix: remove NoNewPrivileges for Go 1.23 compatibility --- internal/config/config.go | 5 + internal/daemon/daemon.go | 236 +++++++++++++++++++++++++++++-- internal/daemon/handlers.go | 17 ++- internal/daemon/mcp_lifecycle.go | 5 +- internal/daemon/raw_client.go | 12 +- internal/daemon/types.go | 27 ++-- internal/daemon/wide_client.go | 51 ++++++- 7 files changed, 321 insertions(+), 32 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index a5428e4..d9f70ce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,6 +20,7 @@ type Config struct { AuditInterval int MCPBinDir string MCPBridges []string + RawModelPath string } var Default = Config{ @@ -44,6 +45,7 @@ var Default = Config{ "serial-mcp", "package-mcp", }, + RawModelPath: "/cognitiveos/models/raw/raw-model.gguf", } func FromEnv() Config { @@ -69,6 +71,9 @@ func FromEnv() Config { if v := os.Getenv("COGNITIVEOS_MCP_BIN_DIR"); v != "" { c.MCPBinDir = v } + if v := os.Getenv("COGNITIVEOS_RAW_MODEL_PATH"); v != "" { + c.RawModelPath = v + } // Derive paths from base dirs c.SocketPath = filepath.Join(c.RunDir, "daemon.sock") diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index b35c6c3..112cb0e 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -44,6 +44,9 @@ type Daemon struct { clients map[string]*ClientConn clientsMu sync.RWMutex + modelRegistry map[string]ModelRegistryEntry + modelRegistryMu sync.RWMutex + signalCh chan os.Signal done chan struct{} idleTimer *time.Timer @@ -59,13 +62,14 @@ func New(cfg config.Config) *Daemon { } return &Daemon{ - Config: cfg, - State: StateIdle, - startTime: time.Now(), - clients: make(map[string]*ClientConn), - signalCh: make(chan os.Signal, 1), - done: make(chan struct{}), - Log: logger, + Config: cfg, + State: StateIdle, + startTime: time.Now(), + clients: make(map[string]*ClientConn), + modelRegistry: make(map[string]ModelRegistryEntry), + signalCh: make(chan os.Signal, 1), + done: make(chan struct{}), + Log: logger, } } @@ -155,6 +159,185 @@ func (d *Daemon) scanPatches() { } } d.Log.Printf("patches scanned: %d installed", count) + d.buildModelRegistry() +} + +func (d *Daemon) buildModelRegistry() { + d.modelRegistryMu.Lock() + defer d.modelRegistryMu.Unlock() + + d.modelRegistry = make(map[string]ModelRegistryEntry) + + entries, err := os.ReadDir(d.Config.PatchDir) + if err != nil { + d.Log.Printf("build model registry: %v", err) + return + } + + for _, e := range entries { + if !e.IsDir() { + continue + } + manifestPath := filepath.Join(d.Config.PatchDir, e.Name(), "cognitive.json") + data, err := os.ReadFile(manifestPath) + if err != nil { + continue + } + + var manifest struct { + Brain *struct { + WideModel *struct { + Routing *struct { + ModelID string `json:"model_id"` + Tags []string `json:"tags"` + } `json:"routing"` + Weights *struct { + Remote *struct { + Filename string `json:"filename"` + } `json:"remote"` + } `json:"weights"` + } `json:"wide_model"` + } `json:"brain"` + } + + if err := json.Unmarshal(data, &manifest); err != nil { + continue + } + + if manifest.Brain == nil || manifest.Brain.WideModel == nil || manifest.Brain.WideModel.Routing == nil { + continue + } + + routing := manifest.Brain.WideModel.Routing + if routing.ModelID == "" { + continue + } + + ggufPath := "" + if manifest.Brain.WideModel.Weights != nil && manifest.Brain.WideModel.Weights.Remote != nil { + ggufPath = filepath.Join(d.Config.PatchDir, e.Name(), "weights", + manifest.Brain.WideModel.Weights.Remote.Filename) + } + + if ggufPath == "" { + d.Log.Printf("model registry: no gguf weights path for %s, skipping", routing.ModelID) + continue + } + + if _, err := os.Stat(ggufPath); err != nil { + d.Log.Printf("model registry: gguf not found at %s for %s", ggufPath, routing.ModelID) + continue + } + + d.modelRegistry[routing.ModelID] = ModelRegistryEntry{ + ModelID: routing.ModelID, + Tags: routing.Tags, + GGUFFilePath: ggufPath, + } + d.Log.Printf("model registry: registered %s (%s)", routing.ModelID, ggufPath) + } +} + +func (d *Daemon) modelRegistryRoutingHints() map[string][]string { + d.modelRegistryMu.RLock() + defer d.modelRegistryMu.RUnlock() + + hints := make(map[string][]string, len(d.modelRegistry)) + for id, entry := range d.modelRegistry { + hints[id] = entry.Tags + } + return hints +} + +func (d *Daemon) resolveModelGGUF(modelID string) string { + d.modelRegistryMu.RLock() + defer d.modelRegistryMu.RUnlock() + + if entry, ok := d.modelRegistry[modelID]; ok { + return entry.GGUFFilePath + } + return "" +} + +func (d *Daemon) hotSwapWideModel(modelID string) error { + if modelID == "" { + return nil + } + + currentID := d.wmClient.LoadedModelID() + if currentID == modelID { + return nil + } + + ggufPath := d.resolveModelGGUF(modelID) + if ggufPath == "" { + return fmt.Errorf("model %s not found in registry", modelID) + } + + d.Log.Printf("hot-swap: unloading current model, loading %s (%s)", modelID, ggufPath) + + if err := d.wmClient.Unload("swap"); err != nil { + d.Log.Printf("hot-swap: unload error: %v", err) + } + + systemPrompt, err := d.mergeSystemPrompts(modelID) + if err != nil { + d.Log.Printf("hot-swap: merge system prompts: %v", err) + } + + if err := d.wmClient.LoadWithID(ggufPath, modelID); err != nil { + d.Log.Printf("hot-swap: load error: %v", err) + return fmt.Errorf("load %s: %w", modelID, err) + } + + if systemPrompt != "" { + d.wmClient.SetSystemPrompt(systemPrompt) + } + + d.Log.Printf("hot-swap: active model is now %s", modelID) + return nil +} + +func (d *Daemon) mergeSystemPrompts(modelID string) (string, error) { + var prompts []string + + basePath := "/cognitiveos/etc/base-prompt.md" + if data, err := os.ReadFile(basePath); err == nil { + prompts = append(prompts, string(data)) + } + + entries, err := os.ReadDir(d.Config.PatchDir) + if err != nil { + return strings.Join(prompts, "\n"), nil + } + + for _, e := range entries { + if !e.IsDir() { + continue + } + manifestPath := filepath.Join(d.Config.PatchDir, e.Name(), "cognitive.json") + data, err := os.ReadFile(manifestPath) + if err != nil { + continue + } + var manifest struct { + Runtime *struct { + SystemPrompt string `json:"system_prompt"` + } `json:"runtime"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + continue + } + if manifest.Runtime != nil && manifest.Runtime.SystemPrompt != "" { + promptPath := filepath.Join(d.Config.PatchDir, e.Name(), manifest.Runtime.SystemPrompt) + if promptData, err := os.ReadFile(promptPath); err == nil { + prompts = append(prompts, string(promptData)) + } + } + } + + merged := strings.Join(prompts, "\n\n") + return merged, nil } func (d *Daemon) patchCount() int { @@ -175,6 +358,39 @@ func (d *Daemon) patchCount() int { } func (d *Daemon) loadWideModel() error { + d.modelRegistryMu.RLock() + hasRegistry := len(d.modelRegistry) > 0 + d.modelRegistryMu.RUnlock() + + if hasRegistry { + d.modelRegistryMu.RLock() + for id, entry := range d.modelRegistry { + if d.rmClient.IsReady() { + _, _, _, allowed, err := d.rmClient.AuditResources(0) + if err != nil { + d.Log.Printf("audit before load: %v", err) + } else if !allowed { + d.Log.Printf("WARN: insufficient resources for Wide Model load") + d.modelRegistryMu.RUnlock() + return fmt.Errorf("insufficient resources") + } + } + if err := d.wmClient.LoadWithID(entry.GGUFFilePath, id); err != nil { + d.Log.Printf("load model %s (%s): %v", id, entry.GGUFFilePath, err) + continue + } + systemPrompt, _ := d.mergeSystemPrompts(id) + if systemPrompt != "" { + d.wmClient.SetSystemPrompt(systemPrompt) + } + d.Log.Printf("wide model loaded from registry: %s (%s)", id, entry.GGUFFilePath) + d.modelRegistryMu.RUnlock() + return nil + } + d.modelRegistryMu.RUnlock() + d.Log.Printf("no registry models could be loaded, falling back to directory scan") + } + modelDir := filepath.Join(d.Config.ModelDir, "wide", "active") entries, err := os.ReadDir(modelDir) if err != nil { @@ -193,7 +409,11 @@ func (d *Daemon) loadWideModel() error { return fmt.Errorf("insufficient resources") } } - return d.wmClient.Load(modelPath) + if err := d.wmClient.Load(modelPath); err != nil { + return err + } + d.Log.Printf("wide model loaded from directory: %s", modelPath) + return nil } } return fmt.Errorf("no model file found in %s", modelDir) diff --git a/internal/daemon/handlers.go b/internal/daemon/handlers.go index ae7e2e9..621ba8b 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -36,7 +36,8 @@ func (d *Daemon) handleInputForward(env Envelope, conn *ClientConn) { func (d *Daemon) processPrompt(prompt, sessionID, from string) { d.touchIdleTimer() - action, modifiedPrompt, reason, err := d.rmClient.ValidatePrompt(prompt) + routingHints := d.modelRegistryRoutingHints() + action, modifiedPrompt, reason, modelID, err := d.rmClient.ValidatePrompt(prompt, routingHints) if err != nil { d.Log.Printf("raw model validate error: %v", err) d.SendToClient(from, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ @@ -69,6 +70,12 @@ func (d *Daemon) processPrompt(prompt, sessionID, from string) { case "allow": } + if modelID != "" { + if err := d.hotSwapWideModel(modelID); err != nil { + d.Log.Printf("hot-swap to %s failed: %v, using current model", modelID, err) + } + } + resp, err := d.wmClient.Generate(prompt) if err != nil { d.Log.Printf("inference error: %v", err) @@ -442,10 +449,15 @@ func (d *Daemon) handleStatusRequest(env Envelope, conn *ClientConn) { wmStatus := WideModelStatus{Status: "unloaded"} if d.wmClient.IsLoaded() { - wmStatus = WideModelStatus{Status: "loaded", Name: d.wmClient.LoadedModelName()} + wmStatus = WideModelStatus{ + Status: "loaded", + Name: d.wmClient.LoadedModelName(), + ModelID: d.wmClient.LoadedModelID(), + } } mcpCount := d.mcpMgr.ActiveCount() + regCount := len(d.modelRegistryRoutingHints()) resp := Envelope{ Type: "status_response", @@ -457,6 +469,7 @@ func (d *Daemon) handleStatusRequest(env Envelope, conn *ClientConn) { State: state, UptimeSeconds: uptime, WideModel: wmStatus, + ModelRegistry: regCount, PatchesInstalled: d.patchCount(), MCPServersActive: mcpCount, } diff --git a/internal/daemon/mcp_lifecycle.go b/internal/daemon/mcp_lifecycle.go index 90cba0e..837a20f 100644 --- a/internal/daemon/mcp_lifecycle.go +++ b/internal/daemon/mcp_lifecycle.go @@ -81,9 +81,8 @@ var deniedSyscalls = []string{ } func setupSeccomp(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - NoNewPrivileges: true, - } + cmd.SysProcAttr = &syscall.SysProcAttr{} + // NoNewPrivileges requires Go 1.24+ — skip for 1.23 compatibility } type MCPServer struct { diff --git a/internal/daemon/raw_client.go b/internal/daemon/raw_client.go index 782e262..169e3bb 100644 --- a/internal/daemon/raw_client.go +++ b/internal/daemon/raw_client.go @@ -154,26 +154,30 @@ func (r *RawModelClient) AuditResources(requestedMB int64) (bool, int64, int64, return resp.Available, resp.TotalMB, resp.FreeMB, resp.Allowed, nil } -func (r *RawModelClient) ValidatePrompt(prompt string) (string, string, string, error) { +func (r *RawModelClient) ValidatePrompt(prompt string, routingHints map[string][]string) (string, string, string, string, error) { params := map[string]interface{}{ "prompt": prompt, } + if len(routingHints) > 0 { + params["routing_hints"] = routingHints + } result, err := r.call("validate_prompt", params) if err != nil { - return "", "", "", err + return "", "", "", "", err } var resp struct { Action string `json:"action"` ModifiedPrompt string `json:"modified_prompt,omitempty"` Reason string `json:"reason,omitempty"` + Model string `json:"model,omitempty"` } if err := json.Unmarshal(result, &resp); err != nil { - return "", "", "", fmt.Errorf("parse validate_prompt response: %w", err) + return "", "", "", "", fmt.Errorf("parse validate_prompt response: %w", err) } - return resp.Action, resp.ModifiedPrompt, resp.Reason, nil + return resp.Action, resp.ModifiedPrompt, resp.Reason, resp.Model, nil } func (r *RawModelClient) ValidatePackageRequest(params PackageValidationParams) (PackageValidationResult, error) { diff --git a/internal/daemon/types.go b/internal/daemon/types.go index 58b85e7..f6e69f6 100644 --- a/internal/daemon/types.go +++ b/internal/daemon/types.go @@ -157,21 +157,30 @@ type NetworkInfo struct { type StatusRequestPayload struct{} type StatusResponsePayload struct { - State string `json:"state"` - UptimeSeconds int64 `json:"uptime_seconds"` - WideModel WideModelStatus `json:"wide_model"` - PatchesInstalled int `json:"patches_installed"` - MCPServersActive int `json:"mcp_servers_active"` + State string `json:"state"` + UptimeSeconds int64 `json:"uptime_seconds"` + WideModel WideModelStatus `json:"wide_model"` + ModelRegistry int `json:"model_registry_count"` + PatchesInstalled int `json:"patches_installed"` + MCPServersActive int `json:"mcp_servers_active"` } type WideModelStatus struct { - Status string `json:"status"` - Name string `json:"name,omitempty"` + Status string `json:"status"` + Name string `json:"name,omitempty"` + ModelID string `json:"model_id,omitempty"` +} + +type ModelRegistryEntry struct { + ModelID string `json:"model_id"` + Tags []string `json:"tags,omitempty"` + GGUFFilePath string `json:"gguf_file_path"` } type WideModelLoadPayload struct { - ModelPath string `json:"model_path"` - Params map[string]interface{} `json:"params,omitempty"` + ModelPath string `json:"model_path"` + SystemPrompt string `json:"system_prompt,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` } type WideModelLoadedPayload struct { diff --git a/internal/daemon/wide_client.go b/internal/daemon/wide_client.go index 81eb0ff..bd5e35b 100644 --- a/internal/daemon/wide_client.go +++ b/internal/daemon/wide_client.go @@ -12,11 +12,13 @@ import ( ) type WideModelClient struct { - daemon *Daemon - client *http.Client - loaded bool - modelName string - mu sync.RWMutex + daemon *Daemon + client *http.Client + loaded bool + modelName string + modelID string + systemPrompt string + mu sync.RWMutex } func NewWideModelClient(d *Daemon) *WideModelClient { @@ -27,9 +29,18 @@ func NewWideModelClient(d *Daemon) *WideModelClient { } func (w *WideModelClient) Generate(prompt string) (string, error) { + w.mu.RLock() + sysPrompt := w.systemPrompt + w.mu.RUnlock() + + fullPrompt := prompt + if sysPrompt != "" { + fullPrompt = sysPrompt + "\n\n" + prompt + } + body := map[string]interface{}{ "model": "wide-model", - "prompt": prompt, + "prompt": fullPrompt, "stream": false, "options": map[string]interface{}{ "temperature": 0.7, @@ -73,7 +84,24 @@ func (w *WideModelClient) Generate(prompt string) (string, error) { return result.Response, nil } +func (w *WideModelClient) SetSystemPrompt(prompt string) { + w.mu.Lock() + defer w.mu.Unlock() + w.systemPrompt = prompt + w.daemon.Log.Printf("system prompt set (%d bytes)", len(prompt)) +} + +func (w *WideModelClient) SystemPrompt() string { + w.mu.RLock() + defer w.mu.RUnlock() + return w.systemPrompt +} + func (w *WideModelClient) Load(modelPath string) error { + return w.LoadWithID(modelPath, "") +} + +func (w *WideModelClient) LoadWithID(modelPath, modelID string) error { body := map[string]interface{}{ "model": "wide-model", "path": modelPath, @@ -97,6 +125,7 @@ func (w *WideModelClient) Load(modelPath string) error { w.mu.Lock() w.loaded = true w.modelName = modelPath + w.modelID = modelID w.mu.Unlock() return nil @@ -127,6 +156,7 @@ func (w *WideModelClient) Unload(reason string) error { w.mu.Lock() w.loaded = false w.modelName = "" + w.modelID = "" w.mu.Unlock() return nil @@ -147,6 +177,15 @@ func (w *WideModelClient) LoadedModelName() string { return w.modelName } +func (w *WideModelClient) LoadedModelID() string { + w.mu.RLock() + defer w.mu.RUnlock() + if !w.loaded { + return "" + } + return w.modelID +} + func (w *WideModelClient) Health() bool { resp, err := w.client.Get(w.daemon.Config.InferenceURL + "/health") if err != nil {