package service import ( "context" "fmt" "sort" "strings" "time" iop "iop/proto/gen/iop" ) var providerCommandAllowlist = map[string]struct{}{ "capabilities": {}, "transport_status": {}, "ollama_api": {}, } func edgeCommandResponseFromNodeCommand(req *iop.EdgeCommandRequest, view NodeCommandView, err error) *iop.EdgeCommandResponse { if err != nil { return &iop.EdgeCommandResponse{ Status: "error", Error: err.Error(), } } keys := make([]string, 0, len(view.Result)) for k := range view.Result { keys = append(keys, k) } sort.Strings(keys) summaries := make([]string, 0, len(keys)) for _, k := range keys { summaries = append(summaries, fmt.Sprintf("%s=%s", k, view.Result[k])) } summaryStr := fmt.Sprintf("NodeCommand %s completed: %s", req.GetParameters()["command"], strings.Join(summaries, ", ")) return &iop.EdgeCommandResponse{ Status: "completed", Summary: summaryStr, } } func unsupportedEdgeCommand(req *iop.EdgeCommandRequest, msg string) *iop.EdgeCommandResponse { return &iop.EdgeCommandResponse{ Status: "unsupported", Error: msg, } } func providerCommandEvent(req *iop.EdgeCommandRequest, phase, summary string) *iop.EdgeCommandEvent { return &iop.EdgeCommandEvent{ CommandId: req.CommandId, Phase: phase, Summary: summary, OccurredAt: time.Now().UnixNano(), } } // finishProviderCommand is the sole post-start terminal transition for a // provider command. Keeping it central makes every started command observable // as exactly one completed or failed event. func finishProviderCommand(req *iop.EdgeCommandRequest, cmdName string, response *iop.EdgeCommandResponse, onEvent func(*iop.EdgeCommandEvent)) *iop.EdgeCommandResponse { phase := "completed" summary := fmt.Sprintf("Command %s completed successfully", cmdName) if response.Status != "completed" { phase = "failed" summary = fmt.Sprintf("Command %s failed: %s", cmdName, response.Error) } onEvent(providerCommandEvent(req, phase, summary)) return response } func (s *Service) ExecuteCommand(ctx context.Context, req *iop.EdgeCommandRequest, onEvent func(*iop.EdgeCommandEvent)) (*iop.EdgeCommandResponse, error) { switch req.Operation { case "health.check": onEvent(&iop.EdgeCommandEvent{ CommandId: req.CommandId, Phase: "started", Summary: "Starting health check", OccurredAt: time.Now().UnixNano(), }) status := s.HealthStatus() summary := fmt.Sprintf("Edge health check OK: %d node(s), %d capability(ies)", len(status.Nodes), len(status.Capabilities)) onEvent(&iop.EdgeCommandEvent{ CommandId: req.CommandId, Phase: "completed", Summary: summary, OccurredAt: time.Now().UnixNano(), }) return &iop.EdgeCommandResponse{ Status: "completed", Summary: summary, }, nil case "node.status": if req.TargetSelector == "" { return nil, fmt.Errorf("target_selector is required for node.status") } onEvent(&iop.EdgeCommandEvent{ CommandId: req.CommandId, Phase: "started", Summary: fmt.Sprintf("Resolving node status for %s", req.TargetSelector), OccurredAt: time.Now().UnixNano(), }) entry, err := s.ResolveNode(req.TargetSelector) if err != nil { return &iop.EdgeCommandResponse{ Status: "error", Error: err.Error(), }, nil } snap := NodeEntrySnapshot(entry) onEvent(&iop.EdgeCommandEvent{ CommandId: req.CommandId, Phase: "completed", Summary: fmt.Sprintf("Resolved node status for %s", req.TargetSelector), OccurredAt: time.Now().UnixNano(), }) return &iop.EdgeCommandResponse{ Status: "completed", Summary: fmt.Sprintf("node ID=%s Alias=%s State=%s", snap.NodeID, snap.Alias, snap.LifecycleState), }, nil case "provider.command": if strings.TrimSpace(req.GetTargetSelector()) == "" { return nil, fmt.Errorf("target_selector is required for provider.command") } cmdName := req.GetParameters()["command"] if _, ok := providerCommandAllowlist[cmdName]; !ok { return unsupportedEdgeCommand(req, fmt.Sprintf("unsupported provider.command command %q", cmdName)), nil } onEvent(providerCommandEvent(req, "started", fmt.Sprintf("Routing command %s to node %s", cmdName, req.TargetSelector))) entry, err := s.ResolveDispatchReady(req.TargetSelector) if err != nil { return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{ Status: "error", Error: err.Error(), }, onEvent), nil } if entry.Client == nil { return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{ Status: "error", Error: "node client not connected", }, onEvent), nil } switch cmdName { case "capabilities": view, err := s.Capabilities(ctx, NodeCommandRequestSpec{ NodeRef: req.TargetSelector, Adapter: req.GetParameters()["adapter"], Target: req.GetParameters()["target"], SessionID: req.GetParameters()["session_id"], TimeoutSec: 10, }) return finishProviderCommand(req, cmdName, edgeCommandResponseFromNodeCommand(req, view, err), onEvent), nil case "transport_status": view, err := s.TransportStatus(ctx, NodeCommandRequestSpec{ NodeRef: req.TargetSelector, Adapter: req.GetParameters()["adapter"], Target: req.GetParameters()["target"], SessionID: req.GetParameters()["session_id"], TimeoutSec: 10, }) return finishProviderCommand(req, cmdName, edgeCommandResponseFromNodeCommand(req, view, err), onEvent), nil case "ollama_api": view, err := s.OllamaAPI(ctx, OllamaAPIRequest{ NodeRef: req.TargetSelector, Adapter: req.GetParameters()["adapter"], Target: req.GetParameters()["target"], Method: req.GetParameters()["method"], Path: req.GetParameters()["path"], Body: req.GetParameters()["body"], TimeoutSec: 10, }) if err != nil { return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{Status: "error", Error: err.Error()}, onEvent), nil } return finishProviderCommand(req, cmdName, &iop.EdgeCommandResponse{Status: "completed", Summary: fmt.Sprintf("provider response status=%d", view.StatusCode)}, onEvent), nil default: // The allowlist was checked before started; retain a defensive return // should the switch and allowlist ever diverge. return finishProviderCommand(req, cmdName, unsupportedEdgeCommand(req, fmt.Sprintf("unsupported provider.command command %q", cmdName)), onEvent), nil } default: return &iop.EdgeCommandResponse{ Status: "unsupported", Error: fmt.Sprintf("unsupported operation %s", req.Operation), }, nil } }