feat: improved codex and auggie default permissions

This commit is contained in:
Carlos Florêncio
2026-01-24 00:19:20 +00:00
committed by GitHub
parent cff2906712
commit 7a7927925e
26 changed files with 514 additions and 189 deletions
+4 -1
View File
@@ -2,6 +2,7 @@ package main
import (
"github.com/kandev/kandev/internal/agent/discovery"
"github.com/kandev/kandev/internal/agent/registry"
agentsettingscontroller "github.com/kandev/kandev/internal/agent/settings/controller"
"github.com/kandev/kandev/internal/common/config"
"github.com/kandev/kandev/internal/common/logger"
@@ -17,7 +18,9 @@ func provideServices(cfg *config.Config, log *logger.Logger, repos *Repositories
if err != nil {
return nil, nil, err
}
agentSettingsController := agentsettingscontroller.NewController(repos.AgentSettings, discoveryRegistry, repos.Task, log)
agentRegistry := registry.NewRegistry(log)
agentRegistry.LoadDefaults()
agentSettingsController := agentsettingscontroller.NewController(repos.AgentSettings, discoveryRegistry, agentRegistry, repos.Task, log)
userSvc := userservice.NewService(repos.User, eventBus, log)
editorSvc := editorservice.NewService(repos.Editor, repos.Task, userSvc)
@@ -88,7 +88,7 @@ func LoadRegistry() (*Registry, error) {
for _, agentCfg := range agentConfigs {
// Convert registry.AgentTypeConfig to discovery.KnownAgent
knownAgent := KnownAgent{
Name: extractAgentName(agentCfg.ID),
Name: agentCfg.ID, // Use ID directly as the short name (e.g., "auggie", "codex")
DisplayName: agentCfg.DisplayName,
SupportsMCP: agentCfg.Discovery.SupportsMCP,
MCPConfigPath: agentCfg.Discovery.MCPConfigPath,
@@ -106,12 +106,6 @@ func LoadRegistry() (*Registry, error) {
return &Registry{adapters: adapters, definitions: definitions}, nil
}
// extractAgentName extracts the short name from an agent ID (e.g., "auggie-agent" -> "auggie")
func extractAgentName(id string) string {
name := strings.TrimSuffix(id, "-agent")
return name
}
func (r *Registry) Definitions() []KnownAgent {
if r == nil {
return nil
@@ -22,6 +22,11 @@ type CommandOptions struct {
Model string // Model to use (appended via ModelFlag if set)
SessionID string // Session ID to resume (appended via SessionConfig.ResumeFlag if not ResumeViaACP)
AutoApprove bool // If true, skip permission flags (auto-approve all tool calls)
// PermissionValues contains the values for each permission setting
// Keys are setting names (e.g., "allow_indexing", "auto_approve")
// Values are the boolean state of each setting
PermissionValues map[string]bool
}
// BuildCommand builds a command slice from agent config and options
@@ -58,6 +63,34 @@ func (cb *CommandBuilder) BuildCommand(agentConfig *registry.AgentTypeConfig, op
}
}
// Apply permission settings that use CLI flags
// Each permission setting can define its own CLI flag via ApplyMethod="cli_flag"
if permSettings := agentConfig.PermissionSettings; permSettings != nil {
for settingName, setting := range permSettings {
// Skip if not supported or not a CLI flag setting
if !setting.Supported || setting.ApplyMethod != "cli_flag" || setting.CLIFlag == "" {
continue
}
// Get the value for this setting from options
value, exists := opts.PermissionValues[settingName]
if !exists {
continue
}
// Only apply if the setting value is true
if value {
if setting.CLIFlagValue != "" {
// Flag with value: "--flag value"
cmd = append(cmd, setting.CLIFlag, setting.CLIFlagValue)
} else {
// Boolean flag: "--flag"
cmd = append(cmd, setting.CLIFlag)
}
}
}
}
return cmd
}
@@ -180,8 +180,16 @@ func (cm *ContainerManager) buildContainerConfig(config ContainerConfig) (docker
// Build command using CommandBuilder
cmdOpts := CommandOptions{
Model: config.Model,
SessionID: config.SessionID,
Model: config.Model,
SessionID: config.SessionID,
PermissionValues: make(map[string]bool),
}
// Get profile settings if available
if config.ProfileInfo != nil {
cmdOpts.AutoApprove = config.ProfileInfo.AutoApprove
cmdOpts.PermissionValues["auto_approve"] = config.ProfileInfo.AutoApprove
cmdOpts.PermissionValues["allow_indexing"] = config.ProfileInfo.AllowIndexing
cmdOpts.PermissionValues["dangerously_skip_permissions"] = config.ProfileInfo.DangerouslySkipPermissions
}
cmd := cm.commandBuilder.BuildCommand(agentConfig, cmdOpts)
@@ -228,9 +228,9 @@ func (m *Manager) createExecution(ctx context.Context, taskID string, info *Work
executionID := uuid.New().String()
agentConfig, err := m.registry.Get(info.AgentID)
if err != nil {
return nil, fmt.Errorf("agent type %q not found in registry: %w", info.AgentID, err)
agentConfig, ok := m.registry.Get(info.AgentID)
if !ok {
return nil, fmt.Errorf("agent type %q not found in registry", info.AgentID)
}
req := &RuntimeCreateRequest{
@@ -387,8 +387,7 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentExecuti
if err != nil {
return nil, fmt.Errorf("failed to resolve agent profile: %w", err)
}
// Map agent name to registry ID (e.g., "auggie" -> "auggie-agent")
agentTypeName = profileInfo.AgentName + "-agent"
agentTypeName = profileInfo.AgentName
m.logger.Info("resolved agent profile",
zap.String("profile_id", req.AgentProfileID),
zap.String("agent_name", profileInfo.AgentName),
@@ -401,9 +400,9 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentExecuti
}
// 2. Get agent config from registry
agentConfig, err := m.registry.Get(agentTypeName)
if err != nil {
return nil, fmt.Errorf("agent type %q not found in registry: %w", agentTypeName, err)
agentConfig, ok := m.registry.Get(agentTypeName)
if !ok {
return nil, fmt.Errorf("agent type %q not found in registry", agentTypeName)
}
if !agentConfig.Enabled {
@@ -475,9 +474,6 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentExecuti
if profileInfo.AutoApprove {
reqWithWorktree.Env["AGENTCTL_AUTO_APPROVE_PERMISSIONS"] = "true"
}
if profileInfo.DangerouslySkipPermissions {
reqWithWorktree.Env["AGENT_DANGEROUSLY_SKIP_PERMISSIONS"] = "true"
}
if profileInfo.Plan != "" {
reqWithWorktree.Env["AGENT_PLAN"] = profileInfo.Plan
}
@@ -544,18 +540,24 @@ func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentExecuti
// Build agent command string for later use with StartAgentProcess
model := ""
autoApprove := false
permissionValues := make(map[string]bool)
if profileInfo != nil {
model = profileInfo.Model
autoApprove = profileInfo.AutoApprove
// Build permission values map from profile info
permissionValues["auto_approve"] = profileInfo.AutoApprove
permissionValues["allow_indexing"] = profileInfo.AllowIndexing
permissionValues["dangerously_skip_permissions"] = profileInfo.DangerouslySkipPermissions
}
// Allow model override from request (for dynamic model switching)
if req.ModelOverride != "" {
model = req.ModelOverride
}
cmdOpts := CommandOptions{
Model: model,
SessionID: req.ACPSessionID,
AutoApprove: autoApprove,
Model: model,
SessionID: req.ACPSessionID,
AutoApprove: autoApprove,
PermissionValues: permissionValues,
}
execution.AgentCommand = m.commandBuilder.BuildCommandString(agentConfig, cmdOpts)
@@ -619,8 +621,22 @@ func (m *Manager) StartAgentProcess(ctx context.Context, executionID string) err
env["TASK_DESCRIPTION"] = taskDescription
}
// Determine approval policy for Codex
// If auto_approve is true, use "never" (no approval needed)
// Otherwise use "untrusted" (request approval for all actions)
approvalPolicy := ""
if execution.AgentProfileID != "" && m.profileResolver != nil {
if profileInfo, err := m.profileResolver.ResolveProfile(ctx, execution.AgentProfileID); err == nil {
if profileInfo.AutoApprove {
approvalPolicy = "never"
} else {
approvalPolicy = "untrusted"
}
}
}
// Configure the agent command
if err := execution.agentctl.ConfigureAgent(ctx, execution.AgentCommand, env); err != nil {
if err := execution.agentctl.ConfigureAgent(ctx, execution.AgentCommand, env, approvalPolicy); err != nil {
return fmt.Errorf("failed to configure agent: %w", err)
}
@@ -794,10 +810,9 @@ func (m *Manager) getAgentConfigForExecution(execution *AgentExecution) (*regist
return nil, fmt.Errorf("failed to resolve profile: %w", err)
}
// Map agent name to registry ID (e.g., "auggie" -> "auggie-agent")
agentTypeName := profileInfo.AgentName + "-agent"
agentConfig, err := m.registry.Get(agentTypeName)
if err != nil {
agentTypeName := profileInfo.AgentName
agentConfig, ok := m.registry.Get(agentTypeName)
if !ok {
return nil, fmt.Errorf("agent type not found: %s", agentTypeName)
}
@@ -39,6 +39,7 @@ func (r *StoreProfileResolver) ResolveProfile(ctx context.Context, profileID str
Model: profile.Model,
AutoApprove: profile.AutoApprove,
DangerouslySkipPermissions: profile.DangerouslySkipPermissions,
AllowIndexing: profile.AllowIndexing,
Plan: profile.Plan,
}, nil
}
@@ -110,6 +110,7 @@ type AgentProfileInfo struct {
Model string
AutoApprove bool
DangerouslySkipPermissions bool
AllowIndexing bool
Plan string
}
@@ -129,7 +130,7 @@ type WorkspaceInfo struct {
SessionID string // Task session ID (from task_sessions table)
WorkspacePath string // Path to the workspace/repository
AgentProfileID string // Optional - agent profile for the task
AgentID string // Agent type ID (e.g., "auggie-agent") - required for runtime creation
AgentID string // Agent type ID (e.g., "auggie", "codex") - required for runtime creation
ACPSessionID string // Agent's session ID for conversation resumption (from session metadata)
}
@@ -3,26 +3,41 @@
"version": "1.0",
"agents": [
{
"id": "auggie-agent",
"id": "auggie",
"name": "Augment Coding Agent",
"display_name": "Auggie",
"description": "Auggie CLI-powered autonomous coding agent.",
"enabled": true,
"image": "kandev/augment-agent",
"tag": "latest",
"cmd": ["auggie", "--acp"],
"cmd": [
"auggie",
"--acp"
],
"working_dir": "/workspace",
"required_env": ["AUGMENT_SESSION_AUTH"],
"required_env": [
"AUGMENT_SESSION_AUTH"
],
"env": {},
"mounts": [
{"source": "{workspace}", "target": "/workspace", "read_only": false}
{
"source": "{workspace}",
"target": "/workspace",
"read_only": false
}
],
"resource_limits": {
"memory_mb": 4096,
"cpu_cores": 2.0,
"timeout_seconds": 3600
},
"capabilities": ["code_generation", "code_review", "refactoring", "testing", "shell_execution"],
"capabilities": [
"code_generation",
"code_review",
"refactoring",
"testing",
"shell_execution"
],
"protocol": "acp",
"protocol_config": {},
"model_flag": "--model {model}",
@@ -43,6 +58,24 @@
"remove-files"
]
},
"permission_settings": {
"auto_approve": {
"supported": true,
"default": true,
"label": "Auto-approve",
"description": "Automatically approve tool calls",
"apply_method": "",
"cli_flag": ""
},
"allow_indexing": {
"supported": true,
"default": true,
"label": "Allow indexing",
"description": "Enable workspace indexing without confirmation",
"apply_method": "cli_flag",
"cli_flag": "--allow-indexing"
}
},
"discovery": {
"supports_mcp": true,
"mcp_config_path": {
@@ -51,9 +84,15 @@
"macos": []
},
"installation_path": {
"linux": ["~/.augment/.auggie.json"],
"windows": ["~/.augment/.auggie.json"],
"macos": ["~/.augment/.auggie.json"]
"linux": [
"~/.augment/.auggie.json"
],
"windows": [
"~/.augment/.auggie.json"
],
"macos": [
"~/.augment/.auggie.json"
]
},
"discovery_capabilities": {
"supports_session_resume": false,
@@ -110,26 +149,39 @@
}
},
{
"id": "codex-agent",
"id": "codex",
"name": "OpenAI Codex Agent",
"display_name": "Codex",
"description": "OpenAI Codex CLI-powered autonomous coding agent using the Codex app-server protocol.",
"enabled": true,
"image": "kandev/codex-agent",
"tag": "latest",
"cmd": ["codex", "app-server"],
"cmd": [
"codex",
"app-server"
],
"working_dir": "/workspace",
"required_env": [],
"env": {},
"mounts": [
{"source": "{workspace}", "target": "/workspace", "read_only": false}
{
"source": "{workspace}",
"target": "/workspace",
"read_only": false
}
],
"resource_limits": {
"memory_mb": 4096,
"cpu_cores": 2.0,
"timeout_seconds": 3600
},
"capabilities": ["code_generation", "code_review", "refactoring", "testing", "shell_execution"],
"capabilities": [
"code_generation",
"code_review",
"refactoring",
"testing",
"shell_execution"
],
"protocol": "codex",
"protocol_config": {},
"model_flag": "-c model=\"{model}\"",
@@ -145,6 +197,16 @@
"permission_flag": "",
"tools_requiring_permission": []
},
"permission_settings": {
"auto_approve": {
"supported": true,
"default": true,
"label": "Auto-approve",
"description": "Skip approval requests from Codex",
"apply_method": "acp",
"cli_flag": ""
}
},
"discovery": {
"supports_mcp": true,
"mcp_config_path": {
@@ -153,9 +215,15 @@
"macos": []
},
"installation_path": {
"linux": ["~/.codex/auth.json"],
"windows": ["~/.codex/auth.json"],
"macos": ["~/.codex/auth.json"]
"linux": [
"~/.codex/auth.json"
],
"windows": [
"~/.codex/auth.json"
],
"macos": [
"~/.codex/auth.json"
]
},
"discovery_capabilities": {
"supports_session_resume": false,
@@ -203,4 +271,3 @@
}
]
}
@@ -54,6 +54,9 @@ type AgentTypeConfig struct {
// Permission configuration
PermissionConfig PermissionConfig `json:"permission_config,omitempty"` // How to handle tool permissions
// Permission settings metadata (what settings are supported for this agent)
PermissionSettings map[string]PermissionSetting `json:"permission_settings,omitempty"` // Supported permission settings
// Discovery configuration (for detecting agent installation)
Discovery DiscoveryConfig `json:"discovery,omitempty"` // Discovery metadata for agent detection
@@ -112,6 +115,33 @@ type PermissionConfig struct {
ToolsRequiringPermission []string `json:"tools_requiring_permission,omitempty"`
}
// PermissionSetting defines metadata for a permission setting option
type PermissionSetting struct {
// Supported indicates whether this setting is available for the agent
Supported bool `json:"supported"`
// Default is the default value for this setting when creating new profiles
Default bool `json:"default"`
// Label is the display label for the UI
Label string `json:"label"`
// Description explains what the setting does
Description string `json:"description"`
// ApplyMethod defines how this setting is applied: "cli_flag", "acp", "config", or empty (none)
ApplyMethod string `json:"apply_method,omitempty"`
// CLIFlag is the CLI flag to use when ApplyMethod is "cli_flag"
// Example: "--allow-indexing" for auggie
CLIFlag string `json:"cli_flag,omitempty"`
// CLIFlagValue is an optional value template for the CLI flag
// If empty and the setting is true, just appends the flag
// If set, appends "flag value" (e.g., "--approval-policy never")
CLIFlagValue string `json:"cli_flag_value,omitempty"`
}
// MountTemplate defines a mount with template support
type MountTemplate struct {
Source string `json:"source"` // Can use {workspace}, {task_id}
@@ -253,26 +283,22 @@ func (r *Registry) Unregister(id string) error {
}
// Get returns an agent type configuration
func (r *Registry) Get(id string) (*AgentTypeConfig, error) {
func (r *Registry) Get(id string) (*AgentTypeConfig, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
config, exists := r.agents[id]
if !exists {
return nil, fmt.Errorf("agent type %q not found", id)
}
return config, nil
return config, exists
}
// GetDefault returns the default agent type configuration.
// It tries "augment-agent" first, then falls back to the first enabled agent.
// It tries "auggie" first, then falls back to the first enabled agent.
func (r *Registry) GetDefault() (*AgentTypeConfig, error) {
r.mu.RLock()
defer r.mu.RUnlock()
// Try augment-agent first
if config, exists := r.agents["augment-agent"]; exists && config.Enabled {
// Try auggie first
if config, exists := r.agents["auggie"]; exists && config.Enabled {
return config, nil
}
@@ -127,18 +127,18 @@ func TestRegistry_Get(t *testing.T) {
_ = reg.Register(config)
// Test successful get
got, err := reg.Get("test-agent")
if err != nil {
t.Fatalf("unexpected error: %v", err)
got, ok := reg.Get("test-agent")
if !ok {
t.Fatal("expected agent to be found")
}
if got.ID != config.ID {
t.Errorf("expected ID %q, got %q", config.ID, got.ID)
}
// Test not found
_, err = reg.Get("non-existent")
if err == nil {
t.Error("expected error for non-existent agent type")
_, ok = reg.Get("non-existent")
if ok {
t.Error("expected agent to not be found")
}
}
@@ -10,6 +10,7 @@ import (
"github.com/kandev/kandev/internal/agent/discovery"
"github.com/kandev/kandev/internal/agent/mcpconfig"
"github.com/kandev/kandev/internal/agent/registry"
"github.com/kandev/kandev/internal/agent/settings/dto"
"github.com/kandev/kandev/internal/agent/settings/models"
"github.com/kandev/kandev/internal/agent/settings/store"
@@ -30,6 +31,7 @@ const defaultAgentProfileName = "default"
type Controller struct {
repo store.Repository
discovery *discovery.Registry
agentRegistry interface{ Get(id string) (*registry.AgentTypeConfig, bool) }
sessionChecker SessionChecker
mcpService *mcpconfig.Service
logger *logger.Logger
@@ -39,10 +41,11 @@ type SessionChecker interface {
HasActiveTaskSessionsByAgentProfile(ctx context.Context, agentProfileID string) (bool, error)
}
func NewController(repo store.Repository, discoveryRegistry *discovery.Registry, sessionChecker SessionChecker, log *logger.Logger) *Controller {
func NewController(repo store.Repository, discoveryRegistry *discovery.Registry, agentRegistry interface{ Get(id string) (*registry.AgentTypeConfig, bool) }, sessionChecker SessionChecker, log *logger.Logger) *Controller {
return &Controller{
repo: repo,
discovery: discoveryRegistry,
agentRegistry: agentRegistry,
sessionChecker: sessionChecker,
mcpService: mcpconfig.NewService(repo),
logger: log.WithFields(zap.String("component", "agent-settings-controller")),
@@ -106,14 +109,31 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl
})
}
// Convert permission settings
var permissionSettings map[string]dto.PermissionSettingDTO
if agentConfig, ok := c.agentRegistry.Get(def.Name); ok && agentConfig.PermissionSettings != nil {
permissionSettings = make(map[string]dto.PermissionSettingDTO, len(agentConfig.PermissionSettings))
for key, setting := range agentConfig.PermissionSettings {
permissionSettings[key] = dto.PermissionSettingDTO{
Supported: setting.Supported,
Default: setting.Default,
Label: setting.Label,
Description: setting.Description,
ApplyMethod: setting.ApplyMethod,
CLIFlag: setting.CLIFlag,
CLIFlagValue: setting.CLIFlagValue,
}
}
}
payload = append(payload, dto.AvailableAgentDTO{
Name: availability.Name,
DisplayName: displayName,
SupportsMCP: availability.SupportsMCP,
MCPConfigPath: availability.MCPConfigPath,
InstallationPaths: availability.InstallationPaths,
Available: availability.Available,
MatchedPath: availability.MatchedPath,
Name: availability.Name,
DisplayName: displayName,
SupportsMCP: availability.SupportsMCP,
MCPConfigPath: availability.MCPConfigPath,
InstallationPaths: availability.InstallationPaths,
Available: availability.Available,
MatchedPath: availability.MatchedPath,
Capabilities: dto.AgentCapabilitiesDTO{
SupportsSessionResume: def.Capabilities.SupportsSessionResume,
SupportsShell: def.Capabilities.SupportsShell,
@@ -123,7 +143,8 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl
DefaultModel: def.ModelConfig.DefaultModel,
AvailableModels: modelEntries,
},
UpdatedAt: now,
PermissionSettings: permissionSettings,
UpdatedAt: now,
})
}
return &dto.ListAvailableAgentsResponse{Agents: payload, Total: len(payload)}, nil
@@ -200,12 +221,29 @@ func (c *Controller) EnsureInitialAgentProfiles(ctx context.Context) error {
if len(profiles) > 0 {
continue
}
// Get agent config to read permission settings defaults
var autoApprove, allowIndexing, skipPermissions bool
if agentConfig, ok := c.agentRegistry.Get(agent.Name); ok && agentConfig.PermissionSettings != nil {
if setting, exists := agentConfig.PermissionSettings["auto_approve"]; exists {
autoApprove = setting.Default
}
if setting, exists := agentConfig.PermissionSettings["allow_indexing"]; exists {
allowIndexing = setting.Default
}
if setting, exists := agentConfig.PermissionSettings["dangerously_skip_permissions"]; exists {
skipPermissions = setting.Default
}
}
defaultProfile := &models.AgentProfile{
AgentID: agent.ID,
Name: defaultAgentProfileName,
Model: defaultModel,
Plan: "",
AgentDisplayName: displayName,
AgentID: agent.ID,
Name: defaultAgentProfileName,
Model: defaultModel,
Plan: "",
AgentDisplayName: displayName,
AutoApprove: autoApprove,
AllowIndexing: allowIndexing,
DangerouslySkipPermissions: skipPermissions,
}
if err := c.repo.CreateAgentProfile(ctx, defaultProfile); err != nil {
return err
@@ -401,6 +439,7 @@ type CreateProfileRequest struct {
Model string
AutoApprove bool
DangerouslySkipPermissions bool
AllowIndexing bool
Plan string
}
@@ -424,6 +463,7 @@ func (c *Controller) CreateProfile(ctx context.Context, req CreateProfileRequest
Model: req.Model,
AutoApprove: req.AutoApprove,
DangerouslySkipPermissions: req.DangerouslySkipPermissions,
AllowIndexing: req.AllowIndexing,
Plan: req.Plan,
}
if err := c.repo.CreateAgentProfile(ctx, profile); err != nil {
@@ -439,6 +479,7 @@ type UpdateProfileRequest struct {
Model *string
AutoApprove *bool
DangerouslySkipPermissions *bool
AllowIndexing *bool
Plan *string
}
@@ -463,6 +504,9 @@ func (c *Controller) UpdateProfile(ctx context.Context, req UpdateProfileRequest
if req.DangerouslySkipPermissions != nil {
profile.DangerouslySkipPermissions = *req.DangerouslySkipPermissions
}
if req.AllowIndexing != nil {
profile.AllowIndexing = *req.AllowIndexing
}
if req.Plan != nil {
profile.Plan = *req.Plan
}
@@ -526,6 +570,7 @@ func toProfileDTO(profile *models.AgentProfile) dto.AgentProfileDTO {
Model: profile.Model,
AutoApprove: profile.AutoApprove,
DangerouslySkipPermissions: profile.DangerouslySkipPermissions,
AllowIndexing: profile.AllowIndexing,
Plan: profile.Plan,
CreatedAt: profile.CreatedAt,
UpdatedAt: profile.UpdatedAt,
+22 -10
View File
@@ -14,6 +14,7 @@ type AgentProfileDTO struct {
Model string `json:"model"`
AutoApprove bool `json:"auto_approve"`
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
AllowIndexing bool `json:"allow_indexing"`
Plan string `json:"plan"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -68,17 +69,28 @@ type ModelConfigDTO struct {
AvailableModels []ModelEntryDTO `json:"available_models"`
}
type PermissionSettingDTO struct {
Supported bool `json:"supported"`
Default bool `json:"default"`
Label string `json:"label"`
Description string `json:"description"`
ApplyMethod string `json:"apply_method,omitempty"`
CLIFlag string `json:"cli_flag,omitempty"`
CLIFlagValue string `json:"cli_flag_value,omitempty"`
}
type AvailableAgentDTO struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
SupportsMCP bool `json:"supports_mcp"`
MCPConfigPath string `json:"mcp_config_path,omitempty"`
InstallationPaths []string `json:"installation_paths,omitempty"`
Available bool `json:"available"`
MatchedPath string `json:"matched_path,omitempty"`
Capabilities AgentCapabilitiesDTO `json:"capabilities"`
ModelConfig ModelConfigDTO `json:"model_config"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
DisplayName string `json:"display_name"`
SupportsMCP bool `json:"supports_mcp"`
MCPConfigPath string `json:"mcp_config_path,omitempty"`
InstallationPaths []string `json:"installation_paths,omitempty"`
Available bool `json:"available"`
MatchedPath string `json:"matched_path,omitempty"`
Capabilities AgentCapabilitiesDTO `json:"capabilities"`
ModelConfig ModelConfigDTO `json:"model_config"`
PermissionSettings map[string]PermissionSettingDTO `json:"permission_settings,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListAvailableAgentsResponse struct {
@@ -282,6 +282,7 @@ type createProfileRequest struct {
Model string `json:"model"`
AutoApprove bool `json:"auto_approve"`
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
AllowIndexing bool `json:"allow_indexing"`
Plan string `json:"plan"`
}
@@ -301,6 +302,7 @@ func (h *Handlers) httpCreateProfile(c *gin.Context) {
Model: body.Model,
AutoApprove: body.AutoApprove,
DangerouslySkipPermissions: body.DangerouslySkipPermissions,
AllowIndexing: body.AllowIndexing,
Plan: body.Plan,
})
if err != nil {
@@ -322,6 +324,7 @@ type updateProfileRequest struct {
Model *string `json:"model,omitempty"`
AutoApprove *bool `json:"auto_approve,omitempty"`
DangerouslySkipPermissions *bool `json:"dangerously_skip_permissions,omitempty"`
AllowIndexing *bool `json:"allow_indexing,omitempty"`
Plan *string `json:"plan,omitempty"`
}
@@ -341,6 +344,7 @@ func (h *Handlers) httpUpdateProfile(c *gin.Context) {
Model: body.Model,
AutoApprove: body.AutoApprove,
DangerouslySkipPermissions: body.DangerouslySkipPermissions,
AllowIndexing: body.AllowIndexing,
Plan: body.Plan,
})
if err != nil {
@@ -20,6 +20,7 @@ type AgentProfile struct {
Model string `json:"model"`
AutoApprove bool `json:"auto_approve"`
DangerouslySkipPermissions bool `json:"dangerously_skip_permissions"`
AllowIndexing bool `json:"allow_indexing"`
Plan string `json:"plan"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -56,6 +56,7 @@ func (r *sqliteRepository) initSchema() error {
model TEXT NOT NULL CHECK(model != ''),
auto_approve INTEGER NOT NULL DEFAULT 0,
dangerously_skip_permissions INTEGER NOT NULL DEFAULT 0,
allow_indexing INTEGER NOT NULL DEFAULT 1,
plan TEXT DEFAULT '',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
@@ -247,10 +248,10 @@ func (r *sqliteRepository) CreateAgentProfile(ctx context.Context, profile *mode
profile.CreatedAt = now
profile.UpdatedAt = now
_, err := r.db.ExecContext(ctx, `
INSERT INTO agent_profiles (id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO agent_profiles (id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, allow_indexing, plan, created_at, updated_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, profile.ID, profile.AgentID, profile.Name, profile.AgentDisplayName, profile.Model, boolToInt(profile.AutoApprove),
boolToInt(profile.DangerouslySkipPermissions), profile.Plan, profile.CreatedAt, profile.UpdatedAt, profile.DeletedAt)
boolToInt(profile.DangerouslySkipPermissions), boolToInt(profile.AllowIndexing), profile.Plan, profile.CreatedAt, profile.UpdatedAt, profile.DeletedAt)
return err
}
@@ -258,10 +259,10 @@ func (r *sqliteRepository) UpdateAgentProfile(ctx context.Context, profile *mode
profile.UpdatedAt = time.Now().UTC()
result, err := r.db.ExecContext(ctx, `
UPDATE agent_profiles
SET name = ?, agent_display_name = ?, model = ?, auto_approve = ?, dangerously_skip_permissions = ?, plan = ?, updated_at = ?
SET name = ?, agent_display_name = ?, model = ?, auto_approve = ?, dangerously_skip_permissions = ?, allow_indexing = ?, plan = ?, updated_at = ?
WHERE id = ? AND deleted_at IS NULL
`, profile.Name, profile.AgentDisplayName, profile.Model, boolToInt(profile.AutoApprove),
boolToInt(profile.DangerouslySkipPermissions), profile.Plan, profile.UpdatedAt, profile.ID)
boolToInt(profile.DangerouslySkipPermissions), boolToInt(profile.AllowIndexing), profile.Plan, profile.UpdatedAt, profile.ID)
if err != nil {
return err
}
@@ -289,7 +290,7 @@ func (r *sqliteRepository) DeleteAgentProfile(ctx context.Context, id string) er
func (r *sqliteRepository) GetAgentProfile(ctx context.Context, id string) (*models.AgentProfile, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at
SELECT id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, allow_indexing, plan, created_at, updated_at, deleted_at
FROM agent_profiles WHERE id = ? AND deleted_at IS NULL
`, id)
return scanAgentProfile(row)
@@ -297,7 +298,7 @@ func (r *sqliteRepository) GetAgentProfile(ctx context.Context, id string) (*mod
func (r *sqliteRepository) ListAgentProfiles(ctx context.Context, agentID string) ([]*models.AgentProfile, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, plan, created_at, updated_at, deleted_at
SELECT id, agent_id, name, agent_display_name, model, auto_approve, dangerously_skip_permissions, allow_indexing, plan, created_at, updated_at, deleted_at
FROM agent_profiles WHERE agent_id = ? AND deleted_at IS NULL ORDER BY created_at DESC
`, agentID)
if err != nil {
@@ -348,6 +349,7 @@ func scanAgentProfile(scanner interface {
profile := &models.AgentProfile{}
var autoApprove int
var skipPermissions int
var allowIndexing int
if err := scanner.Scan(
&profile.ID,
&profile.AgentID,
@@ -356,6 +358,7 @@ func scanAgentProfile(scanner interface {
&profile.Model,
&autoApprove,
&skipPermissions,
&allowIndexing,
&profile.Plan,
&profile.CreatedAt,
&profile.UpdatedAt,
@@ -365,6 +368,7 @@ func scanAgentProfile(scanner interface {
}
profile.AutoApprove = autoApprove == 1
profile.DangerouslySkipPermissions = skipPermissions == 1
profile.AllowIndexing = allowIndexing == 1
return profile, nil
}
@@ -99,14 +99,16 @@ func (c *Client) GetStatus(ctx context.Context) (*StatusResponse, error) {
return &status, nil
}
// ConfigureAgent configures the agent command. Must be called before Start().
func (c *Client) ConfigureAgent(ctx context.Context, command string, env map[string]string) error {
// ConfigureAgent configures the agent command and optional approval policy. Must be called before Start().
func (c *Client) ConfigureAgent(ctx context.Context, command string, env map[string]string, approvalPolicy string) error {
payload := struct {
Command string `json:"command"`
Env map[string]string `json:"env,omitempty"`
Command string `json:"command"`
Env map[string]string `json:"env,omitempty"`
ApprovalPolicy string `json:"approval_policy,omitempty"`
}{
Command: command,
Env: env,
Command: command,
Env: env,
ApprovalPolicy: approvalPolicy,
}
body, err := json.Marshal(payload)
@@ -180,8 +180,9 @@ func (s *Server) handleStart(c *gin.Context) {
// AgentConfigure request/response - configures the agent command before starting
type AgentConfigureRequest struct {
Command string `json:"command"`
Env map[string]string `json:"env,omitempty"`
Command string `json:"command"`
Env map[string]string `json:"env,omitempty"`
ApprovalPolicy string `json:"approval_policy,omitempty"` // For Codex: "untrusted", "on-failure", "on-request", "never"
}
type AgentConfigureResponse struct {
@@ -208,7 +209,7 @@ func (s *Server) handleAgentConfigure(c *gin.Context) {
return
}
if err := s.procMgr.Configure(req.Command, req.Env); err != nil {
if err := s.procMgr.Configure(req.Command, req.Env, req.ApprovalPolicy); err != nil {
s.logger.Error("failed to configure agent", zap.Error(err), zap.String("command", req.Command))
c.JSON(http.StatusInternalServerError, AgentConfigureResponse{
Success: false,
@@ -217,7 +218,7 @@ func (s *Server) handleAgentConfigure(c *gin.Context) {
return
}
s.logger.Info("agent configured", zap.String("command", req.Command))
s.logger.Info("agent configured", zap.String("command", req.Command), zap.String("approval_policy", req.ApprovalPolicy))
c.JSON(http.StatusOK, AgentConfigureResponse{
Success: true,
Message: "agent configured",
@@ -345,7 +345,7 @@ func lookupEnvValue(env []string, key string) string {
// Configure sets the agent command and optional environment variables.
// This must be called before Start() if the instance was created without a command.
func (m *Manager) Configure(command string, env map[string]string) error {
func (m *Manager) Configure(command string, env map[string]string, approvalPolicy string) error {
m.startMu.Lock()
defer m.startMu.Unlock()
@@ -366,6 +366,11 @@ func (m *Manager) Configure(command string, env map[string]string) error {
m.cfg.AgentCommand = command
m.cfg.AgentArgs = args
// Set approval policy if provided (for Codex)
if approvalPolicy != "" {
m.cfg.ApprovalPolicy = approvalPolicy
}
// Merge additional env vars
if len(env) > 0 {
for k, v := range env {
@@ -376,6 +381,7 @@ func (m *Manager) Configure(command string, env map[string]string) error {
m.logger.Info("agent configured",
zap.String("command", command),
zap.Strings("args", args),
zap.String("approval_policy", m.cfg.ApprovalPolicy),
zap.Int("env_count", len(env)))
return nil
+3 -1
View File
@@ -50,6 +50,7 @@ export async function createAgentAction(payload: {
model: string;
auto_approve: boolean;
dangerously_skip_permissions: boolean;
allow_indexing: boolean;
plan: string;
}>;
}): Promise<Agent> {
@@ -80,6 +81,7 @@ export async function createAgentProfileAction(
model: string;
auto_approve: boolean;
dangerously_skip_permissions: boolean;
allow_indexing: boolean;
plan: string;
}
): Promise<AgentProfile> {
@@ -91,7 +93,7 @@ export async function createAgentProfileAction(
export async function updateAgentProfileAction(
id: string,
payload: Partial<Pick<AgentProfile, 'name' | 'model' | 'auto_approve' | 'dangerously_skip_permissions' | 'plan'>>
payload: Partial<Pick<AgentProfile, 'name' | 'model' | 'auto_approve' | 'dangerously_skip_permissions' | 'allow_indexing' | 'plan'>>
): Promise<AgentProfile> {
return fetchJson<AgentProfile>(`${apiBaseUrl}/api/v1/agent-profiles/${id}`, {
method: 'PATCH',
+98 -48
View File
@@ -28,13 +28,14 @@ import {
updateAgentProfileAction,
updateAgentProfileMcpConfigAction,
} from '@/app/actions/agents';
import type { Agent, AgentDiscovery, AgentProfile, McpServerDef, AvailableAgent } from '@/lib/types/http';
import type { Agent, AgentDiscovery, AgentProfile, McpServerDef, AvailableAgent, PermissionSetting } from '@/lib/types/http';
import { generateUUID } from '@/lib/utils';
import { useAppStore } from '@/components/state-provider';
import { useAvailableAgents } from '@/hooks/domains/settings/use-available-agents';
import { ProfileMcpConfigCard } from './profile-mcp-config-card';
type DraftProfile = AgentProfile & {
type DraftProfile = Omit<AgentProfile, 'allow_indexing'> & {
allow_indexing?: boolean;
isNew?: boolean;
mcp_config?: {
enabled: boolean;
@@ -59,14 +60,20 @@ type AgentSetupFormProps = {
onToastError: (error: unknown) => void;
};
const createDraftProfile = (agentId: string, agentDisplayName: string, defaultModel: string): DraftProfile => ({
const createDraftProfile = (
agentId: string,
agentDisplayName: string,
defaultModel: string,
permissionSettings?: Record<string, PermissionSetting>
): DraftProfile => ({
id: `draft-${generateUUID()}`,
agent_id: agentId,
name: '',
agent_display_name: agentDisplayName,
model: defaultModel,
auto_approve: false,
dangerously_skip_permissions: false,
auto_approve: permissionSettings?.auto_approve?.default ?? false,
dangerously_skip_permissions: permissionSettings?.dangerously_skip_permissions?.default ?? false,
allow_indexing: permissionSettings?.allow_indexing?.default ?? false,
plan: '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
@@ -81,13 +88,18 @@ const cloneAgent = (agent: Agent): DraftAgent => ({
profiles: agent.profiles.map((profile) => ({ ...profile })) as DraftProfile[],
});
const ensureProfiles = (agent: DraftAgent, agentDisplayName: string, defaultModel: string): DraftAgent => {
const ensureProfiles = (
agent: DraftAgent,
agentDisplayName: string,
defaultModel: string,
permissionSettings?: Record<string, PermissionSetting>
): DraftAgent => {
if (agent.profiles.length > 0) {
return agent;
}
return {
...agent,
profiles: [createDraftProfile(agent.id, agentDisplayName, defaultModel)],
profiles: [createDraftProfile(agent.id, agentDisplayName, defaultModel, permissionSettings)],
};
};
@@ -111,12 +123,19 @@ function AgentSetupForm({
return draftAgent.profiles.some((profile) => Boolean(profile.mcp_config?.error));
}, [draftAgent.profiles]);
// Get model config for the current agent
const currentAgentModelConfig = useMemo(() => {
const agent = availableAgents.find((item: AvailableAgent) => item.name === draftAgent.name);
return agent?.model_config ?? { default_model: '', available_models: [] };
// Get model config and permission settings for the current agent
const currentAvailableAgent = useMemo(() => {
return availableAgents.find((item: AvailableAgent) => item.name === draftAgent.name) ?? null;
}, [availableAgents, draftAgent.name]);
const currentAgentModelConfig = useMemo(() => {
return currentAvailableAgent?.model_config ?? { default_model: '', available_models: [] };
}, [currentAvailableAgent]);
const permissionSettings = useMemo(() => {
return currentAvailableAgent?.permission_settings ?? {};
}, [currentAvailableAgent]);
const isProfileDirty = (draft: DraftProfile, saved?: AgentProfile) => {
if (!saved) return true;
return (
@@ -124,6 +143,7 @@ function AgentSetupForm({
draft.model !== saved.model ||
draft.auto_approve !== saved.auto_approve ||
draft.dangerously_skip_permissions !== saved.dangerously_skip_permissions ||
draft.allow_indexing !== saved.allow_indexing ||
draft.plan !== saved.plan
);
};
@@ -169,7 +189,7 @@ function AgentSetupForm({
...current,
profiles: [
...current.profiles,
{ ...createDraftProfile(current.id, resolveDisplayName(current.name), currentAgentModelConfig.default_model), id: draftId },
{ ...createDraftProfile(current.id, resolveDisplayName(current.name), currentAgentModelConfig.default_model, permissionSettings), id: draftId },
],
}));
setNewProfileId(draftId);
@@ -183,7 +203,7 @@ function AgentSetupForm({
profiles:
remaining.length > 0
? remaining
: [createDraftProfile(current.id, resolveDisplayName(current.name), currentAgentModelConfig.default_model)],
: [createDraftProfile(current.id, resolveDisplayName(current.name), currentAgentModelConfig.default_model, permissionSettings)],
};
});
if (newProfileId === profileId) {
@@ -263,6 +283,7 @@ function AgentSetupForm({
model: profile.model,
auto_approve: profile.auto_approve,
dangerously_skip_permissions: profile.dangerously_skip_permissions,
allow_indexing: profile.allow_indexing ?? false,
plan: profile.plan,
})),
});
@@ -316,7 +337,7 @@ function AgentSetupForm({
});
}
upsertAgent(created);
setDraftAgent(ensureProfiles(cloneAgent(created), resolveDisplayName(created.name), currentAgentModelConfig.default_model));
setDraftAgent(ensureProfiles(cloneAgent(created), resolveDisplayName(created.name), currentAgentModelConfig.default_model, permissionSettings));
router.replace(`/settings/agents/${encodeURIComponent(created.name)}`);
} else {
const agentPatch: { workspace_id?: string | null; mcp_config_path?: string | null } = {};
@@ -340,6 +361,7 @@ function AgentSetupForm({
model: profile.model,
auto_approve: profile.auto_approve,
dangerously_skip_permissions: profile.dangerously_skip_permissions,
allow_indexing: profile.allow_indexing ?? false,
plan: profile.plan,
});
if (profile.mcp_config && profile.mcp_config.dirty && profile.mcp_config.servers.trim()) {
@@ -362,6 +384,7 @@ function AgentSetupForm({
model: profile.model,
auto_approve: profile.auto_approve,
dangerously_skip_permissions: profile.dangerously_skip_permissions,
allow_indexing: profile.allow_indexing ?? false,
plan: profile.plan,
});
nextProfiles.push(updatedProfile);
@@ -383,7 +406,7 @@ function AgentSetupForm({
profiles: nextProfiles,
};
upsertAgent(nextAgent);
setDraftAgent(ensureProfiles(cloneAgent(nextAgent), resolveDisplayName(nextAgent.name), currentAgentModelConfig.default_model));
setDraftAgent(ensureProfiles(cloneAgent(nextAgent), resolveDisplayName(nextAgent.name), currentAgentModelConfig.default_model, permissionSettings));
}
setSaveStatus('success');
} catch (error) {
@@ -444,9 +467,11 @@ function AgentSetupForm({
placeholder="Default profile"
/>
</div>
<Button size="sm" variant="ghost" onClick={() => handleRemoveProfile(profile.id)}>
Remove
</Button>
{draftAgent.profiles.length > 1 && (
<Button size="sm" variant="ghost" onClick={() => handleRemoveProfile(profile.id)}>
Remove
</Button>
)}
</div>
<div className="space-y-2">
@@ -493,35 +518,56 @@ function AgentSetupForm({
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>Auto-approve</Label>
<p className="text-xs text-muted-foreground">
Automatically approve tool calls.
</p>
{permissionSettings.auto_approve?.supported && (
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>{permissionSettings.auto_approve.label}</Label>
<p className="text-xs text-muted-foreground">
{permissionSettings.auto_approve.description}
</p>
</div>
<Switch
checked={profile.auto_approve}
onCheckedChange={(checked) =>
handleProfileChange(profile.id, { auto_approve: checked })
}
/>
</div>
<Switch
checked={profile.auto_approve}
onCheckedChange={(checked) =>
handleProfileChange(profile.id, { auto_approve: checked })
}
/>
</div>
)}
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>Skip permissions</Label>
<p className="text-xs text-muted-foreground">
Skip permission checks when running tools.
</p>
{permissionSettings.dangerously_skip_permissions?.supported && (
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>{permissionSettings.dangerously_skip_permissions.label}</Label>
<p className="text-xs text-muted-foreground">
{permissionSettings.dangerously_skip_permissions.description}
</p>
</div>
<Switch
checked={profile.dangerously_skip_permissions}
onCheckedChange={(checked) =>
handleProfileChange(profile.id, { dangerously_skip_permissions: checked })
}
/>
</div>
<Switch
checked={profile.dangerously_skip_permissions}
onCheckedChange={(checked) =>
handleProfileChange(profile.id, { dangerously_skip_permissions: checked })
}
/>
</div>
)}
{permissionSettings.allow_indexing?.supported && (
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>{permissionSettings.allow_indexing.label}</Label>
<p className="text-xs text-muted-foreground">
{permissionSettings.allow_indexing.description}
</p>
</div>
<Switch
checked={profile.allow_indexing ?? false}
onCheckedChange={(checked) =>
handleProfileChange(profile.id, { allow_indexing: checked })
}
/>
</div>
)}
</div>
<ProfileMcpConfigCard
@@ -570,12 +616,16 @@ export default function AgentSetupPage() {
const initialAgent = useMemo(() => {
if (!decodedKey) return null;
const resolveAvailableAgent = (name: string) =>
availableAgents.find((item: AvailableAgent) => item.name === name);
const resolveDisplayName = (name: string) =>
availableAgents.find((item: AvailableAgent) => item.name === name)?.display_name ?? '';
resolveAvailableAgent(name)?.display_name ?? '';
const resolveDefaultModel = (name: string) =>
availableAgents.find((item: AvailableAgent) => item.name === name)?.model_config?.default_model ?? '';
resolveAvailableAgent(name)?.model_config?.default_model ?? '';
const resolvePermissionSettings = (name: string) =>
resolveAvailableAgent(name)?.permission_settings;
if (savedAgent) {
return ensureProfiles(cloneAgent(savedAgent), resolveDisplayName(savedAgent.name), resolveDefaultModel(savedAgent.name));
return ensureProfiles(cloneAgent(savedAgent), resolveDisplayName(savedAgent.name), resolveDefaultModel(savedAgent.name), resolvePermissionSettings(savedAgent.name));
}
if (discoveryAgent) {
const draft: DraftAgent = {
@@ -589,7 +639,7 @@ export default function AgentSetupPage() {
updated_at: new Date().toISOString(),
isNew: true,
};
return ensureProfiles(draft, resolveDisplayName(draft.name), resolveDefaultModel(draft.name));
return ensureProfiles(draft, resolveDisplayName(draft.name), resolveDefaultModel(draft.name), resolvePermissionSettings(draft.name));
}
return null;
}, [decodedKey, discoveryAgent, savedAgent, availableAgents]);
@@ -3,12 +3,13 @@
import { useMemo } from 'react';
import { useAvailableAgents } from '@/hooks/domains/settings/use-available-agents';
import { useAppStore } from '@/components/state-provider';
import type { Agent, AgentProfile, ModelConfig, AvailableAgent } from '@/lib/types/http';
import type { Agent, AgentProfile, ModelConfig, AvailableAgent, PermissionSetting } from '@/lib/types/http';
type AgentProfileSettingsResult = {
agent: Agent | null;
profile: AgentProfile | null;
modelConfig: ModelConfig;
permissionSettings: Record<string, PermissionSetting>;
};
export function useAgentProfileSettings(
@@ -26,10 +27,17 @@ export function useAgentProfileSettings(
return agent?.profiles.find((item: AgentProfile) => item.id === profileId) ?? null;
}, [agent?.profiles, profileId]);
const modelConfig = useMemo(() => {
const availableAgent = availableAgents.find((item: AvailableAgent) => item.name === agent?.name);
return availableAgent?.model_config ?? { default_model: '', available_models: [] };
const availableAgent = useMemo(() => {
return availableAgents.find((item: AvailableAgent) => item.name === agent?.name) ?? null;
}, [availableAgents, agent?.name]);
return { agent, profile, modelConfig };
const modelConfig = useMemo(() => {
return availableAgent?.model_config ?? { default_model: '', available_models: [] };
}, [availableAgent]);
const permissionSettings = useMemo(() => {
return availableAgent?.permission_settings ?? {};
}, [availableAgent]);
return { agent, profile, modelConfig, permissionSettings };
}
@@ -16,7 +16,7 @@ import { Textarea } from '@kandev/ui/textarea';
import { useToast } from '@/components/toast-provider';
import { UnsavedChangesBadge, UnsavedSaveButton } from '@/components/settings/unsaved-indicator';
import { deleteAgentProfileAction, updateAgentProfileAction } from '@/app/actions/agents';
import type { Agent, AgentProfile, ModelConfig } from '@/lib/types/http';
import type { Agent, AgentProfile, ModelConfig, PermissionSetting } from '@/lib/types/http';
import { useAppStore } from '@/components/state-provider';
import { ProfileMcpConfigCard } from '@/app/settings/agents/[agentId]/profile-mcp-config-card';
import type { AgentProfileMcpConfig } from '@/lib/types/http';
@@ -26,10 +26,11 @@ type ProfileEditorProps = {
agent: Agent;
profile: AgentProfile;
modelConfig: ModelConfig;
permissionSettings: Record<string, PermissionSetting>;
initialMcpConfig?: AgentProfileMcpConfig | null;
};
function ProfileEditor({ agent, profile, modelConfig, initialMcpConfig }: ProfileEditorProps) {
function ProfileEditor({ agent, profile, modelConfig, permissionSettings, initialMcpConfig }: ProfileEditorProps) {
const { toast } = useToast();
const settingsAgents = useAppStore((state) => state.settingsAgents.items);
const setSettingsAgents = useAppStore((state) => state.setSettingsAgents);
@@ -56,6 +57,7 @@ function ProfileEditor({ agent, profile, modelConfig, initialMcpConfig }: Profil
draft.model !== savedProfile.model ||
draft.auto_approve !== savedProfile.auto_approve ||
draft.dangerously_skip_permissions !== savedProfile.dangerously_skip_permissions ||
draft.allow_indexing !== savedProfile.allow_indexing ||
draft.plan !== savedProfile.plan
);
}, [draft, savedProfile]);
@@ -84,6 +86,7 @@ function ProfileEditor({ agent, profile, modelConfig, initialMcpConfig }: Profil
model: draft.model,
auto_approve: draft.auto_approve,
dangerously_skip_permissions: draft.dangerously_skip_permissions,
allow_indexing: draft.allow_indexing,
plan: draft.plan,
});
setSavedProfile(updated);
@@ -211,33 +214,56 @@ function ProfileEditor({ agent, profile, modelConfig, initialMcpConfig }: Profil
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>Auto-approve</Label>
<p className="text-xs text-muted-foreground">Automatically approve tool calls.</p>
{permissionSettings.auto_approve?.supported && (
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>{permissionSettings.auto_approve.label}</Label>
<p className="text-xs text-muted-foreground">
{permissionSettings.auto_approve.description}
</p>
</div>
<Switch
checked={draft.auto_approve}
onCheckedChange={(checked) =>
setDraft({ ...draft, auto_approve: checked })
}
/>
</div>
<Switch
checked={draft.auto_approve}
onCheckedChange={(checked) =>
setDraft({ ...draft, auto_approve: checked })
}
/>
</div>
)}
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>Skip permissions</Label>
<p className="text-xs text-muted-foreground">
Skip permission checks when running tools.
</p>
{permissionSettings.dangerously_skip_permissions?.supported && (
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>{permissionSettings.dangerously_skip_permissions.label}</Label>
<p className="text-xs text-muted-foreground">
{permissionSettings.dangerously_skip_permissions.description}
</p>
</div>
<Switch
checked={draft.dangerously_skip_permissions}
onCheckedChange={(checked) =>
setDraft({ ...draft, dangerously_skip_permissions: checked })
}
/>
</div>
<Switch
checked={draft.dangerously_skip_permissions}
onCheckedChange={(checked) =>
setDraft({ ...draft, dangerously_skip_permissions: checked })
}
/>
</div>
)}
{permissionSettings.allow_indexing?.supported && (
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label>{permissionSettings.allow_indexing.label}</Label>
<p className="text-xs text-muted-foreground">
{permissionSettings.allow_indexing.description}
</p>
</div>
<Switch
checked={draft.allow_indexing}
onCheckedChange={(checked) =>
setDraft({ ...draft, allow_indexing: checked })
}
/>
</div>
)}
</div>
</CardContent>
</Card>
@@ -284,7 +310,7 @@ export function AgentProfilePage({ initialMcpConfig }: AgentProfilePageClientPro
const profileParam = Array.isArray(params.profileId) ? params.profileId[0] : params.profileId;
const agentKey = decodeURIComponent(agentParam ?? '');
const profileId = profileParam ?? '';
const { agent, profile, modelConfig } = useAgentProfileSettings(agentKey, profileId);
const { agent, profile, modelConfig, permissionSettings } = useAgentProfileSettings(agentKey, profileId);
if (!agent || !profile) {
return (
@@ -305,6 +331,7 @@ export function AgentProfilePage({ initialMcpConfig }: AgentProfilePageClientPro
agent={agent}
profile={profile}
modelConfig={modelConfig}
permissionSettings={permissionSettings}
initialMcpConfig={initialMcpConfig}
/>
);
@@ -52,7 +52,7 @@ export function ChatMessage({ comment, label, className, showRichBlocks }: ChatM
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkBreaks]}
components={{
code: ({ node, className, children, ...props }) => {
code: ({ className, children, ...props }) => {
// Check if it's inline code (no className means inline, className with language-* means code block)
const isInline = !className || !className.startsWith('language-');
@@ -93,7 +93,7 @@ export function ChatMessage({ comment, label, className, showRichBlocks }: ChatM
</code>
);
},
ol: ({ node, children, ...props }) => (
ol: ({ children, ...props }) => (
<ol
style={{
listStyleType: 'decimal',
@@ -106,7 +106,7 @@ export function ChatMessage({ comment, label, className, showRichBlocks }: ChatM
{children}
</ol>
),
ul: ({ node, children, ...props }) => (
ul: ({ children, ...props }) => (
<ul
style={{
listStyleType: 'disc',
@@ -119,7 +119,7 @@ export function ChatMessage({ comment, label, className, showRichBlocks }: ChatM
{children}
</ul>
),
li: ({ node, children, ...props }) => (
li: ({ children, ...props }) => (
<li
style={{
marginTop: '0.375rem',
@@ -130,7 +130,7 @@ export function ChatMessage({ comment, label, className, showRichBlocks }: ChatM
{children}
</li>
),
p: ({ node, children, ...props }) => (
p: ({ children, ...props }) => (
<p
style={{
marginTop: '1rem',
+1
View File
@@ -256,6 +256,7 @@ export type AgentProfilePayload = {
model: string;
auto_approve: boolean;
dangerously_skip_permissions: boolean;
allow_indexing: boolean;
plan: string;
created_at?: string;
updated_at?: string;
+12
View File
@@ -394,6 +394,7 @@ export type AgentProfile = {
model: string;
auto_approve: boolean;
dangerously_skip_permissions: boolean;
allow_indexing: boolean;
plan: string;
created_at: string;
updated_at: string;
@@ -460,6 +461,16 @@ export type ModelConfig = {
available_models: ModelEntry[];
};
export type PermissionSetting = {
supported: boolean;
default: boolean;
label: string;
description: string;
apply_method?: string;
cli_flag?: string;
cli_flag_value?: string;
};
export type AvailableAgent = {
name: string;
display_name: string;
@@ -470,6 +481,7 @@ export type AvailableAgent = {
matched_path?: string | null;
capabilities: AgentCapabilities;
model_config: ModelConfig;
permission_settings?: Record<string, PermissionSetting>;
updated_at: string;
};
+2
View File
@@ -47,6 +47,7 @@ export function registerAgentsHandlers(store: StoreApi<AppState>): WsHandlers {
model: message.payload.profile.model,
auto_approve: message.payload.profile.auto_approve,
dangerously_skip_permissions: message.payload.profile.dangerously_skip_permissions,
allow_indexing: message.payload.profile.allow_indexing,
plan: message.payload.profile.plan,
created_at: message.payload.profile.created_at ?? '',
updated_at: message.payload.profile.updated_at ?? '',
@@ -83,6 +84,7 @@ export function registerAgentsHandlers(store: StoreApi<AppState>): WsHandlers {
model: message.payload.profile.model,
auto_approve: message.payload.profile.auto_approve,
dangerously_skip_permissions: message.payload.profile.dangerously_skip_permissions,
allow_indexing: message.payload.profile.allow_indexing,
plan: message.payload.profile.plan,
updated_at: message.payload.profile.updated_at ?? profile.updated_at,
}