fix(agent): remove unused RunnableByViewer gate blocking viewer chat

The runnable_by_viewer field was never populated for built-in agents,
so TenantRoleViewer users always hit 403 on agent-chat. Drop the field
and RBAC check so agent-chat matches knowledge-chat access for viewers.
This commit is contained in:
wizardchen
2026-06-23 18:19:17 +08:00
committed by lyingbug
parent 35f717f7fa
commit be26563f86
4 changed files with 66 additions and 109 deletions
+64 -65
View File
@@ -15,76 +15,75 @@ import (
// Agent represents an agent entity
type Agent struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Avatar string `json:"avatar"`
IsBuiltin bool `json:"is_builtin"`
TenantID uint64 `json:"tenant_id"`
CreatedBy string `json:"created_by"`
RunnableByViewer bool `json:"runnable_by_viewer"`
Config *AgentConfig `json:"config"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatorName string `json:"creator_name,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Avatar string `json:"avatar"`
IsBuiltin bool `json:"is_builtin"`
TenantID uint64 `json:"tenant_id"`
CreatedBy string `json:"created_by"`
Config *AgentConfig `json:"config"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatorName string `json:"creator_name,omitempty"`
}
// AgentConfig represents the configuration for an agent.
// Field names and JSON tags mirror internal/types.CustomAgentConfig.
type AgentConfig struct {
AgentMode string `json:"agent_mode"`
AgentType string `json:"agent_type,omitempty"`
SystemPrompt string `json:"system_prompt"`
SystemPromptID string `json:"system_prompt_id,omitempty"`
ContextTemplate string `json:"context_template"`
ContextTemplateID string `json:"context_template_id,omitempty"`
ModelID string `json:"model_id"`
RerankModelID string `json:"rerank_model_id"`
Temperature float64 `json:"temperature"`
MaxCompletionTokens int `json:"max_completion_tokens"`
Thinking *bool `json:"thinking"`
MaxIterations int `json:"max_iterations"`
LLMCallTimeout int `json:"llm_call_timeout,omitempty"`
AllowedTools []string `json:"allowed_tools"`
MCPSelectionMode string `json:"mcp_selection_mode"`
MCPServices []string `json:"mcp_services"`
SkillsSelectionMode string `json:"skills_selection_mode"`
SelectedSkills []string `json:"selected_skills"`
KBSelectionMode string `json:"kb_selection_mode"`
KnowledgeBases []string `json:"knowledge_bases"`
RetrieveKBOnlyWhenMentioned bool `json:"retrieve_kb_only_when_mentioned"`
RetainRetrievalHistory bool `json:"retain_retrieval_history"`
ImageUploadEnabled bool `json:"image_upload_enabled"`
VLMModelID string `json:"vlm_model_id"`
AudioUploadEnabled bool `json:"audio_upload_enabled"`
ASRModelID string `json:"asr_model_id"`
ImageStorageProvider string `json:"image_storage_provider"`
SupportedFileTypes []string `json:"supported_file_types"`
DataAnalysisEnabled bool `json:"data_analysis_enabled"`
FAQPriorityEnabled bool `json:"faq_priority_enabled"`
FAQDirectAnswerThreshold float64 `json:"faq_direct_answer_threshold"`
FAQScoreBoost float64 `json:"faq_score_boost"`
WebSearchEnabled bool `json:"web_search_enabled"`
WebSearchMaxResults int `json:"web_search_max_results"`
WebSearchProviderID string `json:"web_search_provider_id,omitempty"`
WebFetchEnabled bool `json:"web_fetch_enabled"`
WebFetchTopN int `json:"web_fetch_top_n,omitempty"`
MultiTurnEnabled bool `json:"multi_turn_enabled"`
HistoryTurns int `json:"history_turns"`
EmbeddingTopK int `json:"embedding_top_k"`
KeywordThreshold float64 `json:"keyword_threshold"`
VectorThreshold float64 `json:"vector_threshold"`
RerankTopK int `json:"rerank_top_k"`
RerankThreshold float64 `json:"rerank_threshold"`
EnableQueryExpansion bool `json:"enable_query_expansion"`
EnableRewrite bool `json:"enable_rewrite"`
RewritePromptSystem string `json:"rewrite_prompt_system"`
RewritePromptUser string `json:"rewrite_prompt_user"`
QueryUnderstandModelID string `json:"query_understand_model_id,omitempty"`
FallbackStrategy string `json:"fallback_strategy"`
FallbackResponse string `json:"fallback_response"`
FallbackPrompt string `json:"fallback_prompt"`
SuggestedPrompts []string `json:"suggested_prompts,omitempty"`
AgentMode string `json:"agent_mode"`
AgentType string `json:"agent_type,omitempty"`
SystemPrompt string `json:"system_prompt"`
SystemPromptID string `json:"system_prompt_id,omitempty"`
ContextTemplate string `json:"context_template"`
ContextTemplateID string `json:"context_template_id,omitempty"`
ModelID string `json:"model_id"`
RerankModelID string `json:"rerank_model_id"`
Temperature float64 `json:"temperature"`
MaxCompletionTokens int `json:"max_completion_tokens"`
Thinking *bool `json:"thinking"`
MaxIterations int `json:"max_iterations"`
LLMCallTimeout int `json:"llm_call_timeout,omitempty"`
AllowedTools []string `json:"allowed_tools"`
MCPSelectionMode string `json:"mcp_selection_mode"`
MCPServices []string `json:"mcp_services"`
SkillsSelectionMode string `json:"skills_selection_mode"`
SelectedSkills []string `json:"selected_skills"`
KBSelectionMode string `json:"kb_selection_mode"`
KnowledgeBases []string `json:"knowledge_bases"`
RetrieveKBOnlyWhenMentioned bool `json:"retrieve_kb_only_when_mentioned"`
RetainRetrievalHistory bool `json:"retain_retrieval_history"`
ImageUploadEnabled bool `json:"image_upload_enabled"`
VLMModelID string `json:"vlm_model_id"`
AudioUploadEnabled bool `json:"audio_upload_enabled"`
ASRModelID string `json:"asr_model_id"`
ImageStorageProvider string `json:"image_storage_provider"`
SupportedFileTypes []string `json:"supported_file_types"`
DataAnalysisEnabled bool `json:"data_analysis_enabled"`
FAQPriorityEnabled bool `json:"faq_priority_enabled"`
FAQDirectAnswerThreshold float64 `json:"faq_direct_answer_threshold"`
FAQScoreBoost float64 `json:"faq_score_boost"`
WebSearchEnabled bool `json:"web_search_enabled"`
WebSearchMaxResults int `json:"web_search_max_results"`
WebSearchProviderID string `json:"web_search_provider_id,omitempty"`
WebFetchEnabled bool `json:"web_fetch_enabled"`
WebFetchTopN int `json:"web_fetch_top_n,omitempty"`
MultiTurnEnabled bool `json:"multi_turn_enabled"`
HistoryTurns int `json:"history_turns"`
EmbeddingTopK int `json:"embedding_top_k"`
KeywordThreshold float64 `json:"keyword_threshold"`
VectorThreshold float64 `json:"vector_threshold"`
RerankTopK int `json:"rerank_top_k"`
RerankThreshold float64 `json:"rerank_threshold"`
EnableQueryExpansion bool `json:"enable_query_expansion"`
EnableRewrite bool `json:"enable_rewrite"`
RewritePromptSystem string `json:"rewrite_prompt_system"`
RewritePromptUser string `json:"rewrite_prompt_user"`
QueryUnderstandModelID string `json:"query_understand_model_id,omitempty"`
FallbackStrategy string `json:"fallback_strategy"`
FallbackResponse string `json:"fallback_response"`
FallbackPrompt string `json:"fallback_prompt"`
SuggestedPrompts []string `json:"suggested_prompts,omitempty"`
}
// CreateAgentRequest represents the request to create an agent.
-22
View File
@@ -583,28 +583,6 @@ func (h *Handler) AgentQA(c *gin.Context) {
agentModeEnabled, reqCtx.customAgent.Config.AgentMode)
}
// Tenant-RBAC gate: block Viewers from running agents whose author
// has cleared RunnableByViewer. The flag exists so an admin can mark
// an agent as "internal tools only" without turning every viewer
// into a contributor. Gated behind cfg.Tenant.EnableRBAC so the
// check is dormant during the rollout window — same pattern as
// middleware/rbac.go.
if agentModeEnabled && reqCtx.customAgent != nil && !reqCtx.customAgent.RunnableByViewer {
role := types.TenantRoleFromContext(reqCtx.ctx)
if !role.HasPermission(types.TenantRoleContributor) {
if h.config != nil && h.config.Tenant.IsRBACEnforced() {
logger.Warnf(reqCtx.ctx,
"[rbac] agent run blocked: viewer cannot run runnable_by_viewer=false agent: agent=%s role=%s",
reqCtx.customAgent.ID, role)
c.Error(errors.NewForbiddenError("Forbidden: this agent is restricted to contributors and above"))
return
}
logger.Warnf(reqCtx.ctx,
"[rbac] agent run would be blocked (logged, not enforced): agent=%s role=%s",
reqCtx.customAgent.ID, role)
}
}
// Sanity gate: agent mode requires a resolved CustomAgent. If we got
// here with agent_enabled=true but agent_id missing/unresolvable, the
// AgentQA service will fail deep inside the async goroutine with a
+2 -3
View File
@@ -503,9 +503,8 @@ func CloneContext(ctx context.Context) context.Context {
// active tenant (PR 2 #1303). Must be propagated for the same
// reason as TenantIDContextKey — any handler that does
// `ctx := logger.CloneContext(c.Request.Context())` and then
// reads role via TenantRoleFromContext (e.g. session/qa.go's
// "viewer cannot run runnable_by_viewer=false agent" gate)
// would otherwise see the type-zero TenantRole and fall back
// reads role via TenantRoleFromContext would otherwise see the
// type-zero TenantRole and fall back
// to Viewer, blocking even Owners.
types.TenantRoleContextKey,
types.LanguageContextKey,
-19
View File
@@ -75,25 +75,6 @@ type CustomAgent struct {
TenantID uint64 `yaml:"tenant_id" json:"tenant_id" gorm:"primaryKey"`
// Created by user ID
CreatedBy string `yaml:"created_by" json:"created_by" gorm:"type:varchar(36)"`
// RunnableByViewer controls whether users with TenantRoleViewer can
// run this agent IN AGENT MODE (i.e. with tools, MCP, sandboxed code
// execution, etc.). Plain RAG / Wiki QA sessions against the same
// agent are unaffected — they don't invoke tools, so restricting
// them buys no security and would just block Viewer consumption of
// published agents. Defaults to true; admins toggle it off for
// agents whose tools should be restricted to contributors and above.
//
// Enforcement lives in handler/session/qa.go's AgentQA, gated behind
// cfg.Tenant.EnableRBAC like every other PR2 check.
//
// Note: no GORM `default:true` tag here. With a plain `bool` field that
// tag causes GORM to treat Go's zero value (false) as "unspecified" and
// silently rewrite it to the DB default (true) on insert — preventing
// admins from ever creating an agent with viewer-runtime disabled. The
// column-level DEFAULT TRUE on the database side still covers rows
// inserted by raw SQL / migrations; GORM Create always writes the
// explicit struct value.
RunnableByViewer bool `yaml:"runnable_by_viewer" json:"runnable_by_viewer"`
// Agent configuration
Config CustomAgentConfig `yaml:"config" json:"config" gorm:"type:json"`