From e1cb5c411b096f77945aafaf5ca9ee4f936f4c54 Mon Sep 17 00:00:00 2001 From: viettranx Date: Mon, 23 Mar 2026 22:57:26 +0700 Subject: [PATCH] refactor(http): move gateway token to package-level auth state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace per-handler `token string` field with package-level `pkgGatewayToken` via `InitGatewayToken()`, matching the existing pattern for `pkgAPIKeyCache`, `pkgOwnerIDs`, etc. - Remove `token` from ~30 HTTP handler structs and constructors - Simplify `requireAuth(minRole, next)` signature (was 3 params) - Simplify `resolveAuth(r)` signature (was 2 params) - Rename `resolveAuthBearer` → `resolveAuthWithBearer` (clearer) - Add `store.WithRole()`/`RoleFromContext()` to propagate caller role through context - Inject role into context in `requireAuth` and `requireAuthBearer` --- cmd/gateway.go | 33 +++++++++++++++--------------- cmd/gateway_http_handlers.go | 24 +++++++++++----------- internal/gateway/server.go | 12 +++++------ internal/http/activity.go | 9 ++++---- internal/http/agents.go | 7 +++---- internal/http/api_keys.go | 7 +++---- internal/http/auth.go | 31 ++++++++++++++++++---------- internal/http/auth_test.go | 29 ++++++++++++++++++-------- internal/http/builtin_tools.go | 7 +++---- internal/http/channel_instances.go | 7 +++---- internal/http/chat_completions.go | 6 ++---- internal/http/files.go | 7 +++---- internal/http/knowledge_graph.go | 7 +++---- internal/http/mcp.go | 11 +++++----- internal/http/media_serve.go | 7 +++---- internal/http/media_upload.go | 10 ++++----- internal/http/memory.go | 7 +++---- internal/http/oauth.go | 6 ++---- internal/http/oauth_test.go | 18 +++++++++------- internal/http/openapi.go | 8 +++----- internal/http/packages.go | 12 +++++------ internal/http/packages_test.go | 7 ++----- internal/http/pending_messages.go | 7 +++---- internal/http/providers.go | 9 ++++---- internal/http/responses.go | 6 ++---- internal/http/secure_cli.go | 7 +++---- internal/http/skills.go | 13 ++++++------ internal/http/skills_grants.go | 8 ++++---- internal/http/storage.go | 7 +++---- internal/http/team_attachments.go | 7 +++---- internal/http/team_events.go | 17 +++------------ internal/http/tenants.go | 7 +++---- internal/http/tools_invoke.go | 6 ++---- internal/http/traces.go | 13 ++++++------ internal/http/usage.go | 7 +++---- internal/http/wake.go | 7 +++---- internal/store/context.go | 15 ++++++++++++++ 37 files changed, 201 insertions(+), 207 deletions(-) diff --git a/cmd/gateway.go b/cmd/gateway.go index 173daee7..9d523ea9 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -289,7 +289,7 @@ func runGateway() { server.SetPolicyEngine(permPE) server.SetPairingService(pgStores.Pairing) server.SetMessageBus(msgBus) - server.SetOAuthHandler(httpapi.NewOAuthHandler(cfg.Gateway.Token, pgStores.Providers, pgStores.ConfigSecrets, providerRegistry, msgBus)) + server.SetOAuthHandler(httpapi.NewOAuthHandler(pgStores.Providers, pgStores.ConfigSecrets, providerRegistry, msgBus)) // contextFileInterceptor is created inside wireExtras. // Declared here so it can be passed to registerAllMethods → AgentsMethods @@ -313,7 +313,8 @@ func runGateway() { if mcpMgr != nil { mcpToolLister = mcpMgr } - agentsH, skillsH, tracesH, mcpH, channelInstancesH, providersH, builtinToolsH, pendingMessagesH, teamEventsH, secureCLIH, mcpUserCredsH := wireHTTP(pgStores, cfg.Gateway.Token, cfg.Agents.Defaults.Workspace, dataDir, bundledSkillsDir, msgBus, toolsReg, providerRegistry, permPE.IsOwner, gatewayAddr, mcpToolLister) + httpapi.InitGatewayToken(cfg.Gateway.Token) + agentsH, skillsH, tracesH, mcpH, channelInstancesH, providersH, builtinToolsH, pendingMessagesH, teamEventsH, secureCLIH, mcpUserCredsH := wireHTTP(pgStores, cfg.Agents.Defaults.Workspace, dataDir, bundledSkillsDir, msgBus, toolsReg, providerRegistry, permPE.IsOwner, gatewayAddr, mcpToolLister) if providersH != nil { providersH.SetAPIBaseFallback(cfg.Providers.APIBaseForType) } @@ -327,7 +328,7 @@ func runGateway() { server.SetTracesHandler(tracesH) } // External wake/trigger API - wakeH := httpapi.NewWakeHandler(agentRouter, cfg.Gateway.Token) + wakeH := httpapi.NewWakeHandler(agentRouter) if postTurn != nil { wakeH.SetPostTurnProcessor(postTurn) } @@ -351,7 +352,7 @@ func runGateway() { server.SetTeamEventsHandler(teamEventsH) } if pgStores != nil && pgStores.Teams != nil { - server.SetTeamAttachmentsHandler(httpapi.NewTeamAttachmentsHandler(pgStores.Teams, cfg.Gateway.Token, workspace)) + server.SetTeamAttachmentsHandler(httpapi.NewTeamAttachmentsHandler(pgStores.Teams, workspace)) } if builtinToolsH != nil { server.SetBuiltinToolsHandler(builtinToolsH) @@ -371,23 +372,23 @@ func runGateway() { // Activity audit log API if pgStores.Activity != nil { - server.SetActivityHandler(httpapi.NewActivityHandler(pgStores.Activity, cfg.Gateway.Token)) + server.SetActivityHandler(httpapi.NewActivityHandler(pgStores.Activity)) } // Usage analytics API if pgStores.Snapshots != nil { - server.SetUsageHandler(httpapi.NewUsageHandler(pgStores.Snapshots, pgStores.DB, cfg.Gateway.Token)) + server.SetUsageHandler(httpapi.NewUsageHandler(pgStores.Snapshots, pgStores.DB)) } // Runtime package management (install/uninstall system/pip/npm packages) - server.SetPackagesHandler(httpapi.NewPackagesHandler(cfg.Gateway.Token)) + server.SetPackagesHandler(httpapi.NewPackagesHandler()) // API key management // API documentation (OpenAPI spec + Swagger UI at /docs) - server.SetDocsHandler(httpapi.NewDocsHandler(cfg.Gateway.Token)) + server.SetDocsHandler(httpapi.NewDocsHandler()) if pgStores != nil && pgStores.APIKeys != nil { - server.SetAPIKeysHandler(httpapi.NewAPIKeysHandler(pgStores.APIKeys, cfg.Gateway.Token, msgBus)) + server.SetAPIKeysHandler(httpapi.NewAPIKeysHandler(pgStores.APIKeys, msgBus)) server.SetAPIKeyStore(pgStores.APIKeys) httpapi.InitAPIKeyCache(pgStores.APIKeys, msgBus) } @@ -399,29 +400,29 @@ func runGateway() { // Memory management API (wired directly, only needs MemoryStore + token) if pgStores != nil && pgStores.Memory != nil { - server.SetMemoryHandler(httpapi.NewMemoryHandler(pgStores.Memory, cfg.Gateway.Token)) + server.SetMemoryHandler(httpapi.NewMemoryHandler(pgStores.Memory)) } // Knowledge graph API if pgStores != nil && pgStores.KnowledgeGraph != nil { - server.SetKnowledgeGraphHandler(httpapi.NewKnowledgeGraphHandler(pgStores.KnowledgeGraph, providerRegistry, cfg.Gateway.Token)) + server.SetKnowledgeGraphHandler(httpapi.NewKnowledgeGraphHandler(pgStores.KnowledgeGraph, providerRegistry)) } // Workspace file serving endpoint — serves files by absolute path, auth-token protected. // Supports media from any agent workspace (each agent has its own workspace from DB). - server.SetFilesHandler(httpapi.NewFilesHandler(cfg.Gateway.Token, workspace, dataDir)) + server.SetFilesHandler(httpapi.NewFilesHandler(workspace, dataDir)) // Storage file management — browse/delete files under the resolved workspace directory. // Uses GOCLAW_WORKSPACE (or default ~/.goclaw/workspace) so it works correctly // in Docker deployments where volumes are mounted outside ~/.goclaw/. - server.SetStorageHandler(httpapi.NewStorageHandler(workspace, cfg.Gateway.Token)) + server.SetStorageHandler(httpapi.NewStorageHandler(workspace)) // Media upload endpoint — accepts multipart file uploads, returns temp path + MIME type. - server.SetMediaUploadHandler(httpapi.NewMediaUploadHandler(cfg.Gateway.Token)) + server.SetMediaUploadHandler(httpapi.NewMediaUploadHandler()) // Media serve endpoint — serves persisted media files by ID for WS/web clients. if mediaStore != nil { - server.SetMediaServeHandler(httpapi.NewMediaServeHandler(mediaStore, cfg.Gateway.Token)) + server.SetMediaServeHandler(httpapi.NewMediaServeHandler(mediaStore)) } // Seed + apply builtin tool disables @@ -907,7 +908,7 @@ func runGateway() { // Tenant management RPC + HTTP if pgStores.Tenants != nil { methods.NewTenantsMethods(pgStores.Tenants, msgBus, workspace).Register(server.Router()) - server.SetTenantsHandler(httpapi.NewTenantsHandler(pgStores.Tenants, cfg.Gateway.Token, msgBus, workspace)) + server.SetTenantsHandler(httpapi.NewTenantsHandler(pgStores.Tenants, msgBus, workspace)) server.Router().SetTenantStore(pgStores.Tenants) // Permission cache for tenant membership checks (tenant role, agent access, etc.) permCache := cache.NewPermissionCache() diff --git a/cmd/gateway_http_handlers.go b/cmd/gateway_http_handlers.go index 483df227..2e04c859 100644 --- a/cmd/gateway_http_handlers.go +++ b/cmd/gateway_http_handlers.go @@ -10,7 +10,7 @@ import ( ) // wireHTTP creates HTTP handlers (agents + skills + traces + MCP + channel instances + providers + builtin tools + pending messages). -func wireHTTP(stores *store.Stores, token, defaultWorkspace, dataDir, bundledSkillsDir string, msgBus *bus.MessageBus, toolsReg *tools.Registry, providerReg *providers.Registry, isOwner func(string) bool, gatewayAddr string, mcpToolLister httpapi.MCPToolLister) (*httpapi.AgentsHandler, *httpapi.SkillsHandler, *httpapi.TracesHandler, *httpapi.MCPHandler, *httpapi.ChannelInstancesHandler, *httpapi.ProvidersHandler, *httpapi.BuiltinToolsHandler, *httpapi.PendingMessagesHandler, *httpapi.TeamEventsHandler, *httpapi.SecureCLIHandler, *httpapi.MCPUserCredentialsHandler) { +func wireHTTP(stores *store.Stores, defaultWorkspace, dataDir, bundledSkillsDir string, msgBus *bus.MessageBus, toolsReg *tools.Registry, providerReg *providers.Registry, isOwner func(string) bool, gatewayAddr string, mcpToolLister httpapi.MCPToolLister) (*httpapi.AgentsHandler, *httpapi.SkillsHandler, *httpapi.TracesHandler, *httpapi.MCPHandler, *httpapi.ChannelInstancesHandler, *httpapi.ProvidersHandler, *httpapi.BuiltinToolsHandler, *httpapi.PendingMessagesHandler, *httpapi.TeamEventsHandler, *httpapi.SecureCLIHandler, *httpapi.MCPUserCredentialsHandler) { var agentsH *httpapi.AgentsHandler var skillsH *httpapi.SkillsHandler var tracesH *httpapi.TracesHandler @@ -26,36 +26,36 @@ func wireHTTP(stores *store.Stores, token, defaultWorkspace, dataDir, bundledSki if providerReg != nil { summoner = httpapi.NewAgentSummoner(stores.Agents, providerReg, msgBus) } - agentsH = httpapi.NewAgentsHandler(stores.Agents, token, defaultWorkspace, msgBus, summoner, isOwner) + agentsH = httpapi.NewAgentsHandler(stores.Agents, defaultWorkspace, msgBus, summoner, isOwner) } if stores != nil && stores.Skills != nil { if pgSkills, ok := stores.Skills.(*pg.PGSkillStore); ok { dirs := pgSkills.Dirs() if len(dirs) > 0 { - skillsH = httpapi.NewSkillsHandler(pgSkills, dirs[0], dataDir, bundledSkillsDir, token, msgBus) + skillsH = httpapi.NewSkillsHandler(pgSkills, dirs[0], dataDir, bundledSkillsDir, msgBus) } } } if stores != nil && stores.Tracing != nil { - tracesH = httpapi.NewTracesHandler(stores.Tracing, token) + tracesH = httpapi.NewTracesHandler(stores.Tracing) } if stores != nil && stores.MCP != nil { - mcpH = httpapi.NewMCPHandler(stores.MCP, token, msgBus, mcpToolLister) + mcpH = httpapi.NewMCPHandler(stores.MCP, msgBus, mcpToolLister) } var mcpUserCredsH *httpapi.MCPUserCredentialsHandler if stores != nil && stores.MCP != nil { - mcpUserCredsH = httpapi.NewMCPUserCredentialsHandler(stores.MCP, token) + mcpUserCredsH = httpapi.NewMCPUserCredentialsHandler(stores.MCP, stores.Tenants) } if stores != nil && stores.ChannelInstances != nil { - channelInstancesH = httpapi.NewChannelInstancesHandler(stores.ChannelInstances, stores.Agents, stores.ConfigPermissions, stores.Contacts, stores.Tenants, token, msgBus) + channelInstancesH = httpapi.NewChannelInstancesHandler(stores.ChannelInstances, stores.Agents, stores.ConfigPermissions, stores.Contacts, stores.Tenants, msgBus) } if stores != nil && stores.Providers != nil { - providersH = httpapi.NewProvidersHandler(stores.Providers, stores.ConfigSecrets, token, providerReg, gatewayAddr) + providersH = httpapi.NewProvidersHandler(stores.Providers, stores.ConfigSecrets, providerReg, gatewayAddr) providersH.SetMessageBus(msgBus) if stores.MCP != nil { providersH.SetMCPServerLookup(buildMCPServerLookup(stores.MCP)) @@ -65,19 +65,19 @@ func wireHTTP(stores *store.Stores, token, defaultWorkspace, dataDir, bundledSki var teamEventsH *httpapi.TeamEventsHandler if stores != nil && stores.Teams != nil { - teamEventsH = httpapi.NewTeamEventsHandler(stores.Teams, token) + teamEventsH = httpapi.NewTeamEventsHandler(stores.Teams) } if stores != nil && stores.BuiltinTools != nil { - builtinToolsH = httpapi.NewBuiltinToolsHandler(stores.BuiltinTools, stores.BuiltinToolTenantCfgs, token, msgBus) + builtinToolsH = httpapi.NewBuiltinToolsHandler(stores.BuiltinTools, stores.BuiltinToolTenantCfgs, msgBus) } if stores != nil && stores.PendingMessages != nil { - pendingMessagesH = httpapi.NewPendingMessagesHandler(stores.PendingMessages, stores.Agents, token, providerReg) + pendingMessagesH = httpapi.NewPendingMessagesHandler(stores.PendingMessages, stores.Agents, providerReg) } if stores != nil && stores.SecureCLI != nil { - secureCLIH = httpapi.NewSecureCLIHandler(stores.SecureCLI, token, msgBus) + secureCLIH = httpapi.NewSecureCLIHandler(stores.SecureCLI, msgBus) } return agentsH, skillsH, tracesH, mcpH, channelInstancesH, providersH, builtinToolsH, pendingMessagesH, teamEventsH, secureCLIH, mcpUserCredsH diff --git a/internal/gateway/server.go b/internal/gateway/server.go index 799fa79c..1e422223 100644 --- a/internal/gateway/server.go +++ b/internal/gateway/server.go @@ -162,7 +162,7 @@ func (s *Server) BuildMux() *http.ServeMux { // OpenAI-compatible chat completions isManaged := s.agentStore != nil - chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged) + chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, isManaged) if s.rateLimiter.Enabled() { chatHandler.SetRateLimiter(s.rateLimiter.Allow) } @@ -172,7 +172,7 @@ func (s *Server) BuildMux() *http.ServeMux { mux.Handle("/v1/chat/completions", chatHandler) // OpenResponses protocol - responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token) + responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions) if s.postTurn != nil { responsesHandler.SetPostTurnProcessor(s.postTurn) } @@ -180,7 +180,7 @@ func (s *Server) BuildMux() *http.ServeMux { // Direct tool invocation if s.tools != nil { - toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore) + toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.agentStore) mux.Handle("/v1/tools/invoke", toolsHandler) } @@ -663,7 +663,7 @@ func StartTestServer(s *Server, ctx context.Context) (addr string, start func()) mux.HandleFunc("/health", s.handleHealth) isManaged := s.agentStore != nil - chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged) + chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, isManaged) if s.rateLimiter.Enabled() { chatHandler.SetRateLimiter(s.rateLimiter.Allow) } @@ -672,14 +672,14 @@ func StartTestServer(s *Server, ctx context.Context) (addr string, start func()) } mux.Handle("/v1/chat/completions", chatHandler) - responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token) + responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions) if s.postTurn != nil { responsesHandler.SetPostTurnProcessor(s.postTurn) } mux.Handle("/v1/responses", responsesHandler) if s.tools != nil { - toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore) + toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.agentStore) mux.Handle("/v1/tools/invoke", toolsHandler) } diff --git a/internal/http/activity.go b/internal/http/activity.go index 1853669f..c21f009e 100644 --- a/internal/http/activity.go +++ b/internal/http/activity.go @@ -11,12 +11,11 @@ import ( // ActivityHandler handles activity audit log endpoints. type ActivityHandler struct { activity store.ActivityStore - token string } // NewActivityHandler creates a handler for activity log endpoints. -func NewActivityHandler(activity store.ActivityStore, token string) *ActivityHandler { - return &ActivityHandler{activity: activity, token: token} +func NewActivityHandler(activity store.ActivityStore) *ActivityHandler { + return &ActivityHandler{activity: activity} } // RegisterRoutes registers activity routes on the given mux. @@ -25,7 +24,7 @@ func (h *ActivityHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *ActivityHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *ActivityHandler) handleList(w http.ResponseWriter, r *http.Request) { @@ -42,7 +41,7 @@ func (h *ActivityHandler) handleList(w http.ResponseWriter, r *http.Request) { } // Non-admin callers may only see their own activity logs. - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { callerID := store.UserIDFromContext(r.Context()) opts.ActorID = callerID diff --git a/internal/http/agents.go b/internal/http/agents.go index 3c767972..66231fdf 100644 --- a/internal/http/agents.go +++ b/internal/http/agents.go @@ -20,7 +20,6 @@ import ( // AgentsHandler handles agent CRUD and sharing endpoints. type AgentsHandler struct { agents store.AgentStore - token string defaultWorkspace string // default workspace path template (e.g. "~/.goclaw/workspace") msgBus *bus.MessageBus // for cache invalidation events (nil = no events) summoner *AgentSummoner // LLM-based agent setup (nil = disabled) @@ -29,8 +28,8 @@ type AgentsHandler struct { // NewAgentsHandler creates a handler for agent management endpoints. // isOwner is a function that checks if a user ID is in GOCLAW_OWNER_IDS (nil = disabled). -func NewAgentsHandler(agents store.AgentStore, token, defaultWorkspace string, msgBus *bus.MessageBus, summoner *AgentSummoner, isOwner func(string) bool) *AgentsHandler { - return &AgentsHandler{agents: agents, token: token, defaultWorkspace: defaultWorkspace, msgBus: msgBus, summoner: summoner, isOwner: isOwner} +func NewAgentsHandler(agents store.AgentStore, defaultWorkspace string, msgBus *bus.MessageBus, summoner *AgentSummoner, isOwner func(string) bool) *AgentsHandler { + return &AgentsHandler{agents: agents, defaultWorkspace: defaultWorkspace, msgBus: msgBus, summoner: summoner, isOwner: isOwner} } // isOwnerUser checks if the given user ID is a system owner. @@ -68,7 +67,7 @@ func (h *AgentsHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *AgentsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *AgentsHandler) handleList(w http.ResponseWriter, r *http.Request) { diff --git a/internal/http/api_keys.go b/internal/http/api_keys.go index dbbed9ff..bf4adea1 100644 --- a/internal/http/api_keys.go +++ b/internal/http/api_keys.go @@ -19,13 +19,12 @@ import ( // APIKeysHandler handles API key management endpoints. type APIKeysHandler struct { apiKeys store.APIKeyStore - token string // gateway token for admin auth msgBus *bus.MessageBus // for cache invalidation events } // NewAPIKeysHandler creates a handler for API key management endpoints. -func NewAPIKeysHandler(apiKeys store.APIKeyStore, token string, msgBus *bus.MessageBus) *APIKeysHandler { - return &APIKeysHandler{apiKeys: apiKeys, token: token, msgBus: msgBus} +func NewAPIKeysHandler(apiKeys store.APIKeyStore, msgBus *bus.MessageBus) *APIKeysHandler { + return &APIKeysHandler{apiKeys: apiKeys, msgBus: msgBus} } // RegisterRoutes registers all API key management routes on the given mux. @@ -37,7 +36,7 @@ func (h *APIKeysHandler) RegisterRoutes(mux *http.ServeMux) { // adminAuth ensures the caller has admin access (gateway token or API key with admin scope). func (h *APIKeysHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, permissions.RoleAdmin, next) + return requireAuth(permissions.RoleAdmin, next) } func (h *APIKeysHandler) handleList(w http.ResponseWriter, r *http.Request) { diff --git a/internal/http/auth.go b/internal/http/auth.go index d8a27f20..c3d284e8 100644 --- a/internal/http/auth.go +++ b/internal/http/auth.go @@ -76,11 +76,18 @@ func extractAgentID(r *http.Request, model string) string { // --- Package-level API key cache for shared auth --- +var pkgGatewayToken string var pkgAPIKeyCache *apiKeyCache var pkgPairingStore store.PairingStore var pkgTenantCache *tenantCache var pkgOwnerIDs []string +// InitGatewayToken sets the gateway bearer token for HTTP auth. +// Must be called once during server startup before handling requests. +func InitGatewayToken(token string) { + pkgGatewayToken = token +} + // InitAPIKeyCache initializes the shared API key cache with TTL and pubsub invalidation. // Must be called once during server startup before handling requests. func InitAPIKeyCache(s store.APIKeyStore, mb *bus.MessageBus) { @@ -157,17 +164,17 @@ type authResult struct { // resolveAuth determines the caller's role from the request. // Priority: gateway token → API key → no-auth fallback. -func resolveAuth(r *http.Request, gatewayToken string) authResult { - return resolveAuthBearer(r, gatewayToken, extractBearerToken(r)) +func resolveAuth(r *http.Request) authResult { + return resolveAuthWithBearer(r, extractBearerToken(r)) } -// resolveAuthBearer is like resolveAuth but accepts a pre-extracted bearer token. +// resolveAuthWithBearer is like resolveAuth but accepts a pre-extracted bearer token. // Useful for handlers that also accept tokens from query params. -func resolveAuthBearer(r *http.Request, gatewayToken, bearer string) authResult { +func resolveAuthWithBearer(r *http.Request, bearer string) authResult { // Gateway token → admin // Only owner IDs get cross-tenant access; others are tenant-scoped. // If no owner IDs configured, all gateway token users get cross-tenant (backward compat). - if gatewayToken != "" && tokenMatch(bearer, gatewayToken) { + if pkgGatewayToken != "" && tokenMatch(bearer, pkgGatewayToken) { userID := extractUserID(r) isOwner := isHTTPOwnerID(userID, pkgOwnerIDs) res := authResult{Role: permissions.RoleAdmin, Authenticated: true, CrossTenant: isOwner} @@ -221,7 +228,7 @@ func resolveAuthBearer(r *http.Request, gatewayToken, bearer string) authResult } } // No auth configured → admin (no token = dev/single-user mode, full access) - if gatewayToken == "" { + if pkgGatewayToken == "" { return authResult{Role: permissions.RoleAdmin, Authenticated: true, TenantID: store.MasterTenantID} } return authResult{} @@ -239,11 +246,11 @@ func httpMinRole(method string) permissions.Role { // requireAuth is a middleware that checks authentication and minimum role. // Pass "" for minRole to auto-detect from HTTP method (GET→Viewer, POST→Operator). -// Injects locale and userID into request context. -func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc) http.HandlerFunc { +// Injects locale, role, userID and tenantID into request context. +func requireAuth(minRole permissions.Role, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { locale := extractLocale(r) - auth := resolveAuth(r, token) + auth := resolveAuth(r) if !auth.Authenticated { writeJSON(w, http.StatusUnauthorized, map[string]string{ @@ -265,6 +272,7 @@ func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc) } ctx := store.WithLocale(r.Context(), locale) + ctx = store.WithRole(ctx, string(auth.Role)) userID := extractUserID(r) // If the API key has a bound owner, force user_id to owner regardless of header. if auth.KeyData != nil && auth.KeyData.OwnerID != "" { @@ -318,9 +326,9 @@ func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc) // requireAuthBearer is like requireAuth but accepts a pre-extracted bearer token. // Used by handlers that accept tokens from query params (files, media). // Returns the authenticated request with user context applied (owner_id enforcement included). -func requireAuthBearer(token string, minRole permissions.Role, bearer string, w http.ResponseWriter, r *http.Request) (*http.Request, bool) { +func requireAuthBearer(minRole permissions.Role, bearer string, w http.ResponseWriter, r *http.Request) (*http.Request, bool) { locale := extractLocale(r) - auth := resolveAuthBearer(r, token, bearer) + auth := resolveAuthWithBearer(r, bearer) if !auth.Authenticated { writeJSON(w, http.StatusUnauthorized, map[string]string{ @@ -343,6 +351,7 @@ func requireAuthBearer(token string, minRole permissions.Role, bearer string, w // Apply user context + owner_id enforcement (same as requireAuth). ctx := store.WithLocale(r.Context(), locale) + ctx = store.WithRole(ctx, string(auth.Role)) userID := extractUserID(r) if auth.KeyData != nil && auth.KeyData.OwnerID != "" { if userID != "" && userID != auth.KeyData.OwnerID { diff --git a/internal/http/auth_test.go b/internal/http/auth_test.go index 5c20a2f9..3d602d51 100644 --- a/internal/http/auth_test.go +++ b/internal/http/auth_test.go @@ -26,13 +26,22 @@ func setupTestCache(t *testing.T, keys map[string]*store.APIKeyData) *mockAPIKey return ms } +// setupTestToken sets the package-level gateway token for testing. +func setupTestToken(t *testing.T, token string) { + t.Helper() + old := pkgGatewayToken + pkgGatewayToken = token + t.Cleanup(func() { pkgGatewayToken = old }) +} + func TestResolveAuth_GatewayToken(t *testing.T) { setupTestCache(t, nil) + setupTestToken(t, "my-gateway-token") r := httptest.NewRequest("GET", "/v1/agents", nil) r.Header.Set("Authorization", "Bearer my-gateway-token") - auth := resolveAuth(r, "my-gateway-token") + auth := resolveAuth(r) if !auth.Authenticated { t.Fatal("expected authenticated") } @@ -43,11 +52,12 @@ func TestResolveAuth_GatewayToken(t *testing.T) { func TestResolveAuth_WrongToken(t *testing.T) { setupTestCache(t, nil) + setupTestToken(t, "correct-token") r := httptest.NewRequest("GET", "/v1/agents", nil) r.Header.Set("Authorization", "Bearer wrong-token") - auth := resolveAuth(r, "correct-token") + auth := resolveAuth(r) if auth.Authenticated { t.Fatal("expected unauthenticated for wrong token") } @@ -58,7 +68,7 @@ func TestResolveAuth_NoAuthConfigured(t *testing.T) { r := httptest.NewRequest("GET", "/v1/agents", nil) - auth := resolveAuth(r, "") // no gateway token configured + auth := resolveAuth(r) // no gateway token configured if !auth.Authenticated { t.Fatal("expected authenticated when no token configured") } @@ -155,8 +165,9 @@ func TestHttpMinRole(t *testing.T) { func TestRequireAuth_Unauthorized(t *testing.T) { setupTestCache(t, nil) + setupTestToken(t, "secret") - handler := requireAuth("secret", "", func(w http.ResponseWriter, r *http.Request) { + handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) @@ -171,8 +182,9 @@ func TestRequireAuth_Unauthorized(t *testing.T) { func TestRequireAuth_GatewayTokenPasses(t *testing.T) { setupTestCache(t, nil) + setupTestToken(t, "secret") - handler := requireAuth("secret", "", func(w http.ResponseWriter, r *http.Request) { + handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) @@ -188,9 +200,10 @@ func TestRequireAuth_GatewayTokenPasses(t *testing.T) { func TestRequireAuth_InjectLocaleAndUserID(t *testing.T) { setupTestCache(t, nil) + setupTestToken(t, "secret") var gotLocale, gotUserID string - handler := requireAuth("secret", "", func(w http.ResponseWriter, r *http.Request) { + handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) { gotLocale = store.LocaleFromContext(r.Context()) gotUserID = store.UserIDFromContext(r.Context()) w.WriteHeader(http.StatusOK) @@ -218,7 +231,7 @@ func TestRequireAuth_AdminRoleEnforced(t *testing.T) { // No auth configured → admin role (dev/single-user mode) → admin endpoint accessible setupTestCache(t, nil) - handler := requireAuth("", permissions.RoleAdmin, func(w http.ResponseWriter, r *http.Request) { + handler := requireAuth(permissions.RoleAdmin, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) @@ -236,7 +249,7 @@ func TestRequireAuth_AutoDetectRole_GET(t *testing.T) { // No auth configured → operator role. GET needs viewer → passes. setupTestCache(t, nil) - handler := requireAuth("", "", func(w http.ResponseWriter, r *http.Request) { + handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) diff --git a/internal/http/builtin_tools.go b/internal/http/builtin_tools.go index f560bbd3..146efb74 100644 --- a/internal/http/builtin_tools.go +++ b/internal/http/builtin_tools.go @@ -18,13 +18,12 @@ import ( type BuiltinToolsHandler struct { store store.BuiltinToolStore tenantCfgStore store.BuiltinToolTenantConfigStore - token string msgBus *bus.MessageBus } // NewBuiltinToolsHandler creates a handler for built-in tool management endpoints. -func NewBuiltinToolsHandler(s store.BuiltinToolStore, tenantCfgs store.BuiltinToolTenantConfigStore, token string, msgBus *bus.MessageBus) *BuiltinToolsHandler { - return &BuiltinToolsHandler{store: s, tenantCfgStore: tenantCfgs, token: token, msgBus: msgBus} +func NewBuiltinToolsHandler(s store.BuiltinToolStore, tenantCfgs store.BuiltinToolTenantConfigStore, msgBus *bus.MessageBus) *BuiltinToolsHandler { + return &BuiltinToolsHandler{store: s, tenantCfgStore: tenantCfgs, msgBus: msgBus} } // RegisterRoutes registers all built-in tool routes on the given mux. @@ -37,7 +36,7 @@ func (h *BuiltinToolsHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *BuiltinToolsHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *BuiltinToolsHandler) emitCacheInvalidate(key string) { diff --git a/internal/http/channel_instances.go b/internal/http/channel_instances.go index cd55660e..ef050498 100644 --- a/internal/http/channel_instances.go +++ b/internal/http/channel_instances.go @@ -22,13 +22,12 @@ type ChannelInstancesHandler struct { configPermStore store.ConfigPermissionStore contactStore store.ContactStore tenantStore store.TenantStore - token string msgBus *bus.MessageBus } // NewChannelInstancesHandler creates a handler for channel instance management endpoints. -func NewChannelInstancesHandler(s store.ChannelInstanceStore, agentStore store.AgentStore, configPermStore store.ConfigPermissionStore, contactStore store.ContactStore, tenantStore store.TenantStore, token string, msgBus *bus.MessageBus) *ChannelInstancesHandler { - return &ChannelInstancesHandler{store: s, agentStore: agentStore, configPermStore: configPermStore, contactStore: contactStore, tenantStore: tenantStore, token: token, msgBus: msgBus} +func NewChannelInstancesHandler(s store.ChannelInstanceStore, agentStore store.AgentStore, configPermStore store.ConfigPermissionStore, contactStore store.ContactStore, tenantStore store.TenantStore, msgBus *bus.MessageBus) *ChannelInstancesHandler { + return &ChannelInstancesHandler{store: s, agentStore: agentStore, configPermStore: configPermStore, contactStore: contactStore, tenantStore: tenantStore, msgBus: msgBus} } // RegisterRoutes registers all channel instance routes on the given mux. @@ -61,7 +60,7 @@ func (h *ChannelInstancesHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *ChannelInstancesHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *ChannelInstancesHandler) emitCacheInvalidate() { diff --git a/internal/http/chat_completions.go b/internal/http/chat_completions.go index 85dda076..2836b7da 100644 --- a/internal/http/chat_completions.go +++ b/internal/http/chat_completions.go @@ -21,7 +21,6 @@ import ( type ChatCompletionsHandler struct { agents *agent.Router sessions store.SessionStore - token string // expected bearer token (empty = no auth) isManaged bool rateLimiter func(string) bool // rate limit check: key → allowed (nil = no limit) postTurn tools.PostTurnProcessor @@ -33,11 +32,10 @@ func (h *ChatCompletionsHandler) SetPostTurnProcessor(pt tools.PostTurnProcessor } // NewChatCompletionsHandler creates a handler for the chat completions endpoint. -func NewChatCompletionsHandler(agents *agent.Router, sess store.SessionStore, token string, isManaged bool) *ChatCompletionsHandler { +func NewChatCompletionsHandler(agents *agent.Router, sess store.SessionStore, isManaged bool) *ChatCompletionsHandler { return &ChatCompletionsHandler{ agents: agents, sessions: sess, - token: token, isManaged: isManaged, } } @@ -91,7 +89,7 @@ func (h *ChatCompletionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques } // Auth + RBAC check (gateway token or API key, operator required for POST) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !auth.Authenticated { http.Error(w, fmt.Sprintf(`{"error":{"message":"%s","type":"invalid_request_error"}}`, i18n.T(locale, i18n.MsgInvalidAuth)), http.StatusUnauthorized) return diff --git a/internal/http/files.go b/internal/http/files.go index 49e1130c..e2764e52 100644 --- a/internal/http/files.go +++ b/internal/http/files.go @@ -18,7 +18,6 @@ import ( // When an exact path is not found, falls back to searching the workspace for // generated files by basename (goclaw_gen_* filenames are globally unique). type FilesHandler struct { - token string workspace string // workspace root for fallback file search dataDir string // data directory root for tenant path validation } @@ -26,8 +25,8 @@ type FilesHandler struct { // NewFilesHandler creates a handler that serves files by absolute path. // workspace is the root directory used for fallback generated file search. // dataDir is used for tenant path validation (files must be within tenant's dirs). -func NewFilesHandler(token, workspace, dataDir string) *FilesHandler { - return &FilesHandler{token: token, workspace: workspace, dataDir: dataDir} +func NewFilesHandler(workspace, dataDir string) *FilesHandler { + return &FilesHandler{workspace: workspace, dataDir: dataDir} } // RegisterRoutes registers the file serving route. @@ -49,7 +48,7 @@ func (h *FilesHandler) auth(next http.HandlerFunc) http.HandlerFunc { } // Priority 2: Bearer header (API clients only). provided := extractBearerToken(r) - authedReq, ok := requireAuthBearer(h.token, "", provided, w, r) + authedReq, ok := requireAuthBearer("", provided, w, r) if !ok { return } diff --git a/internal/http/knowledge_graph.go b/internal/http/knowledge_graph.go index 1cd73a29..d78c6c2a 100644 --- a/internal/http/knowledge_graph.go +++ b/internal/http/knowledge_graph.go @@ -13,12 +13,11 @@ import ( type KnowledgeGraphHandler struct { store store.KnowledgeGraphStore providerReg *providers.Registry - token string } // NewKnowledgeGraphHandler creates a handler for KG management endpoints. -func NewKnowledgeGraphHandler(s store.KnowledgeGraphStore, providerReg *providers.Registry, token string) *KnowledgeGraphHandler { - return &KnowledgeGraphHandler{store: s, providerReg: providerReg, token: token} +func NewKnowledgeGraphHandler(s store.KnowledgeGraphStore, providerReg *providers.Registry) *KnowledgeGraphHandler { + return &KnowledgeGraphHandler{store: s, providerReg: providerReg} } // NewExtractor creates an Extractor from the given provider name and model. @@ -46,5 +45,5 @@ func (h *KnowledgeGraphHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *KnowledgeGraphHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } diff --git a/internal/http/mcp.go b/internal/http/mcp.go index 85ad6f34..1faa95dd 100644 --- a/internal/http/mcp.go +++ b/internal/http/mcp.go @@ -26,15 +26,14 @@ type MCPPoolEvictor interface { // MCPHandler handles MCP server management HTTP endpoints. type MCPHandler struct { store store.MCPServerStore - token string msgBus *bus.MessageBus - mgr MCPToolLister // optional, nil when Manager not available - poolEvictor MCPPoolEvictor // optional, nil when pool not available + mgr MCPToolLister // optional, nil when Manager not available + poolEvictor MCPPoolEvictor // optional, nil when pool not available } // NewMCPHandler creates a handler for MCP server management endpoints. -func NewMCPHandler(s store.MCPServerStore, token string, msgBus *bus.MessageBus, mgr MCPToolLister) *MCPHandler { - return &MCPHandler{store: s, token: token, msgBus: msgBus, mgr: mgr} +func NewMCPHandler(s store.MCPServerStore, msgBus *bus.MessageBus, mgr MCPToolLister) *MCPHandler { + return &MCPHandler{store: s, msgBus: msgBus, mgr: mgr} } // SetPoolEvictor sets the pool evictor for credential rotation handling. @@ -82,7 +81,7 @@ func (h *MCPHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *MCPHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } // --- Server CRUD --- diff --git a/internal/http/media_serve.go b/internal/http/media_serve.go index 7e997f08..2225e0f5 100644 --- a/internal/http/media_serve.go +++ b/internal/http/media_serve.go @@ -14,12 +14,11 @@ import ( // MediaServeHandler serves persisted media files by ID. type MediaServeHandler struct { store *media.Store - token string } // NewMediaServeHandler creates a media serve handler. -func NewMediaServeHandler(store *media.Store, token string) *MediaServeHandler { - return &MediaServeHandler{store: store, token: token} +func NewMediaServeHandler(store *media.Store) *MediaServeHandler { + return &MediaServeHandler{store: store} } // RegisterRoutes registers the media serve endpoint. @@ -41,7 +40,7 @@ func (h *MediaServeHandler) auth(next http.HandlerFunc) http.HandlerFunc { } // Priority 2: Bearer header (API clients only). provided := extractBearerToken(r) - authedReq, ok := requireAuthBearer(h.token, "", provided, w, r) + authedReq, ok := requireAuthBearer("", provided, w, r) if !ok { return } diff --git a/internal/http/media_upload.go b/internal/http/media_upload.go index bb2a8ff5..a4b701da 100644 --- a/internal/http/media_upload.go +++ b/internal/http/media_upload.go @@ -19,13 +19,11 @@ const ( ) // MediaUploadHandler handles media file uploads for WebSocket clients. -type MediaUploadHandler struct { - token string -} +type MediaUploadHandler struct{} // NewMediaUploadHandler creates a media upload handler. -func NewMediaUploadHandler(token string) *MediaUploadHandler { - return &MediaUploadHandler{token: token} +func NewMediaUploadHandler() *MediaUploadHandler { + return &MediaUploadHandler{} } // RegisterRoutes registers the upload endpoint. @@ -34,7 +32,7 @@ func (h *MediaUploadHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *MediaUploadHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *MediaUploadHandler) handleUpload(w http.ResponseWriter, r *http.Request) { diff --git a/internal/http/memory.go b/internal/http/memory.go index f281f989..ee59a865 100644 --- a/internal/http/memory.go +++ b/internal/http/memory.go @@ -9,12 +9,11 @@ import ( // MemoryHandler handles memory document management endpoints. type MemoryHandler struct { store store.MemoryStore - token string } // NewMemoryHandler creates a handler for memory management endpoints. -func NewMemoryHandler(s store.MemoryStore, token string) *MemoryHandler { - return &MemoryHandler{store: s, token: token} +func NewMemoryHandler(s store.MemoryStore) *MemoryHandler { + return &MemoryHandler{store: s} } // RegisterRoutes registers all memory routes on the given mux. @@ -31,5 +30,5 @@ func (h *MemoryHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *MemoryHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } diff --git a/internal/http/oauth.go b/internal/http/oauth.go index 82350614..95d3bb40 100644 --- a/internal/http/oauth.go +++ b/internal/http/oauth.go @@ -19,7 +19,6 @@ import ( // OAuthHandler handles OAuth-related HTTP endpoints for web UI. type OAuthHandler struct { - token string // gateway auth token provStore store.ProviderStore secretStore store.ConfigSecretsStore providerReg *providers.Registry @@ -30,9 +29,8 @@ type OAuthHandler struct { } // NewOAuthHandler creates a handler for OAuth endpoints. -func NewOAuthHandler(token string, provStore store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, msgBus *bus.MessageBus) *OAuthHandler { +func NewOAuthHandler(provStore store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, msgBus *bus.MessageBus) *OAuthHandler { return &OAuthHandler{ - token: token, provStore: provStore, secretStore: secretStore, providerReg: providerReg, @@ -49,7 +47,7 @@ func (h *OAuthHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *OAuthHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *OAuthHandler) newTokenSource(r *http.Request) *oauth.DBTokenSource { diff --git a/internal/http/oauth_test.go b/internal/http/oauth_test.go index 0eb4d746..885b9d34 100644 --- a/internal/http/oauth_test.go +++ b/internal/http/oauth_test.go @@ -114,14 +114,18 @@ func (m *mockSecretsStore) GetAll(_ context.Context) (map[string]string, error) // --- helper --- -func newTestOAuthHandler(token string) *OAuthHandler { - return NewOAuthHandler(token, newMockProviderStore(), newMockSecretsStore(), nil, nil) +func newTestOAuthHandler(t *testing.T, token string) *OAuthHandler { + t.Helper() + old := pkgGatewayToken + pkgGatewayToken = token + t.Cleanup(func() { pkgGatewayToken = old }) + return NewOAuthHandler(newMockProviderStore(), newMockSecretsStore(), nil, nil) } // --- tests --- func TestOAuthHandlerStatusNoToken(t *testing.T) { - h := newTestOAuthHandler("") + h := newTestOAuthHandler(t, "") mux := http.NewServeMux() h.RegisterRoutes(mux) @@ -142,7 +146,7 @@ func TestOAuthHandlerStatusNoToken(t *testing.T) { } func TestOAuthHandlerAuth(t *testing.T) { - h := newTestOAuthHandler("secret-token") + h := newTestOAuthHandler(t, "secret-token") mux := http.NewServeMux() h.RegisterRoutes(mux) @@ -167,7 +171,7 @@ func TestOAuthHandlerAuth(t *testing.T) { } func TestOAuthHandlerLogoutNoProvider(t *testing.T) { - h := newTestOAuthHandler("") + h := newTestOAuthHandler(t, "") mux := http.NewServeMux() h.RegisterRoutes(mux) @@ -188,7 +192,7 @@ func TestOAuthHandlerLogoutNoProvider(t *testing.T) { } func TestOAuthHandlerRouteRegistration(t *testing.T) { - h := newTestOAuthHandler("") + h := newTestOAuthHandler(t, "") mux := http.NewServeMux() h.RegisterRoutes(mux) @@ -212,7 +216,7 @@ func TestOAuthHandlerRouteRegistration(t *testing.T) { } func TestOAuthHandlerStartReturnsAuthURL(t *testing.T) { - h := newTestOAuthHandler("") + h := newTestOAuthHandler(t, "") mux := http.NewServeMux() h.RegisterRoutes(mux) diff --git a/internal/http/openapi.go b/internal/http/openapi.go index 4c07058f..58f7db9e 100644 --- a/internal/http/openapi.go +++ b/internal/http/openapi.go @@ -9,13 +9,11 @@ import ( var openapiSpec []byte // DocsHandler serves the OpenAPI spec and Swagger UI. -type DocsHandler struct { - token string -} +type DocsHandler struct{} // NewDocsHandler creates a handler for API documentation endpoints. -func NewDocsHandler(token string) *DocsHandler { - return &DocsHandler{token: token} +func NewDocsHandler() *DocsHandler { + return &DocsHandler{} } // RegisterRoutes registers documentation routes on the given mux. diff --git a/internal/http/packages.go b/internal/http/packages.go index fe946873..07859bfe 100644 --- a/internal/http/packages.go +++ b/internal/http/packages.go @@ -15,13 +15,11 @@ import ( var validPkgName = regexp.MustCompile(`^[a-zA-Z0-9@][a-zA-Z0-9._+\-/@]*$`) // PackagesHandler handles runtime package management HTTP endpoints. -type PackagesHandler struct { - token string -} +type PackagesHandler struct{} // NewPackagesHandler creates a handler for package management endpoints. -func NewPackagesHandler(token string) *PackagesHandler { - return &PackagesHandler{token: token} +func NewPackagesHandler() *PackagesHandler { + return &PackagesHandler{} } // RegisterRoutes registers all package management routes on the given mux. @@ -35,14 +33,14 @@ func (h *PackagesHandler) RegisterRoutes(mux *http.ServeMux) { // readAuth allows viewer+ for read operations. func (h *PackagesHandler) readAuth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } // adminAuth requires admin role for write operations (install/uninstall). // Prevents agents from calling these endpoints even if they obtain the gateway token, // since agent requests via browser pairing only get operator role. func (h *PackagesHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, permissions.RoleAdmin, next) + return requireAuth(permissions.RoleAdmin, next) } // handleList returns all installed packages grouped by category (system/pip/npm). diff --git a/internal/http/packages_test.go b/internal/http/packages_test.go index 6e271f64..8442d82b 100644 --- a/internal/http/packages_test.go +++ b/internal/http/packages_test.go @@ -163,18 +163,15 @@ func TestParseAndValidatePackage_BodySizeLimit(t *testing.T) { // TestNewPackagesHandler creates a handler. func TestNewPackagesHandler(t *testing.T) { - h := NewPackagesHandler("test-token") + h := NewPackagesHandler() if h == nil { t.Fatal("NewPackagesHandler() returned nil") } - if h.token != "test-token" { - t.Errorf("token = %q, want 'test-token'", h.token) - } } // TestPackagesHandler_RegisterRoutes ensures routes are registered without panic. func TestPackagesHandler_RegisterRoutes(t *testing.T) { - h := NewPackagesHandler("test-token") + h := NewPackagesHandler() mux := http.NewServeMux() // Should not panic diff --git a/internal/http/pending_messages.go b/internal/http/pending_messages.go index 4f612de7..3a105087 100644 --- a/internal/http/pending_messages.go +++ b/internal/http/pending_messages.go @@ -17,7 +17,6 @@ import ( type PendingMessagesHandler struct { store store.PendingMessageStore agentStore store.AgentStore - token string providerReg *providers.Registry keepRecent int // global keepRecent from config (0 = use default 15) maxTokens int // max output tokens for LLM summarization (0 = use default) @@ -25,8 +24,8 @@ type PendingMessagesHandler struct { cfgModel string // config-level model override (empty = resolve from agent) } -func NewPendingMessagesHandler(s store.PendingMessageStore, agentStore store.AgentStore, token string, providerReg *providers.Registry) *PendingMessagesHandler { - return &PendingMessagesHandler{store: s, agentStore: agentStore, token: token, providerReg: providerReg} +func NewPendingMessagesHandler(s store.PendingMessageStore, agentStore store.AgentStore, providerReg *providers.Registry) *PendingMessagesHandler { + return &PendingMessagesHandler{store: s, agentStore: agentStore, providerReg: providerReg} } // SetKeepRecent sets the global keepRecent value from config. @@ -49,7 +48,7 @@ func (h *PendingMessagesHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *PendingMessagesHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } // GET /v1/pending-messages — list all groups with resolved titles diff --git a/internal/http/providers.go b/internal/http/providers.go index cbc29c9f..16b74235 100644 --- a/internal/http/providers.go +++ b/internal/http/providers.go @@ -20,7 +20,6 @@ import ( type ProvidersHandler struct { store store.ProviderStore secretStore store.ConfigSecretsStore - token string providerReg *providers.Registry gatewayAddr string // for injecting MCP bridge into Claude CLI providers mcpLookup providers.MCPServerLookup // optional: resolves per-agent MCP servers @@ -30,8 +29,8 @@ type ProvidersHandler struct { } // NewProvidersHandler creates a handler for provider management endpoints. -func NewProvidersHandler(s store.ProviderStore, secretStore store.ConfigSecretsStore, token string, providerReg *providers.Registry, gatewayAddr string) *ProvidersHandler { - return &ProvidersHandler{store: s, secretStore: secretStore, token: token, providerReg: providerReg, gatewayAddr: gatewayAddr} +func NewProvidersHandler(s store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, gatewayAddr string) *ProvidersHandler { + return &ProvidersHandler{store: s, secretStore: secretStore, providerReg: providerReg, gatewayAddr: gatewayAddr} } // SetMessageBus sets the message bus for audit event broadcasting. @@ -95,7 +94,7 @@ func (h *ProvidersHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *ProvidersHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } // maskAPIKey replaces non-empty API keys with "***". @@ -125,7 +124,7 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) { var cliOpts []providers.ClaudeCLIOption cliOpts = append(cliOpts, providers.WithClaudeCLISecurityHooks("", true)) if h.gatewayAddr != "" { - mcpData := providers.BuildCLIMCPConfigData(nil, h.gatewayAddr, h.token) + mcpData := providers.BuildCLIMCPConfigData(nil, h.gatewayAddr, pkgGatewayToken) mcpData.AgentMCPLookup = h.mcpLookup cliOpts = append(cliOpts, providers.WithClaudeCLIMCPConfigData(mcpData)) } diff --git a/internal/http/responses.go b/internal/http/responses.go index 436235e9..70c8ddc8 100644 --- a/internal/http/responses.go +++ b/internal/http/responses.go @@ -20,7 +20,6 @@ import ( type ResponsesHandler struct { agents *agent.Router sessions store.SessionStore - token string postTurn tools.PostTurnProcessor } @@ -30,11 +29,10 @@ func (h *ResponsesHandler) SetPostTurnProcessor(pt tools.PostTurnProcessor) { } // NewResponsesHandler creates a handler for the responses endpoint. -func NewResponsesHandler(agents *agent.Router, sess store.SessionStore, token string) *ResponsesHandler { +func NewResponsesHandler(agents *agent.Router, sess store.SessionStore) *ResponsesHandler { return &ResponsesHandler{ agents: agents, sessions: sess, - token: token, } } @@ -52,7 +50,7 @@ func (h *ResponsesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Auth + RBAC check (gateway token or API key, operator required for POST) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !auth.Authenticated { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return diff --git a/internal/http/secure_cli.go b/internal/http/secure_cli.go index 3fb6f27e..05651bb1 100644 --- a/internal/http/secure_cli.go +++ b/internal/http/secure_cli.go @@ -20,13 +20,12 @@ import ( // SecureCLIHandler handles secure CLI binary credential CRUD endpoints. type SecureCLIHandler struct { store store.SecureCLIStore - token string msgBus *bus.MessageBus } // NewSecureCLIHandler creates a handler for secure CLI credential management. -func NewSecureCLIHandler(s store.SecureCLIStore, token string, msgBus *bus.MessageBus) *SecureCLIHandler { - return &SecureCLIHandler{store: s, token: token, msgBus: msgBus} +func NewSecureCLIHandler(s store.SecureCLIStore, msgBus *bus.MessageBus) *SecureCLIHandler { + return &SecureCLIHandler{store: s, msgBus: msgBus} } // RegisterRoutes registers all secure CLI routes on the given mux. @@ -41,7 +40,7 @@ func (h *SecureCLIHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *SecureCLIHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, permissions.RoleAdmin, next) + return requireAuth(permissions.RoleAdmin, next) } func (h *SecureCLIHandler) emitCacheInvalidate(key string) { diff --git a/internal/http/skills.go b/internal/http/skills.go index 737ff01c..ed11cca8 100644 --- a/internal/http/skills.go +++ b/internal/http/skills.go @@ -28,13 +28,12 @@ type SkillsHandler struct { baseDir string // filesystem base for skill content (skills-store/) — master tenant dataDir string // parent data dir for tenant-scoped skill paths bundledDir string // original bundled skills dir (fallback for broken managed copies) - token string msgBus *bus.MessageBus } // NewSkillsHandler creates a handler for skill management endpoints. -func NewSkillsHandler(skills *pg.PGSkillStore, baseDir, dataDir, bundledDir, token string, msgBus *bus.MessageBus) *SkillsHandler { - return &SkillsHandler{skills: skills, baseDir: baseDir, dataDir: dataDir, bundledDir: bundledDir, token: token, msgBus: msgBus} +func NewSkillsHandler(skills *pg.PGSkillStore, baseDir, dataDir, bundledDir string, msgBus *bus.MessageBus) *SkillsHandler { + return &SkillsHandler{skills: skills, baseDir: baseDir, dataDir: dataDir, bundledDir: bundledDir, msgBus: msgBus} } // tenantSkillsDir returns the skills-store directory scoped to the requesting tenant. @@ -81,13 +80,13 @@ func (h *SkillsHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *SkillsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } // adminMiddleware requires admin role — used for system-level operations // (rescan deps, install packages, toggle skills) that affect the entire server. func (h *SkillsHandler) adminMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, permissions.RoleAdmin, next) + return requireAuth(permissions.RoleAdmin, next) } // requireMasterTenant rejects requests from non-master tenants. @@ -135,7 +134,7 @@ func (h *SkillsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) { } // Ownership check (admins bypass) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { userID := store.UserIDFromContext(r.Context()) if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), id); found && ownerID != userID { @@ -176,7 +175,7 @@ func (h *SkillsHandler) handleDelete(w http.ResponseWriter, r *http.Request) { } // Ownership check (admins bypass) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { userID := store.UserIDFromContext(r.Context()) if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), id); found && ownerID != userID { diff --git a/internal/http/skills_grants.go b/internal/http/skills_grants.go index a19d4825..007c84fc 100644 --- a/internal/http/skills_grants.go +++ b/internal/http/skills_grants.go @@ -45,7 +45,7 @@ func (h *SkillsHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) } // Ownership check (admins bypass) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID { writeJSON(w, http.StatusForbidden, map[string]string{"error": "only the skill owner can perform this action"}) @@ -93,7 +93,7 @@ func (h *SkillsHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request } // Ownership check (admins bypass) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { userID := store.UserIDFromContext(r.Context()) if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID { @@ -131,7 +131,7 @@ func (h *SkillsHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) } // Ownership check (admins bypass) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID { writeJSON(w, http.StatusForbidden, map[string]string{"error": "only the skill owner can perform this action"}) @@ -176,7 +176,7 @@ func (h *SkillsHandler) handleRevokeUser(w http.ResponseWriter, r *http.Request) } // Ownership check (admins bypass) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { userID := store.UserIDFromContext(r.Context()) if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID { diff --git a/internal/http/storage.go b/internal/http/storage.go index 4cbc94f1..f4da48f0 100644 --- a/internal/http/storage.go +++ b/internal/http/storage.go @@ -31,15 +31,14 @@ type sizeCacheEntry struct { type StorageHandler struct { baseDir string // global data dir (resolved absolute path to ~/.goclaw/) - token string // sizeCache caches the total storage size per tenant for 60 minutes. sizeCache sync.Map // tenantBaseDir (string) → *sizeCacheEntry } // NewStorageHandler creates a handler for workspace storage management. -func NewStorageHandler(baseDir, token string) *StorageHandler { - return &StorageHandler{baseDir: baseDir, token: token} +func NewStorageHandler(baseDir string) *StorageHandler { + return &StorageHandler{baseDir: baseDir} } // RegisterRoutes registers storage management routes on the given mux. @@ -51,7 +50,7 @@ func (h *StorageHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *StorageHandler) auth(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } // tenantBaseDir resolves the data directory scoped to the requesting tenant. diff --git a/internal/http/team_attachments.go b/internal/http/team_attachments.go index 90350974..0e2de939 100644 --- a/internal/http/team_attachments.go +++ b/internal/http/team_attachments.go @@ -14,13 +14,12 @@ import ( // TeamAttachmentsHandler serves team task attachment files for download. type TeamAttachmentsHandler struct { teamStore store.TeamStore - token string dataDir string } // NewTeamAttachmentsHandler creates a new handler for serving team attachments. -func NewTeamAttachmentsHandler(teamStore store.TeamStore, token, dataDir string) *TeamAttachmentsHandler { - return &TeamAttachmentsHandler{teamStore: teamStore, token: token, dataDir: dataDir} +func NewTeamAttachmentsHandler(teamStore store.TeamStore, dataDir string) *TeamAttachmentsHandler { + return &TeamAttachmentsHandler{teamStore: teamStore, dataDir: dataDir} } // RegisterRoutes registers the attachment download endpoint. @@ -46,7 +45,7 @@ func (h *TeamAttachmentsHandler) authMiddleware(next http.HandlerFunc) http.Hand http.Error(w, "unauthorized", http.StatusUnauthorized) return } - authedReq, ok := requireAuthBearer(h.token, "", provided, w, r) + authedReq, ok := requireAuthBearer("", provided, w, r) if !ok { return } diff --git a/internal/http/team_events.go b/internal/http/team_events.go index a04f9636..35290dec 100644 --- a/internal/http/team_events.go +++ b/internal/http/team_events.go @@ -13,11 +13,10 @@ import ( // TeamEventsHandler handles team event history HTTP endpoints. type TeamEventsHandler struct { teamStore store.TeamStore - token string } -func NewTeamEventsHandler(teamStore store.TeamStore, token string) *TeamEventsHandler { - return &TeamEventsHandler{teamStore: teamStore, token: token} +func NewTeamEventsHandler(teamStore store.TeamStore) *TeamEventsHandler { + return &TeamEventsHandler{teamStore: teamStore} } func (h *TeamEventsHandler) RegisterRoutes(mux *http.ServeMux) { @@ -25,17 +24,7 @@ func (h *TeamEventsHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *TeamEventsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if h.token != "" { - provided := extractBearerToken(r) - if !tokenMatch(provided, h.token) { - locale := extractLocale(r) - writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)}) - return - } - } - next(w, r) - } + return requireAuth("", next) } func (h *TeamEventsHandler) handleList(w http.ResponseWriter, r *http.Request) { diff --git a/internal/http/tenants.go b/internal/http/tenants.go index 4e9ddc1b..5b97dd6b 100644 --- a/internal/http/tenants.go +++ b/internal/http/tenants.go @@ -19,20 +19,19 @@ import ( // TenantsHandler handles tenant CRUD and membership endpoints. type TenantsHandler struct { tenantStore store.TenantStore - token string msgBus *bus.MessageBus workspace string // base workspace directory for tenant dirs } // NewTenantsHandler creates a handler for tenant management endpoints. -func NewTenantsHandler(tenantStore store.TenantStore, token string, msgBus *bus.MessageBus, workspace string) *TenantsHandler { - return &TenantsHandler{tenantStore: tenantStore, token: token, msgBus: msgBus, workspace: workspace} +func NewTenantsHandler(tenantStore store.TenantStore, msgBus *bus.MessageBus, workspace string) *TenantsHandler { + return &TenantsHandler{tenantStore: tenantStore, msgBus: msgBus, workspace: workspace} } // RegisterRoutes registers all tenant management routes on the given mux. func (h *TenantsHandler) RegisterRoutes(mux *http.ServeMux) { admin := func(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, permissions.RoleAdmin, next) + return requireAuth(permissions.RoleAdmin, next) } mux.HandleFunc("GET /v1/tenants", admin(h.handleList)) mux.HandleFunc("POST /v1/tenants", admin(h.handleCreate)) diff --git a/internal/http/tools_invoke.go b/internal/http/tools_invoke.go index 9933e7f9..7805ba08 100644 --- a/internal/http/tools_invoke.go +++ b/internal/http/tools_invoke.go @@ -14,15 +14,13 @@ import ( // ToolsInvokeHandler handles POST /v1/tools/invoke (direct tool invocation). type ToolsInvokeHandler struct { registry *tools.Registry - token string agentStore store.AgentStore // nil if not configured } // NewToolsInvokeHandler creates a handler for the tools invoke endpoint. -func NewToolsInvokeHandler(registry *tools.Registry, token string, agentStore store.AgentStore) *ToolsInvokeHandler { +func NewToolsInvokeHandler(registry *tools.Registry, agentStore store.AgentStore) *ToolsInvokeHandler { return &ToolsInvokeHandler{ registry: registry, - token: token, agentStore: agentStore, } } @@ -47,7 +45,7 @@ func (h *ToolsInvokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !auth.Authenticated { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)}) return diff --git a/internal/http/traces.go b/internal/http/traces.go index 383d1ed3..5e470c33 100644 --- a/internal/http/traces.go +++ b/internal/http/traces.go @@ -19,12 +19,11 @@ import ( // TracesHandler handles LLM trace listing and detail endpoints. type TracesHandler struct { tracing store.TracingStore - token string } // NewTracesHandler creates a handler for trace management endpoints. -func NewTracesHandler(tracing store.TracingStore, token string) *TracesHandler { - return &TracesHandler{tracing: tracing, token: token} +func NewTracesHandler(tracing store.TracingStore) *TracesHandler { + return &TracesHandler{tracing: tracing} } // RegisterRoutes registers trace routes on the given mux. @@ -36,7 +35,7 @@ func (h *TracesHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *TracesHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *TracesHandler) handleList(w http.ResponseWriter, r *http.Request) { @@ -75,7 +74,7 @@ func (h *TracesHandler) handleList(w http.ResponseWriter, r *http.Request) { } // Non-admin callers may only see their own traces. - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { callerID := store.UserIDFromContext(r.Context()) opts.UserID = callerID @@ -113,7 +112,7 @@ func (h *TracesHandler) handleGet(w http.ResponseWriter, r *http.Request) { } // Non-admin callers may only access their own traces. - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) { callerID := store.UserIDFromContext(r.Context()) if trace.UserID != callerID { @@ -184,7 +183,7 @@ func (h *TracesHandler) handleExport(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "trace", traceID.String())}) return } - authExport := resolveAuth(r, h.token) + authExport := resolveAuth(r) if !permissions.HasMinRole(authExport.Role, permissions.RoleAdmin) { callerID := store.UserIDFromContext(r.Context()) if rootTrace.UserID != callerID { diff --git a/internal/http/usage.go b/internal/http/usage.go index 13a923bb..901573fe 100644 --- a/internal/http/usage.go +++ b/internal/http/usage.go @@ -16,11 +16,10 @@ import ( type UsageHandler struct { snapshots store.SnapshotStore db *sql.DB - token string } -func NewUsageHandler(snapshots store.SnapshotStore, db *sql.DB, token string) *UsageHandler { - return &UsageHandler{snapshots: snapshots, db: db, token: token} +func NewUsageHandler(snapshots store.SnapshotStore, db *sql.DB) *UsageHandler { + return &UsageHandler{snapshots: snapshots, db: db} } func (h *UsageHandler) RegisterRoutes(mux *http.ServeMux) { @@ -30,7 +29,7 @@ func (h *UsageHandler) RegisterRoutes(mux *http.ServeMux) { } func (h *UsageHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { - return requireAuth(h.token, "", next) + return requireAuth("", next) } func (h *UsageHandler) handleTimeSeries(w http.ResponseWriter, r *http.Request) { diff --git a/internal/http/wake.go b/internal/http/wake.go index 3cf3ddc4..003d877a 100644 --- a/internal/http/wake.go +++ b/internal/http/wake.go @@ -20,7 +20,6 @@ import ( // Allows orchestrators (Paperclip, n8n, etc.) to trigger agent runs via HTTP. type WakeHandler struct { agents *agent.Router - token string postTurn tools.PostTurnProcessor } @@ -30,8 +29,8 @@ func (h *WakeHandler) SetPostTurnProcessor(pt tools.PostTurnProcessor) { } // NewWakeHandler creates a handler for the wake endpoint. -func NewWakeHandler(agents *agent.Router, token string) *WakeHandler { - return &WakeHandler{agents: agents, token: token} +func NewWakeHandler(agents *agent.Router) *WakeHandler { + return &WakeHandler{agents: agents} } // RegisterRoutes registers wake routes on the given mux. @@ -62,7 +61,7 @@ func (h *WakeHandler) handleWake(w http.ResponseWriter, r *http.Request) { locale := extractLocale(r) // Auth + RBAC check (gateway token or API key, operator required for POST) - auth := resolveAuth(r, h.token) + auth := resolveAuth(r) if !auth.Authenticated { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)}) return diff --git a/internal/store/context.go b/internal/store/context.go index 86a3a068..5ed8fced 100644 --- a/internal/store/context.go +++ b/internal/store/context.go @@ -35,6 +35,8 @@ const ( CrossTenantKey contextKey = "goclaw_cross_tenant" // TenantSlugKey stores the tenant's URL-safe slug for filesystem paths. TenantSlugKey contextKey = "goclaw_tenant_slug" + // RoleKey is the context key for the caller's permission role (e.g. "admin", "operator", "viewer"). + RoleKey contextKey = "goclaw_role" ) // WithShellDenyGroups returns a new context with shell deny group overrides. @@ -204,3 +206,16 @@ func TenantSlugFromContext(ctx context.Context) string { } return "" } + +// WithRole returns a new context with the caller's permission role. +func WithRole(ctx context.Context, role string) context.Context { + return context.WithValue(ctx, RoleKey, role) +} + +// RoleFromContext extracts the permission role from context. Returns "" if not set. +func RoleFromContext(ctx context.Context) string { + if v, ok := ctx.Value(RoleKey).(string); ok { + return v + } + return "" +}