Files
micro--go-micro/cmd/micro/server/server.go
T
wehub-resource-sync e071084ebe
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

1615 lines
50 KiB
Go

package server
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"text/template"
"time"
"github.com/urfave/cli/v2"
"go-micro.dev/v6/ai"
_ "go-micro.dev/v6/ai/anthropic"
_ "go-micro.dev/v6/ai/atlascloud"
_ "go-micro.dev/v6/ai/gemini"
_ "go-micro.dev/v6/ai/groq"
_ "go-micro.dev/v6/ai/mistral"
_ "go-micro.dev/v6/ai/openai"
_ "go-micro.dev/v6/ai/together"
"go-micro.dev/v6/auth"
"go-micro.dev/v6/client"
"go-micro.dev/v6/cmd"
codecBytes "go-micro.dev/v6/codec/bytes"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/store"
"golang.org/x/crypto/bcrypt"
)
// HTML is the embedded filesystem for templates and static files, set by main.go
var HTML fs.FS
const agentSystemPrompt = "You are an agent that helps users interact with microservices. Use the available tools to fulfill user requests. When you call a tool, explain what you are doing."
var (
apiCache struct {
sync.Mutex
data map[string]any
time time.Time
}
)
type templates struct {
api *template.Template
service *template.Template
form *template.Template
home *template.Template
logs *template.Template
log *template.Template
status *template.Template
authTokens *template.Template
authLogin *template.Template
authUsers *template.Template
playground *template.Template
scopes *template.Template
}
type TemplateUser struct {
ID string
}
// Account is an alias for auth.Account from the framework.
// The gateway stores accounts in the default store under "auth/<id>" keys.
// Scopes on accounts are checked against endpoint-scopes by checkEndpointScopes.
type Account = auth.Account
func parseTemplates() *templates {
return &templates{
api: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/api.html")),
service: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/service.html")),
form: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/form.html")),
home: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/home.html")),
logs: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/logs.html")),
log: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/log.html")),
status: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/status.html")),
authTokens: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_tokens.html")),
authLogin: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")),
authUsers: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_users.html")),
playground: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/playground.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
}
}
// Helper to extract user info from JWT cookie
func getUser(r *http.Request) string {
cookie, err := r.Cookie("micro_token")
if err != nil || cookie.Value == "" {
return ""
}
// Parse JWT claims (just decode, don't verify)
parts := strings.Split(cookie.Value, ".")
if len(parts) != 3 {
return ""
}
payload, err := decodeSegment(parts[1])
if err != nil {
return ""
}
var claims map[string]any
if err := json.Unmarshal(payload, &claims); err != nil {
return ""
}
if sub, ok := claims["sub"].(string); ok {
return sub
}
if id, ok := claims["id"].(string); ok {
return id
}
return ""
}
// Helper to decode JWT base64url segment
func decodeSegment(seg string) ([]byte, error) {
// JWT uses base64url, no padding
missing := len(seg) % 4
if missing != 0 {
seg += strings.Repeat("=", 4-missing)
}
return decodeBase64Url(seg)
}
func decodeBase64Url(s string) ([]byte, error) {
return base64.URLEncoding.DecodeString(s)
}
// Helper: store JWT token
func storeJWTToken(storeInst store.Store, token, userID string) {
_ = storeInst.Write(&store.Record{Key: "jwt/" + token, Value: []byte(userID)})
}
// Helper: check if JWT token is revoked (not present in store)
func isTokenRevoked(storeInst store.Store, token string) bool {
recs, _ := storeInst.Read("jwt/" + token)
return len(recs) == 0
}
// Helper: delete all JWT tokens for a user
func deleteUserTokens(storeInst store.Store, userID string) {
recs, _ := storeInst.Read("jwt/", store.ReadPrefix())
for _, rec := range recs {
if string(rec.Value) == userID {
_ = storeInst.Delete(rec.Key)
}
}
}
// Updated authRequired to accept storeInst as argument
func authRequired(storeInst store.Store) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var token string
// 1. Check Authorization: Bearer header
authz := r.Header.Get("Authorization")
if strings.HasPrefix(authz, "Bearer ") {
token = strings.TrimPrefix(authz, "Bearer ")
token = strings.TrimSpace(token)
}
// 2. Fallback to micro_token cookie if no header
if token == "" {
cookie, err := r.Cookie("micro_token")
if err == nil && cookie.Value != "" {
token = cookie.Value
}
}
if token == "" {
if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"missing or invalid token"}`))
return
}
// For API endpoints, return 401. For UI, redirect to login.
if strings.HasPrefix(r.URL.Path, "/api/") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized: missing token"))
return
}
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
claims, err := ParseJWT(token)
if err != nil {
if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"invalid token"}`))
return
}
if strings.HasPrefix(r.URL.Path, "/api/") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized: invalid token"))
return
}
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
if exp, ok := claims["exp"].(float64); ok {
if int64(exp) < time.Now().Unix() {
if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"token expired"}`))
return
}
if strings.HasPrefix(r.URL.Path, "/api/") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized: token expired"))
return
}
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
}
// Check for token revocation
if isTokenRevoked(storeInst, token) {
if strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/api" && r.URL.Path != "/api/" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"token revoked"}`))
return
}
if strings.HasPrefix(r.URL.Path, "/api/") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized: token revoked"))
return
}
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
next(w, r)
}
}
}
func wrapAuth(authRequired func(http.HandlerFunc) http.HandlerFunc) func(http.HandlerFunc) http.HandlerFunc {
return func(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasPrefix(path, "/auth/login") || strings.HasPrefix(path, "/auth/logout") ||
path == "/styles.css" || path == "/main.js" {
h(w, r)
return
}
authRequired(h)(w, r)
}
}
}
func getDashboardData() (serviceCount, runningCount, stoppedCount int, statusDot string) {
homeDir, err := os.UserHomeDir()
if err != nil {
return
}
pidDir := homeDir + "/micro/run"
dirEntries, err := os.ReadDir(pidDir)
if err != nil {
return
}
for _, entry := range dirEntries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") || strings.HasPrefix(entry.Name(), ".") {
continue
}
pidFile := pidDir + "/" + entry.Name()
pidBytes, err := os.ReadFile(pidFile)
if err != nil {
continue
}
lines := strings.Split(string(pidBytes), "\n")
pid := "-"
if len(lines) > 0 && len(lines[0]) > 0 {
pid = lines[0]
}
serviceCount++
if pid != "-" {
if _, err := os.FindProcess(parsePid(pid)); err == nil {
if processRunning(pid) {
runningCount++
} else {
stoppedCount++
}
} else {
stoppedCount++
}
} else {
stoppedCount++
}
}
if serviceCount > 0 && runningCount == serviceCount {
statusDot = "green"
} else if serviceCount > 0 && runningCount > 0 {
statusDot = "yellow"
} else {
statusDot = "red"
}
return
}
func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Store, authEnabled bool) {
var wrap func(http.HandlerFunc) http.HandlerFunc
if authEnabled {
authMw := authRequired(storeInst)
wrap = wrapAuth(authMw)
} else {
// No auth in dev mode - pass through handlers unchanged
wrap = func(h http.HandlerFunc) http.HandlerFunc {
return h
}
}
// renderPage injects AuthEnabled into template data so the sidebar can
// conditionally show/hide auth links.
renderPage := func(w http.ResponseWriter, tmpl *template.Template, data map[string]any) error {
data["AuthEnabled"] = authEnabled
return tmpl.Execute(w, data)
}
// checkEndpointScopes verifies the caller's token scopes against the
// required scopes for a service endpoint. Returns true if allowed.
// If not allowed, writes a 403 response and returns false.
checkEndpointScopes := func(w http.ResponseWriter, r *http.Request, endpointKey string) bool {
if !authEnabled {
return true
}
recs, _ := storeInst.Read("endpoint-scopes/" + endpointKey)
if len(recs) == 0 {
return true // no scopes configured = unrestricted
}
var requiredScopes []string
if err := json.Unmarshal(recs[0].Value, &requiredScopes); err != nil || len(requiredScopes) == 0 {
return true
}
// Extract caller's scopes from JWT
callerScopes := []string{}
token := ""
if authz := r.Header.Get("Authorization"); strings.HasPrefix(authz, "Bearer ") {
token = strings.TrimPrefix(authz, "Bearer ")
}
if token == "" {
if cookie, err := r.Cookie("micro_token"); err == nil {
token = cookie.Value
}
}
if token != "" {
if claims, err := ParseJWT(token); err == nil {
if s, ok := claims["scopes"].([]interface{}); ok {
for _, v := range s {
if str, ok := v.(string); ok {
callerScopes = append(callerScopes, str)
}
}
}
}
}
for _, cs := range callerScopes {
if cs == "*" {
return true
}
for _, rs := range requiredScopes {
if cs == rs {
return true
}
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(map[string]string{
"error": "insufficient scopes",
"required_scopes": strings.Join(requiredScopes, ","),
})
return false
}
// Serve static files with correct Content-Type
mux.HandleFunc("/styles.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
f, err := HTML.Open("web/styles.css")
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
defer f.Close()
_, _ = io.Copy(w, f)
})
mux.HandleFunc("/main.js", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
f, err := HTML.Open("web/main.js")
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
defer f.Close()
_, _ = io.Copy(w, f)
})
// MCP API endpoints - list tools and call tools through the web server
mux.HandleFunc("/mcp/tools", wrap(func(w http.ResponseWriter, r *http.Request) {
services, err := registry.ListServices()
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
var tools []map[string]any
for _, svc := range services {
fullSvcs, err := registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
toolName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
description := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if ep.Metadata != nil {
if desc, ok := ep.Metadata["description"]; ok && desc != "" {
description = desc
}
}
inputSchema := map[string]any{
"type": "object",
"properties": map[string]any{},
}
if ep.Request != nil && len(ep.Request.Values) > 0 {
props := inputSchema["properties"].(map[string]any)
for _, field := range ep.Request.Values {
props[field.Name] = map[string]any{
"type": mapGoTypeToJSON(field.Type),
"description": fmt.Sprintf("%s field", field.Name),
}
}
}
tool := map[string]any{
"name": toolName,
"description": description,
"inputSchema": inputSchema,
}
// Extract scopes from endpoint metadata or store
if ep.Metadata != nil {
if scopes, ok := ep.Metadata["scopes"]; ok && scopes != "" {
tool["scopes"] = strings.Split(scopes, ",")
}
}
// Override with stored scopes (from UI) if present
if recs, _ := storeInst.Read("endpoint-scopes/" + toolName); len(recs) > 0 {
var storedScopes []string
if err := json.Unmarshal(recs[0].Value, &storedScopes); err == nil && len(storedScopes) > 0 {
tool["scopes"] = storedScopes
}
}
tools = append(tools, tool)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"tools": tools})
}))
mux.HandleFunc("/mcp/call", wrap(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
return
}
var req struct {
Tool string `json:"tool"`
Input map[string]any `json:"input"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Parse tool name into service and endpoint
parts := strings.SplitN(req.Tool, ".", 2)
if len(parts) != 2 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid tool name, expected service.endpoint"})
return
}
serviceName := parts[0]
endpointName := parts[1]
// Check endpoint scopes
if !checkEndpointScopes(w, r, req.Tool) {
return
}
// Build RPC request using default client
inputBytes, err := json.Marshal(req.Input)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
rpcReq := client.DefaultClient.NewRequest(serviceName, endpointName, &codecBytes.Frame{Data: inputBytes})
var rsp codecBytes.Frame
if err := client.DefaultClient.Call(r.Context(), rpcReq, &rsp); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("RPC call failed: %v", err)})
return
}
var traceBytes [16]byte
_, _ = rand.Read(traceBytes[:])
traceID := fmt.Sprintf("%x", traceBytes)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"result": json.RawMessage(rsp.Data),
"trace_id": traceID,
})
}))
// Agent settings endpoints
mux.HandleFunc("/api/agent/settings", wrap(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
recs, _ := storeInst.Read("agent/settings")
if len(recs) == 0 {
json.NewEncoder(w).Encode(map[string]string{})
return
}
var settings map[string]string
if err := json.Unmarshal(recs[0].Value, &settings); err != nil {
log.Printf("[agent] failed to parse settings: %v", err)
json.NewEncoder(w).Encode(map[string]string{})
return
}
json.NewEncoder(w).Encode(settings)
return
}
if r.Method == http.MethodPost {
var settings map[string]string
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
b, _ := json.Marshal(settings)
_ = storeInst.Write(&store.Record{Key: "agent/settings", Value: b})
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
}))
// Agent prompt endpoint — sends user prompt to LLM with tool definitions
mux.HandleFunc("/api/agent/prompt", wrap(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
return
}
var req struct {
Prompt string `json:"prompt"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Load settings
recs, _ := storeInst.Read("agent/settings")
var settings map[string]string
if len(recs) > 0 {
if err := json.Unmarshal(recs[0].Value, &settings); err != nil {
log.Printf("[agent] failed to parse settings: %v", err)
}
}
apiKey := ""
modelName := ""
baseURL := ""
provider := ""
if settings != nil {
if v := settings["api_key"]; v != "" {
apiKey = v
}
if v := settings["model"]; v != "" {
modelName = v
}
if v := settings["base_url"]; v != "" {
baseURL = v
}
if v := settings["provider"]; v != "" {
provider = v
}
}
if apiKey == "" {
json.NewEncoder(w).Encode(map[string]string{"error": "No API key configured. Go to Agent settings to add one."})
return
}
// Auto-detect provider if not explicitly set
if provider == "" {
provider = ai.AutoDetectProvider(baseURL)
}
// Discover tools from registry
services, _ := registry.ListServices()
var discoveredTools []ai.Tool
// safeNameMap maps LLM-safe names back to original dotted names
safeNameMap := map[string]string{}
for _, svc := range services {
fullSvcs, err := registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
tName := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
safeName := strings.ReplaceAll(tName, ".", "_")
safeNameMap[safeName] = tName
desc := fmt.Sprintf("Call %s on %s service", ep.Name, svc.Name)
if ep.Metadata != nil {
if d, ok := ep.Metadata["description"]; ok && d != "" {
desc = d
}
}
props := map[string]any{}
if ep.Request != nil {
for _, field := range ep.Request.Values {
props[field.Name] = map[string]any{
"type": mapGoTypeToJSON(field.Type),
"description": fmt.Sprintf("%s (%s)", field.Name, field.Type),
}
}
}
discoveredTools = append(discoveredTools, ai.Tool{
Name: safeName,
OriginalName: tName,
Description: desc,
Properties: props,
})
}
}
// executeToolCall runs an RPC tool call and returns the result.
// toolName can be either the original dotted name or the LLM-safe
// underscored name; the safe name is resolved first.
// Checks endpoint scopes against the caller's token before executing.
executeToolCall := func(_ context.Context, call ai.ToolCall) ai.ToolResult {
toolName := call.Name
input := call.Input
if orig, ok := safeNameMap[toolName]; ok {
toolName = orig
}
// Check endpoint scopes
if authEnabled {
recs, _ := storeInst.Read("endpoint-scopes/" + toolName)
if len(recs) > 0 {
var requiredScopes []string
if err := json.Unmarshal(recs[0].Value, &requiredScopes); err == nil && len(requiredScopes) > 0 {
// Get caller's scopes from JWT
callerScopes := []string{}
token := ""
if authz := r.Header.Get("Authorization"); strings.HasPrefix(authz, "Bearer ") {
token = strings.TrimPrefix(authz, "Bearer ")
}
if token == "" {
if cookie, err := r.Cookie("micro_token"); err == nil {
token = cookie.Value
}
}
if token != "" {
if claims, err := ParseJWT(token); err == nil {
if s, ok := claims["scopes"].([]interface{}); ok {
for _, v := range s {
if str, ok := v.(string); ok {
callerScopes = append(callerScopes, str)
}
}
}
}
}
allowed := false
for _, cs := range callerScopes {
if cs == "*" {
allowed = true
break
}
for _, rs := range requiredScopes {
if cs == rs {
allowed = true
break
}
}
if allowed {
break
}
}
if !allowed {
errMsg := fmt.Sprintf(`{"error":"insufficient scopes","required_scopes":"%s"}`, strings.Join(requiredScopes, ","))
return ai.ToolResult{ID: call.ID, Value: map[string]string{"error": "insufficient scopes", "required_scopes": strings.Join(requiredScopes, ",")}, Content: errMsg}
}
}
}
}
parts := strings.SplitN(toolName, ".", 2)
if len(parts) != 2 {
errMsg := `{"error":"invalid tool name"}`
return ai.ToolResult{ID: call.ID, Value: map[string]string{"error": "invalid tool name"}, Content: errMsg}
}
inputBytes, _ := json.Marshal(input)
rpcReq := client.DefaultClient.NewRequest(parts[0], parts[1], &codecBytes.Frame{Data: inputBytes})
var rsp codecBytes.Frame
if err := client.DefaultClient.Call(r.Context(), rpcReq, &rsp); err != nil {
errMsg := fmt.Sprintf(`{"error":"%s"}`, err.Error())
return ai.ToolResult{ID: call.ID, Value: map[string]string{"error": err.Error()}, Content: errMsg}
}
var rpcResult any
if err := json.Unmarshal(rsp.Data, &rpcResult); err != nil {
rpcResult = string(rsp.Data)
}
return ai.ToolResult{ID: call.ID, Value: rpcResult, Content: string(rsp.Data)}
}
// Create model with options
var modelOpts []ai.Option
modelOpts = append(modelOpts, ai.WithAPIKey(apiKey))
if modelName != "" {
modelOpts = append(modelOpts, ai.WithModel(modelName))
}
if baseURL != "" {
modelOpts = append(modelOpts, ai.WithBaseURL(baseURL))
}
modelOpts = append(modelOpts, ai.WithToolHandler(executeToolCall))
m := ai.New(provider, modelOpts...)
if m == nil {
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to create model provider"})
return
}
// Build request
modelReq := &ai.Request{
Prompt: req.Prompt,
SystemPrompt: agentSystemPrompt,
Tools: discoveredTools,
}
// Generate response
response, err := m.Generate(r.Context(), modelReq)
if err != nil {
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
// Build result
result := map[string]any{}
if response.Reply != "" {
result["reply"] = response.Reply
}
if len(response.ToolCalls) > 0 {
var toolCalls []map[string]any
for _, tc := range response.ToolCalls {
toolCalls = append(toolCalls, map[string]any{
"tool": tc.Name,
"input": tc.Input,
})
}
result["tool_calls"] = toolCalls
}
if response.Answer != "" {
result["answer"] = response.Answer
}
json.NewEncoder(w).Encode(result)
}))
mux.HandleFunc("/", wrap(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasPrefix(path, "/auth/") {
// Let the dedicated /auth/* handlers process this
return
}
userID := getUser(r)
var user any
if userID != "" {
user = &TemplateUser{ID: userID}
} else {
user = nil
}
if path == "/" {
serviceCount, runningCount, stoppedCount, statusDot := getDashboardData()
// Fetch registered services for the home page
var homeServices []string
if svcs, err := registry.ListServices(); err == nil {
for _, s := range svcs {
homeServices = append(homeServices, s.Name)
}
sort.Strings(homeServices)
}
err := renderPage(w, tmpls.home, map[string]any{
"Title": "Home",
"WebLink": "/",
"ServiceCount": serviceCount,
"RunningCount": runningCount,
"StoppedCount": stoppedCount,
"StatusDot": statusDot,
"Services": homeServices,
"User": user,
})
if err != nil {
log.Printf("[TEMPLATE ERROR] home: %v", err)
}
return
}
if path == "/api" || path == "/api/" {
apiCache.Lock()
useCache := false
if apiCache.data != nil && time.Since(apiCache.time) < 30*time.Second {
useCache = true
}
var apiData map[string]any
var sidebarEndpoints []map[string]string
if useCache {
apiData = apiCache.data
if v, ok := apiData["SidebarEndpoints"]; ok {
sidebarEndpoints, _ = v.([]map[string]string)
}
} else {
services, _ := registry.ListServices()
var apiServices []map[string]any
for _, srv := range services {
srvs, err := registry.GetService(srv.Name)
if err != nil || len(srvs) == 0 {
continue
}
s := srvs[0]
if len(s.Endpoints) == 0 {
continue
}
endpoints := []map[string]any{}
for _, ep := range s.Endpoints {
parts := strings.Split(ep.Name, ".")
if len(parts) != 2 {
continue
}
apiPath := fmt.Sprintf("/api/%s/%s/%s", s.Name, parts[0], parts[1])
var params, response string
if ep.Request != nil && len(ep.Request.Values) > 0 {
params += "<ul class=no-bullets>"
for _, v := range ep.Request.Values {
params += fmt.Sprintf("<li><b>%s</b> <span style='color:#888;'>%s</span></li>", v.Name, v.Type)
}
params += "</ul>"
} else {
params = "<i style='color:#888;'>No parameters</i>"
}
if ep.Response != nil && len(ep.Response.Values) > 0 {
response += "<ul class=no-bullets>"
for _, v := range ep.Response.Values {
response += fmt.Sprintf("<li><b>%s</b> <span style='color:#888;'>%s</span></li>", v.Name, v.Type)
}
response += "</ul>"
} else {
response = "<i style='color:#888;'>No response fields</i>"
}
endpoints = append(endpoints, map[string]any{
"Name": ep.Name,
"Path": apiPath,
"Params": params,
"Response": response,
})
}
anchor := strings.ReplaceAll(s.Name, ".", "-")
apiServices = append(apiServices, map[string]any{
"Name": s.Name,
"Anchor": anchor,
"Endpoints": endpoints,
})
sidebarEndpoints = append(sidebarEndpoints, map[string]string{"Name": s.Name, "Anchor": anchor})
}
sort.Slice(sidebarEndpoints, func(i, j int) bool {
return sidebarEndpoints[i]["Name"] < sidebarEndpoints[j]["Name"]
})
apiData = map[string]any{"Title": "API", "WebLink": "/", "Services": apiServices, "SidebarEndpoints": sidebarEndpoints, "SidebarEndpointsEnabled": true, "User": user}
apiCache.data = apiData
apiCache.time = time.Now()
}
apiCache.Unlock()
// Add API auth doc at the top
apiData["ApiAuthDoc"] = `<div style='background:#f8f8e8; border:1px solid #e0e0b0; padding:1em; margin-bottom:2em; font-size:1.08em;'>
<b>API Authentication Required:</b> All API calls to <code>/api/...</code> endpoints (except this page) must include an <b>Authorization: Bearer &lt;token&gt;</b> header. <br>
You can generate tokens on the <a href='/auth/tokens'>Tokens page</a>.
</div>`
_ = renderPage(w, tmpls.api, apiData)
return
}
if path == "/services" {
// Do NOT include SidebarEndpoints on this page
services, _ := registry.ListServices()
var serviceNames []string
for _, service := range services {
serviceNames = append(serviceNames, service.Name)
}
sort.Strings(serviceNames)
_ = renderPage(w, tmpls.service, map[string]any{"Title": "Services", "WebLink": "/", "Services": serviceNames, "User": user})
return
}
if path == "/agent" {
_ = renderPage(w, tmpls.playground, map[string]any{"Title": "Agent", "WebLink": "/", "User": user})
return
}
if path == "/logs" || path == "/logs/" {
// Do NOT include SidebarEndpoints on this page
homeDir, err := os.UserHomeDir()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not get home directory"))
return
}
logsDir := homeDir + "/micro/logs"
dirEntries, err := os.ReadDir(logsDir)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not list logs directory: " + err.Error()))
return
}
serviceNames := []string{}
for _, entry := range dirEntries {
name := entry.Name()
if !entry.IsDir() && strings.HasSuffix(name, ".log") && !strings.HasPrefix(name, ".") {
serviceNames = append(serviceNames, strings.TrimSuffix(name, ".log"))
}
}
_ = renderPage(w, tmpls.logs, map[string]any{"Title": "Logs", "WebLink": "/", "Services": serviceNames, "User": user})
return
}
if strings.HasPrefix(path, "/logs/") {
// Do NOT include SidebarEndpoints on this page
service := strings.TrimPrefix(path, "/logs/")
if service == "" {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Service not specified"))
return
}
homeDir, err := os.UserHomeDir()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not get home directory"))
return
}
logFilePath := homeDir + "/micro/logs/" + service + ".log"
f, err := os.Open(logFilePath)
if err != nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Could not open log file for service: " + service))
return
}
defer f.Close()
logBytes, err := io.ReadAll(f)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not read log file for service: " + service))
return
}
logText := string(logBytes)
_ = renderPage(w, tmpls.log, map[string]any{"Title": "Logs for " + service, "WebLink": "/logs", "Service": service, "Log": logText, "User": user})
return
}
if path == "/status" {
// Do NOT include SidebarEndpoints on this page
homeDir, err := os.UserHomeDir()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not get home directory"))
return
}
pidDir := homeDir + "/micro/run"
dirEntries, err := os.ReadDir(pidDir)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Could not list pid directory: " + err.Error()))
return
}
statuses := []map[string]string{}
for _, entry := range dirEntries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") || strings.HasPrefix(entry.Name(), ".") {
continue
}
pidFile := pidDir + "/" + entry.Name()
pidBytes, err := os.ReadFile(pidFile)
if err != nil {
statuses = append(statuses, map[string]string{
"Service": entry.Name(),
"Dir": "-",
"Status": "unknown",
"PID": "-",
"Uptime": "-",
"ID": strings.TrimSuffix(entry.Name(), ".pid"),
})
continue
}
lines := strings.Split(string(pidBytes), "\n")
pid := "-"
dir := "-"
service := "-"
start := "-"
if len(lines) > 0 && len(lines[0]) > 0 {
pid = lines[0]
}
if len(lines) > 1 && len(lines[1]) > 0 {
dir = lines[1]
}
if len(lines) > 2 && len(lines[2]) > 0 {
service = lines[2]
}
if len(lines) > 3 && len(lines[3]) > 0 {
start = lines[3]
}
status := "stopped"
if pid != "-" {
if _, err := os.FindProcess(parsePid(pid)); err == nil {
if processRunning(pid) {
status = "running"
}
} else {
status = "stopped"
}
}
uptime := "-"
if start != "-" {
if t, err := parseStartTime(start); err == nil {
uptime = time.Since(t).Truncate(time.Second).String()
}
}
statuses = append(statuses, map[string]string{
"Service": service,
"Dir": dir,
"Status": status,
"PID": pid,
"Uptime": uptime,
"ID": strings.TrimSuffix(entry.Name(), ".pid"),
})
}
_ = renderPage(w, tmpls.status, map[string]any{"Title": "Status", "WebLink": "/", "Statuses": statuses, "User": user})
return
}
// Match /{service} and /{service}/{endpoint}
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) >= 1 && parts[0] != "api" && parts[0] != "html" && parts[0] != "services" {
service := parts[0]
if len(parts) == 1 {
s, err := registry.GetService(service)
if err != nil || len(s) == 0 {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Service not found: %s", service)
return
}
endpoints := []map[string]string{}
for _, ep := range s[0].Endpoints {
endpoints = append(endpoints, map[string]string{
"Name": ep.Name,
"Path": fmt.Sprintf("/%s/%s", service, ep.Name),
})
}
b, _ := json.MarshalIndent(s[0], "", " ")
_ = renderPage(w, tmpls.service, map[string]any{
"Title": "Service: " + service,
"WebLink": "/",
"ServiceName": service,
"Endpoints": endpoints,
"Description": string(b),
"User": user,
})
return
}
if len(parts) == 2 {
service := parts[0]
endpoint := parts[1] // Use the actual endpoint name from the URL, e.g. Foo.Bar
s, err := registry.GetService(service)
if err != nil || len(s) == 0 {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Service not found: " + service))
return
}
var ep *registry.Endpoint
for _, eps := range s[0].Endpoints {
if eps.Name == endpoint {
ep = eps
break
}
}
if ep == nil {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Endpoint not found"))
return
}
if r.Method == http.MethodGet {
// Build form fields from endpoint request values
var inputs []map[string]string
if ep.Request != nil && len(ep.Request.Values) > 0 {
for _, input := range ep.Request.Values {
inputs = append(inputs, map[string]string{
"Label": input.Name,
"Name": input.Name,
"Placeholder": input.Name,
"Value": "",
})
}
}
_ = renderPage(w, tmpls.form, map[string]any{
"Title": "Service: " + service,
"WebLink": "/",
"ServiceName": service,
"EndpointName": ep.Name,
"Inputs": inputs,
"Action": service + "/" + endpoint,
"User": user,
})
return
}
if r.Method == http.MethodPost {
// Check endpoint scopes
endpointKey := fmt.Sprintf("%s.%s", service, endpoint)
if !checkEndpointScopes(w, r, endpointKey) {
return
}
// Parse form values into a map
var reqBody map[string]interface{}
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
defer r.Body.Close()
_ = json.NewDecoder(r.Body).Decode(&reqBody)
} else {
reqBody = map[string]interface{}{}
_ = r.ParseForm()
for k, v := range r.Form {
if len(v) == 1 {
if len(v[0]) == 0 {
continue
}
reqBody[k] = v[0]
} else {
reqBody[k] = v
}
}
}
// For now, just echo the request body as JSON
w.Header().Set("Content-Type", "application/json")
b, _ := json.MarshalIndent(reqBody, "", " ")
w.Write(b)
return
}
}
}
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Not found"))
}))
// Auth routes - only registered when auth is enabled
if authEnabled {
authMw := authRequired(storeInst)
// loadEndpointScopes returns all stored endpoint scopes from the store
loadEndpointScopes := func() map[string][]string {
recs, _ := storeInst.Read("endpoint-scopes/", store.ReadPrefix())
result := map[string][]string{}
for _, rec := range recs {
name := strings.TrimPrefix(rec.Key, "endpoint-scopes/")
var scopes []string
if err := json.Unmarshal(rec.Value, &scopes); err == nil && len(scopes) > 0 {
result[name] = scopes
}
}
return result
}
// Scopes management — per-endpoint scope requirements
mux.HandleFunc("/auth/scopes", authMw(func(w http.ResponseWriter, r *http.Request) {
userID := getUser(r)
var user any
if userID != "" {
user = &TemplateUser{ID: userID}
}
success := false
if r.Method == http.MethodPost {
endpoint := r.FormValue("endpoint")
scopesStr := r.FormValue("scopes")
if endpoint != "" {
if scopesStr == "" {
_ = storeInst.Delete("endpoint-scopes/" + endpoint)
} else {
scopes := strings.Split(scopesStr, ",")
for i := range scopes {
scopes[i] = strings.TrimSpace(scopes[i])
}
b, _ := json.Marshal(scopes)
_ = storeInst.Write(&store.Record{Key: "endpoint-scopes/" + endpoint, Value: b})
}
success = true
}
}
// Discover endpoints
services, _ := registry.ListServices()
storedScopes := loadEndpointScopes()
type endpointEntry struct {
Name string
Service string
Endpoint string
Scopes []string
ScopesStr string
}
var endpoints []endpointEntry
for _, svc := range services {
fullSvcs, err := registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
key := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
scopes := storedScopes[key]
scopesStr := strings.Join(scopes, ", ")
endpoints = append(endpoints, endpointEntry{
Name: key,
Service: svc.Name,
Endpoint: ep.Name,
Scopes: scopes,
ScopesStr: scopesStr,
})
}
}
_ = renderPage(w, tmpls.scopes, map[string]any{
"Title": "Scopes",
"Endpoints": endpoints,
"User": user,
"Success": success,
})
}))
// Bulk set scopes for endpoints matching a pattern
mux.HandleFunc("/auth/scopes/bulk", authMw(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/auth/scopes", http.StatusSeeOther)
return
}
pattern := r.FormValue("pattern")
scopesStr := r.FormValue("scopes")
if pattern == "" {
http.Redirect(w, r, "/auth/scopes", http.StatusSeeOther)
return
}
scopes := []string{}
if scopesStr != "" {
scopes = strings.Split(scopesStr, ",")
for i := range scopes {
scopes[i] = strings.TrimSpace(scopes[i])
}
}
// Find matching endpoints
services, _ := registry.ListServices()
for _, svc := range services {
fullSvcs, err := registry.GetService(svc.Name)
if err != nil || len(fullSvcs) == 0 {
continue
}
for _, ep := range fullSvcs[0].Endpoints {
key := fmt.Sprintf("%s.%s", svc.Name, ep.Name)
matched := false
if strings.HasSuffix(pattern, "*") {
prefix := strings.TrimSuffix(pattern, "*")
matched = strings.HasPrefix(key, prefix)
} else {
matched = key == pattern
}
if matched {
if len(scopes) == 0 {
_ = storeInst.Delete("endpoint-scopes/" + key)
} else {
b, _ := json.Marshal(scopes)
_ = storeInst.Write(&store.Record{Key: "endpoint-scopes/" + key, Value: b})
}
}
}
}
http.Redirect(w, r, "/auth/scopes", http.StatusSeeOther)
}))
mux.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "micro_token", Value: "", Path: "/", Expires: time.Now().Add(-1 * time.Hour), HttpOnly: true})
http.Redirect(w, r, "/auth/login", http.StatusSeeOther)
})
mux.HandleFunc("/auth/tokens", authMw(func(w http.ResponseWriter, r *http.Request) {
userID := getUser(r)
var user any
if userID != "" {
user = &TemplateUser{ID: userID}
} else {
user = nil
}
if r.Method == http.MethodPost {
id := r.FormValue("id")
typeStr := r.FormValue("type")
scopesStr := r.FormValue("scopes")
accType := "user"
switch typeStr {
case "admin":
accType = "admin"
case "service":
accType = "service"
}
scopes := []string{"*"}
if scopesStr != "" {
scopes = strings.Split(scopesStr, ",")
for i := range scopes {
scopes[i] = strings.TrimSpace(scopes[i])
}
}
acc := &Account{
ID: id,
Type: accType,
Scopes: scopes,
Metadata: map[string]string{"created": time.Now().Format(time.RFC3339)},
}
// Service tokens do not require a password, generate a JWT directly
tok, _ := GenerateJWT(acc.ID, acc.Type, acc.Scopes, 24*time.Hour)
acc.Metadata["token"] = tok
b, _ := json.Marshal(acc)
_ = storeInst.Write(&store.Record{Key: "auth/" + id, Value: b})
storeJWTToken(storeInst, tok, acc.ID) // Store the JWT token
http.Redirect(w, r, "/auth/tokens", http.StatusSeeOther)
return
}
recs, _ := storeInst.Read("auth/", store.ReadPrefix())
var tokens []map[string]any
for _, rec := range recs {
var acc Account
if err := json.Unmarshal(rec.Value, &acc); err == nil {
tok := ""
if t, ok := acc.Metadata["token"]; ok {
tok = t
}
var tokenPrefix, tokenSuffix string
if len(tok) > 12 {
tokenPrefix = tok[:4]
tokenSuffix = tok[len(tok)-4:]
} else {
tokenPrefix = tok
tokenSuffix = ""
}
tokens = append(tokens, map[string]any{
"ID": acc.ID,
"Type": acc.Type,
"Scopes": acc.Scopes,
"Metadata": acc.Metadata,
"Token": tok,
"TokenPrefix": tokenPrefix,
"TokenSuffix": tokenSuffix,
})
}
}
_ = renderPage(w, tmpls.authTokens, map[string]any{"Title": "Tokens", "Tokens": tokens, "User": user, "Sub": userID})
}))
mux.HandleFunc("/auth/users", authMw(func(w http.ResponseWriter, r *http.Request) {
userID := getUser(r)
var user any
if userID != "" {
user = &TemplateUser{ID: userID}
} else {
user = nil
}
if r.Method == http.MethodPost {
if del := r.FormValue("delete"); del != "" {
// Delete user
_ = storeInst.Delete("auth/" + del)
deleteUserTokens(storeInst, del)
// Mark default admin as deleted so it won't be recreated on restart
if del == "admin" {
_ = storeInst.Write(&store.Record{Key: "auth/.admin-deleted", Value: []byte("true")})
}
http.Redirect(w, r, "/auth/users", http.StatusSeeOther)
return
}
id := r.FormValue("id")
if id == "" {
http.Redirect(w, r, "/auth/users", http.StatusSeeOther)
return
}
pass := r.FormValue("password")
typeStr := r.FormValue("type")
accType := "user"
if typeStr == "admin" {
accType = "admin"
}
hash, _ := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
acc := &Account{
ID: id,
Type: accType,
Scopes: []string{"*"},
Metadata: map[string]string{"created": time.Now().Format(time.RFC3339), "password_hash": string(hash)},
}
b, _ := json.Marshal(acc)
_ = storeInst.Write(&store.Record{Key: "auth/" + id, Value: b})
http.Redirect(w, r, "/auth/users", http.StatusSeeOther)
return
}
recs, _ := storeInst.Read("auth/", store.ReadPrefix())
var users []Account
for _, rec := range recs {
var acc Account
if err := json.Unmarshal(rec.Value, &acc); err == nil {
if acc.Type == "user" || acc.Type == "admin" {
users = append(users, acc)
}
}
}
_ = renderPage(w, tmpls.authUsers, map[string]any{"Title": "Users", "Users": users, "User": user})
}))
mux.HandleFunc("/auth/login", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
loginTmpl, err := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Template error: " + err.Error()))
return
}
_ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "", "User": getUser(r), "HideSidebar": true})
return
}
if r.Method == http.MethodPost {
id := r.FormValue("id")
pass := r.FormValue("password")
recKey := "auth/" + id
recs, _ := storeInst.Read(recKey)
if len(recs) == 0 {
loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")
_ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true})
return
}
var acc Account
if err := json.Unmarshal(recs[0].Value, &acc); err != nil {
loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")
_ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true})
return
}
hash, ok := acc.Metadata["password_hash"]
if !ok || bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) != nil {
loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")
_ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Invalid credentials", "User": "", "HideSidebar": true})
return
}
tok, err := GenerateJWT(acc.ID, acc.Type, acc.Scopes, 24*time.Hour)
if err != nil {
log.Printf("[LOGIN ERROR] Token generation failed: %v\nAccount: %+v", err, acc)
loginTmpl, _ := template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")
_ = loginTmpl.Execute(w, map[string]any{"Title": "Login", "Error": "Token error", "User": "", "HideSidebar": true})
return
}
storeJWTToken(storeInst, tok, acc.ID) // Store the JWT token
http.SetCookie(w, &http.Cookie{
Name: "micro_token",
Value: tok,
Path: "/",
Expires: time.Now().Add(time.Hour * 24),
HttpOnly: true,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("Method not allowed"))
})
} // end if authEnabled
}
func Run(c *cli.Context) error {
addr := c.String("address")
if addr == "" {
addr = ":8080"
}
mcpAddr := c.String("mcp-address")
// Run the gateway with authentication enabled
opts := GatewayOptions{
Address: addr,
AuthEnabled: true,
Context: c.Context,
MCPEnabled: mcpAddr != "",
MCPAddress: mcpAddr,
}
return RunGateway(opts)
}
// mapGoTypeToJSON maps Go types to JSON schema types
func mapGoTypeToJSON(goType string) string {
switch goType {
case "string":
return "string"
case "int", "int32", "int64", "uint", "uint32", "uint64":
return "integer"
case "float32", "float64":
return "number"
case "bool":
return "boolean"
default:
return "object"
}
}
// --- PID FILES ---
func parsePid(pidStr string) int {
pid, _ := strconv.Atoi(pidStr)
return pid
}
func processRunning(pid string) bool {
proc, err := os.FindProcess(parsePid(pid))
if err != nil {
return false
}
// On unix, sending syscall.Signal(0) checks if process exists
return proc.Signal(syscall.Signal(0)) == nil
}
func initAuth() error {
// --- AUTH SETUP ---
homeDir, _ := os.UserHomeDir()
keyDir := filepath.Join(homeDir, "micro", "keys")
privPath := filepath.Join(keyDir, "private.pem")
pubPath := filepath.Join(keyDir, "public.pem")
_ = os.MkdirAll(keyDir, 0700)
// Generate keypair if not exist
if _, err := os.Stat(privPath); os.IsNotExist(err) {
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
privBytes := x509.MarshalPKCS1PrivateKey(priv)
privPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes})
_ = os.WriteFile(privPath, privPem, 0600)
// Use PKIX format for public key
pubBytes, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes})
_ = os.WriteFile(pubPath, pubPem, 0644)
}
_, _ = os.ReadFile(privPath)
_, _ = os.ReadFile(pubPath)
storeInst := store.DefaultStore
// --- Ensure default admin account exists on first run ---
// If the admin was explicitly deleted (marker key exists), don't recreate.
adminID := "admin"
adminPass := "micro"
adminKey := "auth/" + adminID
adminDeletedKey := "auth/.admin-deleted"
if recs, _ := storeInst.Read(adminDeletedKey); len(recs) > 0 {
// Admin was explicitly deleted — don't recreate
} else if recs, _ := storeInst.Read(adminKey); len(recs) == 0 {
// Hash the admin password with bcrypt
hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost)
if err != nil {
return err
}
acc := &Account{
ID: adminID,
Type: "admin",
Scopes: []string{"*"},
Metadata: map[string]string{"created": time.Now().Format(time.RFC3339), "password_hash": string(hash)},
}
b, _ := json.Marshal(acc)
_ = storeInst.Write(&store.Record{Key: adminKey, Value: b})
}
return nil
}
// parseStartTime parses a string as RFC3339 time
func parseStartTime(s string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
func init() {
cmd.Register(&cli.Command{
Name: "server",
Usage: "Production mode: run the micro server with dashboard and auth",
Action: Run,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Usage: "Address to listen on",
EnvVars: []string{"MICRO_SERVER_ADDRESS"},
Value: ":8080",
},
&cli.StringFlag{
Name: "mcp-address",
Usage: "MCP gateway address (e.g., :3000). Enables MCP protocol support for AI tools.",
EnvVars: []string{"MICRO_MCP_ADDRESS"},
},
},
})
}