From 0c8673a766f2306cf6d8aeed3f526bbbec9419b9 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sat, 27 Jun 2026 17:09:44 -0400 Subject: [PATCH 1/3] 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 a8ebb3e..1f2ac28 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 { @@ -96,6 +97,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 e3a6e38..4db8cc8 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -58,10 +58,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 d9441ce6508adfd02106d4fa09eb9743f6d720b9 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Sat, 27 Jun 2026 17:11:54 -0400 Subject: [PATCH 2/3] 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 | 208 ++++++++++++++++++++++++++++++++++-- 2 files changed, 332 insertions(+), 15 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 1f2ac28..b795981 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 } @@ -99,6 +106,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) @@ -120,6 +140,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 @@ -129,11 +235,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() @@ -143,6 +251,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") } @@ -208,6 +331,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)) } @@ -268,6 +395,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 4db8cc8..837ebda 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" ) @@ -28,16 +30,145 @@ func (d *Daemon) handleInputForward(env Envelope, conn *ClientConn) { } go func() { - resp, err := d.wmClient.Generate(payload.Content) + d.processPrompt(payload.Content, sessionID, env.From) + }() +} + +func (d *Daemon) processPrompt(prompt, sessionID, from string) { + d.touchIdleTimer() + + 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 + + case "modify": + if modifiedPrompt != "" { + prompt = modifiedPrompt + } + case "allow": + } + + 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: fmt.Sprintf("Error: %v", err), + ContentType: "text", + })) + d.SetState(StateListening) + return + } + + 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{ + SessionID: sessionID, + Content: finalResponse, + ContentType: "text", + })) +} + +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, 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) + } + + newResp, err := d.wmClient.Generate(originalPrompt + "\n\nTool results:\n" + strings.Join(results, "\n") + "\n\nContinue.") 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.Log.Printf("re-generate error: %v", err) + return currentResponse, toolResults + } + currentResponse = newResp + } + + return currentResponse, toolResults +} + +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 + } } d.SetState(StateListening) @@ -50,6 +181,60 @@ func (d *Daemon) handleInputForward(env Envelope, conn *ClientConn) { }() } +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 { @@ -90,9 +275,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" @@ -233,7 +419,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 3b9008b1ac2f331f2d5235087e9fcb358f6890d3 Mon Sep 17 00:00:00 2001 From: Jean Machuca Date: Thu, 2 Jul 2026 17:27:35 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20parseToolCalls=20=E2=80=94=20remove?= =?UTF-8?q?=20stray=20code,=20add=20ToolCall=20struct,=20add=20return=20ca?= =?UTF-8?q?lls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/daemon/handlers.go | 10 +++------- internal/daemon/types.go | 5 +++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/daemon/handlers.go b/internal/daemon/handlers.go index 837ebda..463bd86 100644 --- a/internal/daemon/handlers.go +++ b/internal/daemon/handlers.go @@ -171,14 +171,10 @@ func parseToolCalls(response string) []ToolCall { } } - d.SetState(StateListening) + calls = append(calls, ToolCall{Tool: toolName, Arguments: args}) + } - d.SendToClient(env.From, NewEnvelope("output_deliver", "cognitiveosd", OutputPayload{ - SessionID: sessionID, - Content: resp, - ContentType: "text", - })) - }() + return calls } func (d *Daemon) handleWideModelLoad(env Envelope, conn *ClientConn) { diff --git a/internal/daemon/types.go b/internal/daemon/types.go index 01d307e..c49c05b 100644 --- a/internal/daemon/types.go +++ b/internal/daemon/types.go @@ -83,6 +83,11 @@ type MCPTool struct { InputSchema interface{} `json:"inputSchema"` } +type ToolCall struct { + Tool string `json:"tool"` + Arguments map[string]interface{} `json:"arguments"` +} + type MCPRegisteredPayload struct { Status string `json:"status"` ServerID string `json:"server_id"`