diff --git a/internal/gateway/router.go b/internal/gateway/router.go index 2c62717b..7a4fcf3d 100644 --- a/internal/gateway/router.go +++ b/internal/gateway/router.go @@ -71,7 +71,7 @@ func (r *MethodRouter) Handle(ctx context.Context, client *Client, req *protocol client.SendResponse(protocol.NewErrorResponse( req.ID, protocol.ErrUnauthorized, - i18n.T(locale, i18n.MsgPermissionDenied, req.Method), + i18n.T(locale, i18n.MsgPermissionDenied, req.Method+" requires "+string(permissions.MethodRole(req.Method))+" role"), )) return } diff --git a/internal/http/agents.go b/internal/http/agents.go index af70dd11..ac62773a 100644 --- a/internal/http/agents.go +++ b/internal/http/agents.go @@ -14,6 +14,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/i18n" + "github.com/nextlevelbuilder/goclaw/internal/permissions" "github.com/nextlevelbuilder/goclaw/internal/providers" "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/pkg/protocol" @@ -66,27 +67,36 @@ func (h *AgentsHandler) emitCacheInvalidate(kind, key string) { // RegisterRoutes registers all agent management routes on the given mux. func (h *AgentsHandler) RegisterRoutes(mux *http.ServeMux) { + // Agent CRUD (reads: viewer+, writes: admin+) mux.HandleFunc("GET /v1/agents", h.authMiddleware(h.handleList)) - mux.HandleFunc("POST /v1/agents", h.authMiddleware(h.handleCreate)) + mux.HandleFunc("POST /v1/agents", h.adminMiddleware(h.handleCreate)) mux.HandleFunc("GET /v1/agents/{id}", h.authMiddleware(h.handleGet)) - mux.HandleFunc("PUT /v1/agents/{id}", h.authMiddleware(h.handleUpdate)) - mux.HandleFunc("DELETE /v1/agents/{id}", h.authMiddleware(h.handleDelete)) + mux.HandleFunc("PUT /v1/agents/{id}", h.adminMiddleware(h.handleUpdate)) + mux.HandleFunc("DELETE /v1/agents/{id}", h.adminMiddleware(h.handleDelete)) + // Sharing (admin+) mux.HandleFunc("GET /v1/agents/{id}/shares", h.authMiddleware(h.handleListShares)) - mux.HandleFunc("POST /v1/agents/{id}/shares", h.authMiddleware(h.handleShare)) - mux.HandleFunc("DELETE /v1/agents/{id}/shares/{userID}", h.authMiddleware(h.handleRevokeShare)) - mux.HandleFunc("POST /v1/agents/{id}/regenerate", h.authMiddleware(h.handleRegenerate)) - mux.HandleFunc("POST /v1/agents/{id}/resummon", h.authMiddleware(h.handleResummon)) + mux.HandleFunc("POST /v1/agents/{id}/shares", h.adminMiddleware(h.handleShare)) + mux.HandleFunc("DELETE /v1/agents/{id}/shares/{userID}", h.adminMiddleware(h.handleRevokeShare)) + // Agent operations (admin+) + mux.HandleFunc("POST /v1/agents/{id}/regenerate", h.adminMiddleware(h.handleRegenerate)) + mux.HandleFunc("POST /v1/agents/{id}/resummon", h.adminMiddleware(h.handleResummon)) + // Read-only (viewer+) mux.HandleFunc("GET /v1/agents/{id}/codex-pool-activity", h.authMiddleware(h.handleCodexPoolActivity)) mux.HandleFunc("GET /v1/agents/{id}/instances", h.authMiddleware(h.handleListInstances)) mux.HandleFunc("GET /v1/agents/{id}/instances/{userID}/files", h.authMiddleware(h.handleGetInstanceFiles)) - mux.HandleFunc("PUT /v1/agents/{id}/instances/{userID}/files/{fileName}", h.authMiddleware(h.handleSetInstanceFile)) - mux.HandleFunc("PATCH /v1/agents/{id}/instances/{userID}/metadata", h.authMiddleware(h.handleUpdateInstanceMetadata)) + // Instance writes (admin+) + mux.HandleFunc("PUT /v1/agents/{id}/instances/{userID}/files/{fileName}", h.adminMiddleware(h.handleSetInstanceFile)) + mux.HandleFunc("PATCH /v1/agents/{id}/instances/{userID}/metadata", h.adminMiddleware(h.handleUpdateInstanceMetadata)) } func (h *AgentsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { return requireAuth("", next) } +func (h *AgentsHandler) adminMiddleware(next http.HandlerFunc) http.HandlerFunc { + return requireAuth(permissions.RoleAdmin, next) +} + func (h *AgentsHandler) handleList(w http.ResponseWriter, r *http.Request) { userID := store.UserIDFromContext(r.Context()) if userID == "" { diff --git a/internal/http/auth.go b/internal/http/auth.go index 14f89064..45ce10de 100644 --- a/internal/http/auth.go +++ b/internal/http/auth.go @@ -367,7 +367,7 @@ func requireAuth(minRole permissions.Role, next http.HandlerFunc) http.HandlerFu if !permissions.HasMinRole(auth.Role, required) { writeJSON(w, http.StatusForbidden, map[string]string{ - "error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path), + "error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path+" requires "+string(required)+" role"), }) return } @@ -398,7 +398,7 @@ func requireAuthBearer(minRole permissions.Role, bearer string, w http.ResponseW if !permissions.HasMinRole(auth.Role, required) { writeJSON(w, http.StatusForbidden, map[string]string{ - "error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path), + "error": i18n.T(locale, i18n.MsgPermissionDenied, r.URL.Path+" requires "+string(required)+" role"), }) return r, false } diff --git a/internal/http/builtin_tools.go b/internal/http/builtin_tools.go index 351b0dfb..dff3a71f 100644 --- a/internal/http/builtin_tools.go +++ b/internal/http/builtin_tools.go @@ -9,6 +9,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/i18n" + "github.com/nextlevelbuilder/goclaw/internal/permissions" "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) @@ -29,17 +30,22 @@ func NewBuiltinToolsHandler(s store.BuiltinToolStore, tenantCfgs store.BuiltinTo // RegisterRoutes registers all built-in tool routes on the given mux. func (h *BuiltinToolsHandler) RegisterRoutes(mux *http.ServeMux) { + // Builtin tools (reads: viewer+, writes: admin+) mux.HandleFunc("GET /v1/tools/builtin", h.auth(h.handleList)) mux.HandleFunc("GET /v1/tools/builtin/{name}", h.auth(h.handleGet)) - mux.HandleFunc("PUT /v1/tools/builtin/{name}", h.auth(h.handleUpdate)) - mux.HandleFunc("PUT /v1/tools/builtin/{name}/tenant-config", h.auth(h.handleSetTenantConfig)) - mux.HandleFunc("DELETE /v1/tools/builtin/{name}/tenant-config", h.auth(h.handleDeleteTenantConfig)) + mux.HandleFunc("PUT /v1/tools/builtin/{name}", h.adminAuth(h.handleUpdate)) + mux.HandleFunc("PUT /v1/tools/builtin/{name}/tenant-config", h.adminAuth(h.handleSetTenantConfig)) + mux.HandleFunc("DELETE /v1/tools/builtin/{name}/tenant-config", h.adminAuth(h.handleDeleteTenantConfig)) } func (h *BuiltinToolsHandler) auth(next http.HandlerFunc) http.HandlerFunc { return requireAuth("", next) } +func (h *BuiltinToolsHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc { + return requireAuth(permissions.RoleAdmin, next) +} + func (h *BuiltinToolsHandler) emitCacheInvalidate(key string) { if h.msgBus == nil { return diff --git a/internal/http/channel_instances.go b/internal/http/channel_instances.go index d170983a..ee0f9c29 100644 --- a/internal/http/channel_instances.go +++ b/internal/http/channel_instances.go @@ -11,6 +11,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/i18n" + "github.com/nextlevelbuilder/goclaw/internal/permissions" "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) @@ -32,18 +33,19 @@ func NewChannelInstancesHandler(s store.ChannelInstanceStore, agentStore store.A // RegisterRoutes registers all channel instance routes on the given mux. func (h *ChannelInstancesHandler) RegisterRoutes(mux *http.ServeMux) { + // Channel instance CRUD (reads: viewer+, writes: admin+) mux.HandleFunc("GET /v1/channels/instances", h.auth(h.handleList)) - mux.HandleFunc("POST /v1/channels/instances", h.auth(h.handleCreate)) + mux.HandleFunc("POST /v1/channels/instances", h.adminAuth(h.handleCreate)) mux.HandleFunc("GET /v1/channels/instances/{id}", h.auth(h.handleGet)) - mux.HandleFunc("PUT /v1/channels/instances/{id}", h.auth(h.handleUpdate)) - mux.HandleFunc("DELETE /v1/channels/instances/{id}", h.auth(h.handleDelete)) + mux.HandleFunc("PUT /v1/channels/instances/{id}", h.adminAuth(h.handleUpdate)) + mux.HandleFunc("DELETE /v1/channels/instances/{id}", h.adminAuth(h.handleDelete)) // Channel contacts (global, not per-agent) if h.contactStore != nil { mux.HandleFunc("GET /v1/contacts", h.auth(h.handleListContacts)) mux.HandleFunc("GET /v1/contacts/resolve", h.auth(h.handleResolveContacts)) - mux.HandleFunc("POST /v1/contacts/merge", h.auth(h.handleMergeContacts)) - mux.HandleFunc("POST /v1/contacts/unmerge", h.auth(h.handleUnmergeContacts)) + mux.HandleFunc("POST /v1/contacts/merge", h.adminAuth(h.handleMergeContacts)) + mux.HandleFunc("POST /v1/contacts/unmerge", h.adminAuth(h.handleUnmergeContacts)) mux.HandleFunc("GET /v1/contacts/merged/{tenantUserId}", h.auth(h.handleListMergedContacts)) } if h.tenantStore != nil { @@ -54,8 +56,8 @@ func (h *ChannelInstancesHandler) RegisterRoutes(mux *http.ServeMux) { if h.configPermStore != nil { mux.HandleFunc("GET /v1/channels/instances/{id}/writers/groups", h.auth(h.handleWriterGroups)) mux.HandleFunc("GET /v1/channels/instances/{id}/writers", h.auth(h.handleListWriters)) - mux.HandleFunc("POST /v1/channels/instances/{id}/writers", h.auth(h.handleAddWriter)) - mux.HandleFunc("DELETE /v1/channels/instances/{id}/writers/{userId}", h.auth(h.handleRemoveWriter)) + mux.HandleFunc("POST /v1/channels/instances/{id}/writers", h.adminAuth(h.handleAddWriter)) + mux.HandleFunc("DELETE /v1/channels/instances/{id}/writers/{userId}", h.adminAuth(h.handleRemoveWriter)) } } @@ -63,6 +65,10 @@ func (h *ChannelInstancesHandler) auth(next http.HandlerFunc) http.HandlerFunc { return requireAuth("", next) } +func (h *ChannelInstancesHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc { + return requireAuth(permissions.RoleAdmin, next) +} + func (h *ChannelInstancesHandler) emitCacheInvalidate() { if h.msgBus == nil { return diff --git a/internal/http/mcp.go b/internal/http/mcp.go index a6eaa0b1..c65e8000 100644 --- a/internal/http/mcp.go +++ b/internal/http/mcp.go @@ -9,6 +9,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/bus" "github.com/nextlevelbuilder/goclaw/internal/i18n" + "github.com/nextlevelbuilder/goclaw/internal/permissions" "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) @@ -51,42 +52,46 @@ func (h *MCPHandler) emitCacheInvalidate() { // RegisterRoutes registers all MCP management routes on the given mux. func (h *MCPHandler) RegisterRoutes(mux *http.ServeMux) { - // Server CRUD + // Server CRUD (reads: viewer+, writes: admin+) mux.HandleFunc("GET /v1/mcp/servers", h.auth(h.handleListServers)) - mux.HandleFunc("POST /v1/mcp/servers", h.auth(h.handleCreateServer)) + mux.HandleFunc("POST /v1/mcp/servers", h.adminAuth(h.handleCreateServer)) mux.HandleFunc("GET /v1/mcp/servers/{id}", h.auth(h.handleGetServer)) - mux.HandleFunc("PUT /v1/mcp/servers/{id}", h.auth(h.handleUpdateServer)) - mux.HandleFunc("DELETE /v1/mcp/servers/{id}", h.auth(h.handleDeleteServer)) + mux.HandleFunc("PUT /v1/mcp/servers/{id}", h.adminAuth(h.handleUpdateServer)) + mux.HandleFunc("DELETE /v1/mcp/servers/{id}", h.adminAuth(h.handleDeleteServer)) - // Test connection (no save) - mux.HandleFunc("POST /v1/mcp/servers/test", h.auth(h.handleTestConnection)) + // Test connection (admin+ — infra operation) + mux.HandleFunc("POST /v1/mcp/servers/test", h.adminAuth(h.handleTestConnection)) - // Reconnect (evict pooled connection so next use re-discovers tools) - mux.HandleFunc("POST /v1/mcp/servers/{id}/reconnect", h.auth(h.handleReconnectServer)) + // Reconnect (admin+ — evict pooled connection) + mux.HandleFunc("POST /v1/mcp/servers/{id}/reconnect", h.adminAuth(h.handleReconnectServer)) - // Server tools (runtime-discovered) + // Server tools (read-only: viewer+) mux.HandleFunc("GET /v1/mcp/servers/{id}/tools", h.auth(h.handleListServerTools)) - // Agent grants + // Agent grants (reads: viewer+, writes: admin+) mux.HandleFunc("GET /v1/mcp/servers/{id}/grants", h.auth(h.handleListServerGrants)) - mux.HandleFunc("POST /v1/mcp/servers/{id}/grants/agent", h.auth(h.handleGrantAgent)) - mux.HandleFunc("DELETE /v1/mcp/servers/{id}/grants/agent/{agentID}", h.auth(h.handleRevokeAgent)) + mux.HandleFunc("POST /v1/mcp/servers/{id}/grants/agent", h.adminAuth(h.handleGrantAgent)) + mux.HandleFunc("DELETE /v1/mcp/servers/{id}/grants/agent/{agentID}", h.adminAuth(h.handleRevokeAgent)) mux.HandleFunc("GET /v1/mcp/grants/agent/{agentID}", h.auth(h.handleListAgentGrants)) - // User grants - mux.HandleFunc("POST /v1/mcp/servers/{id}/grants/user", h.auth(h.handleGrantUser)) - mux.HandleFunc("DELETE /v1/mcp/servers/{id}/grants/user/{userID}", h.auth(h.handleRevokeUser)) + // User grants (admin+) + mux.HandleFunc("POST /v1/mcp/servers/{id}/grants/user", h.adminAuth(h.handleGrantUser)) + mux.HandleFunc("DELETE /v1/mcp/servers/{id}/grants/user/{userID}", h.adminAuth(h.handleRevokeUser)) - // Access requests + // Access requests (create: viewer+, list: viewer+, review: admin+) mux.HandleFunc("POST /v1/mcp/requests", h.auth(h.handleCreateRequest)) mux.HandleFunc("GET /v1/mcp/requests", h.auth(h.handleListPendingRequests)) - mux.HandleFunc("POST /v1/mcp/requests/{id}/review", h.auth(h.handleReviewRequest)) + mux.HandleFunc("POST /v1/mcp/requests/{id}/review", h.adminAuth(h.handleReviewRequest)) } func (h *MCPHandler) auth(next http.HandlerFunc) http.HandlerFunc { return requireAuth("", next) } +func (h *MCPHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc { + return requireAuth(permissions.RoleAdmin, next) +} + // --- Server CRUD --- // mcpServerWithCounts extends MCPServerData with agent grant count for list responses. diff --git a/internal/http/skills.go b/internal/http/skills.go index 410aa734..d28430fb 100644 --- a/internal/http/skills.go +++ b/internal/http/skills.go @@ -58,19 +58,22 @@ func (h *SkillsHandler) emitCacheInvalidate(kind, key string) { // RegisterRoutes registers all skill management routes on the given mux. func (h *SkillsHandler) RegisterRoutes(mux *http.ServeMux) { + // Skill reads (viewer+) mux.HandleFunc("GET /v1/skills", h.authMiddleware(h.handleList)) - mux.HandleFunc("POST /v1/skills/upload", h.authMiddleware(h.handleUpload)) mux.HandleFunc("GET /v1/skills/{id}", h.authMiddleware(h.handleGet)) - mux.HandleFunc("PUT /v1/skills/{id}", h.authMiddleware(h.handleUpdate)) - mux.HandleFunc("DELETE /v1/skills/{id}", h.authMiddleware(h.handleDelete)) - mux.HandleFunc("POST /v1/skills/{id}/grants/agent", h.authMiddleware(h.handleGrantAgent)) - mux.HandleFunc("DELETE /v1/skills/{id}/grants/agent/{agentID}", h.authMiddleware(h.handleRevokeAgent)) - mux.HandleFunc("POST /v1/skills/{id}/grants/user", h.authMiddleware(h.handleGrantUser)) - mux.HandleFunc("DELETE /v1/skills/{id}/grants/user/{userID}", h.authMiddleware(h.handleRevokeUser)) mux.HandleFunc("GET /v1/agents/{agentID}/skills", h.authMiddleware(h.handleListAgentSkills)) mux.HandleFunc("GET /v1/skills/{id}/versions", h.authMiddleware(h.handleListVersions)) mux.HandleFunc("GET /v1/skills/{id}/files/{path...}", h.authMiddleware(h.handleReadFile)) mux.HandleFunc("GET /v1/skills/{id}/files", h.authMiddleware(h.handleListFiles)) + // Skill writes (admin+) + mux.HandleFunc("POST /v1/skills/upload", h.adminMiddleware(h.handleUpload)) + mux.HandleFunc("PUT /v1/skills/{id}", h.adminMiddleware(h.handleUpdate)) + mux.HandleFunc("DELETE /v1/skills/{id}", h.adminMiddleware(h.handleDelete)) + // Skill grants (admin+) + mux.HandleFunc("POST /v1/skills/{id}/grants/agent", h.adminMiddleware(h.handleGrantAgent)) + mux.HandleFunc("DELETE /v1/skills/{id}/grants/agent/{agentID}", h.adminMiddleware(h.handleRevokeAgent)) + mux.HandleFunc("POST /v1/skills/{id}/grants/user", h.adminMiddleware(h.handleGrantUser)) + mux.HandleFunc("DELETE /v1/skills/{id}/grants/user/{userID}", h.adminMiddleware(h.handleRevokeUser)) // System-level operations: admin + master tenant only. // These execute shell commands (pip/npm install) and affect the entire server. mux.HandleFunc("POST /v1/skills/rescan-deps", h.adminMiddleware(h.handleRescanDeps)) @@ -78,9 +81,9 @@ func (h *SkillsHandler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("POST /v1/skills/install-dep", h.adminMiddleware(h.handleInstallDep)) mux.HandleFunc("GET /v1/skills/runtimes", h.adminMiddleware(h.handleRuntimes)) mux.HandleFunc("POST /v1/skills/{id}/toggle", h.adminMiddleware(h.handleToggle)) - // Per-tenant overrides: tenant-level admin check inside handler (not system admin). - mux.HandleFunc("PUT /v1/skills/{id}/tenant-config", h.authMiddleware(h.handleSetTenantConfig)) - mux.HandleFunc("DELETE /v1/skills/{id}/tenant-config", h.authMiddleware(h.handleDeleteTenantConfig)) + // Per-tenant overrides (admin+) + mux.HandleFunc("PUT /v1/skills/{id}/tenant-config", h.adminMiddleware(h.handleSetTenantConfig)) + mux.HandleFunc("DELETE /v1/skills/{id}/tenant-config", h.adminMiddleware(h.handleDeleteTenantConfig)) } func (h *SkillsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc { diff --git a/internal/i18n/catalog_en.go b/internal/i18n/catalog_en.go index 05f241c0..fb39ebb1 100644 --- a/internal/i18n/catalog_en.go +++ b/internal/i18n/catalog_en.go @@ -10,7 +10,7 @@ func init() { MsgInvalidRequest: "invalid request: %s", MsgInvalidJSON: "invalid JSON", MsgUnauthorized: "unauthorized", - MsgPermissionDenied: "permission denied: insufficient role for %s", + MsgPermissionDenied: "permission denied: %s", MsgInternalError: "internal error: %s", MsgInvalidSlug: "%s must be a valid slug (lowercase letters, numbers, hyphens only)", MsgFailedToList: "failed to list %s", diff --git a/internal/i18n/catalog_vi.go b/internal/i18n/catalog_vi.go index dd727bfc..ae9f611f 100644 --- a/internal/i18n/catalog_vi.go +++ b/internal/i18n/catalog_vi.go @@ -10,7 +10,7 @@ func init() { MsgInvalidRequest: "yêu cầu không hợp lệ: %s", MsgInvalidJSON: "JSON không hợp lệ", MsgUnauthorized: "chưa xác thực", - MsgPermissionDenied: "từ chối quyền truy cập: không đủ quyền cho %s", + MsgPermissionDenied: "từ chối quyền truy cập: %s", MsgInternalError: "lỗi nội bộ: %s", MsgInvalidSlug: "%s phải là slug hợp lệ (chữ thường, số, dấu gạch ngang)", MsgFailedToList: "không thể liệt kê %s", diff --git a/internal/i18n/catalog_zh.go b/internal/i18n/catalog_zh.go index 9386a600..a13d09a5 100644 --- a/internal/i18n/catalog_zh.go +++ b/internal/i18n/catalog_zh.go @@ -10,7 +10,7 @@ func init() { MsgInvalidRequest: "无效请求:%s", MsgInvalidJSON: "无效的 JSON", MsgUnauthorized: "未授权", - MsgPermissionDenied: "权限不足:无法访问 %s", + MsgPermissionDenied: "权限不足:%s", MsgInternalError: "内部错误:%s", MsgInvalidSlug: "%s 必须是有效的 slug(小写字母、数字、连字符)", MsgFailedToList: "获取 %s 列表失败", diff --git a/internal/i18n/keys.go b/internal/i18n/keys.go index 0da1bdec..f8ab95f1 100644 --- a/internal/i18n/keys.go +++ b/internal/i18n/keys.go @@ -11,7 +11,7 @@ const ( MsgInvalidRequest = "error.invalid_request" // "invalid request: %s" MsgInvalidJSON = "error.invalid_json" // "invalid JSON" MsgUnauthorized = "error.unauthorized" // "unauthorized" - MsgPermissionDenied = "error.permission_denied" // "permission denied: insufficient role for %s" + MsgPermissionDenied = "error.permission_denied" // "permission denied: %s" MsgInternalError = "error.internal" // "internal error: %s" MsgInvalidSlug = "error.invalid_slug" // "%s must be a valid slug (lowercase letters, numbers, hyphens only)" MsgFailedToList = "error.failed_to_list" // "failed to list %s" diff --git a/internal/permissions/policy.go b/internal/permissions/policy.go index d890e51e..4f5cf7f8 100644 --- a/internal/permissions/policy.go +++ b/internal/permissions/policy.go @@ -180,6 +180,7 @@ func isAdminMethod(method string) bool { protocol.MethodAPIKeysList, protocol.MethodAPIKeysCreate, protocol.MethodAPIKeysRevoke, + protocol.MethodSkillsUpdate, } return slices.Contains(adminMethods, method) } @@ -195,7 +196,6 @@ func isWriteMethod(method string) bool { protocol.MethodCronUpdate, protocol.MethodCronDelete, protocol.MethodCronToggle, - protocol.MethodSkillsUpdate, "pairing.", "device.pair.", "approvals.",