diff --git a/src/AxCopilot/Models/AppSettings.cs b/src/AxCopilot/Models/AppSettings.cs
index 9a6867f..f17c408 100644
--- a/src/AxCopilot/Models/AppSettings.cs
+++ b/src/AxCopilot/Models/AppSettings.cs
@@ -591,7 +591,7 @@ public class LlmSettings
///
/// 파일 접근 권한 수준.
- /// Ask = 매번 확인 | Auto = 자동 허용 | Deny = 차단
+ /// Ask = 매번 확인 | Plan = 계획/승인 중심 | Auto = 자동 허용 | Deny = 차단
///
[JsonPropertyName("filePermission")]
public string FilePermission { get; set; } = "Ask";
diff --git a/src/AxCopilot/Models/ChatModels.cs b/src/AxCopilot/Models/ChatModels.cs
index 190d652..9ae4ba6 100644
--- a/src/AxCopilot/Models/ChatModels.cs
+++ b/src/AxCopilot/Models/ChatModels.cs
@@ -97,7 +97,7 @@ public class ChatConversation
// ─── 대화별 설정 (하단 바에서 변경, 대화마다 독립 저장) ───
- /// 파일 접근 권한. null이면 전역 설정 사용. "Auto" | "Ask" | "Deny"
+ /// 파일 접근 권한. null이면 전역 설정 사용. "Ask" | "Plan" | "Auto" | "Deny"
[JsonPropertyName("permission")]
public string? Permission { get; set; }
diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs
index 5f9afa3..8c50b3a 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopService.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs
@@ -4068,6 +4068,9 @@ public partial class AgentLoopService
case "ask":
normalized = "ask";
return true;
+ case "plan":
+ normalized = "ask";
+ return true;
case "auto":
normalized = "auto";
return true;
@@ -4153,16 +4156,16 @@ public partial class AgentLoopService
messages,
success: true);
- var effectivePerm = context.GetEffectiveToolPermission(toolName, target);
+ var effectivePerm = PermissionModeCatalog.NormalizeGlobalMode(context.GetEffectiveToolPermission(toolName, target));
- if (string.Equals(effectivePerm, "Ask", StringComparison.OrdinalIgnoreCase))
- EmitEvent(AgentEventType.PermissionRequest, toolName, $"권한 확인 필요 · 대상: {target}");
+ if (PermissionModeCatalog.RequiresUserApproval(effectivePerm))
+ EmitEvent(AgentEventType.PermissionRequest, toolName, $"권한 확인 필요({effectivePerm}) · 대상: {target}");
var allowed = await context.CheckToolPermissionAsync(toolName, target);
if (allowed)
{
- if (string.Equals(effectivePerm, "Ask", StringComparison.OrdinalIgnoreCase))
- EmitEvent(AgentEventType.PermissionGranted, toolName, $"권한 승인됨 · 대상: {target}");
+ if (PermissionModeCatalog.RequiresUserApproval(effectivePerm))
+ EmitEvent(AgentEventType.PermissionGranted, toolName, $"권한 승인됨({effectivePerm}) · 대상: {target}");
await RunPermissionLifecycleHooksAsync(
"__permission_granted__",
"post",
diff --git a/src/AxCopilot/Services/Agent/IAgentTool.cs b/src/AxCopilot/Services/Agent/IAgentTool.cs
index 4a43b41..49bece7 100644
--- a/src/AxCopilot/Services/Agent/IAgentTool.cs
+++ b/src/AxCopilot/Services/Agent/IAgentTool.cs
@@ -98,7 +98,7 @@ public class AgentContext
/// 작업 폴더 경로.
public string WorkFolder { get; set; } = "";
- /// 파일 접근 권한. Ask | Auto | Deny
+ /// 파일 접근 권한. Ask | Plan | Auto | Deny
public string Permission { get; init; } = "Ask";
/// 도구별 권한 오버라이드. 키: 도구명, 값: "ask" | "auto" | "deny".
@@ -192,19 +192,21 @@ public class AgentContext
public string GetEffectiveToolPermission(string toolName, string? target)
{
if (TryResolvePatternPermission(toolName, target, out var patternPermission))
- return patternPermission;
+ return PermissionModeCatalog.NormalizeToolOverride(patternPermission);
if (ToolPermissions.TryGetValue(toolName, out var toolPerm) &&
!string.IsNullOrWhiteSpace(toolPerm))
- return toolPerm;
+ return PermissionModeCatalog.NormalizeToolOverride(toolPerm);
if (ToolPermissions.TryGetValue("*", out var wildcardPerm) &&
!string.IsNullOrWhiteSpace(wildcardPerm))
- return wildcardPerm;
+ return PermissionModeCatalog.NormalizeToolOverride(wildcardPerm);
if (ToolPermissions.TryGetValue("default", out var defaultPerm) &&
!string.IsNullOrWhiteSpace(defaultPerm))
- return defaultPerm;
+ return PermissionModeCatalog.NormalizeToolOverride(defaultPerm);
- return SensitiveTools.Contains(toolName) ? Permission : "Auto";
+ return SensitiveTools.Contains(toolName)
+ ? PermissionModeCatalog.NormalizeGlobalMode(Permission)
+ : PermissionModeCatalog.Auto;
}
public async Task CheckToolPermissionAsync(string toolName, string target)
@@ -213,9 +215,9 @@ public class AgentContext
&& AxCopilot.Services.OperationModePolicy.IsBlockedAgentToolInInternalMode(toolName, target))
return false;
- var effectivePerm = GetEffectiveToolPermission(toolName, target);
- if (string.Equals(effectivePerm, "Deny", StringComparison.OrdinalIgnoreCase)) return false;
- if (string.Equals(effectivePerm, "Auto", StringComparison.OrdinalIgnoreCase)) return true;
+ var effectivePerm = PermissionModeCatalog.NormalizeGlobalMode(GetEffectiveToolPermission(toolName, target));
+ if (PermissionModeCatalog.IsDeny(effectivePerm)) return false;
+ if (PermissionModeCatalog.IsAuto(effectivePerm)) return true;
if (AskPermission == null) return false;
var normalizedTarget = string.IsNullOrWhiteSpace(target) ? toolName : target.Trim();
diff --git a/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs b/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs
new file mode 100644
index 0000000..dab2935
--- /dev/null
+++ b/src/AxCopilot/Services/Agent/PermissionModeCatalog.cs
@@ -0,0 +1,72 @@
+namespace AxCopilot.Services.Agent;
+
+///
+/// AX Agent permission mode constants and normalization helpers.
+/// Accepts legacy Ask/Auto/Deny values plus claw-code style aliases.
+///
+public static class PermissionModeCatalog
+{
+ public const string Ask = "Ask";
+ public const string Plan = "Plan";
+ public const string Auto = "Auto";
+ public const string Deny = "Deny";
+
+ public static readonly IReadOnlyList UserSelectableModes = new[]
+ {
+ Ask,
+ Plan,
+ Auto,
+ Deny,
+ };
+
+ ///
+ /// Normalize global permission mode.
+ /// Supported aliases: ask/auto/deny/plan plus default, acceptEdits, dontAsk, bypassPermissions.
+ ///
+ public static string NormalizeGlobalMode(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return Ask;
+
+ return value.Trim().ToLowerInvariant() switch
+ {
+ "ask" => Ask,
+ "default" => Ask,
+ "plan" => Plan,
+ "auto" => Auto,
+ "acceptedits" => Auto,
+ "dontask" => Auto,
+ "deny" => Deny,
+ "bypasspermissions" => Deny,
+ _ => Ask,
+ };
+ }
+
+ ///
+ /// Normalize tool override permission.
+ /// Tool override is constrained to ask/auto/deny.
+ ///
+ public static string NormalizeToolOverride(string? value)
+ {
+ var mode = NormalizeGlobalMode(value);
+ return mode switch
+ {
+ Auto => "auto",
+ Deny => "deny",
+ _ => "ask",
+ };
+ }
+
+ public static bool IsAuto(string? mode) =>
+ string.Equals(NormalizeGlobalMode(mode), Auto, StringComparison.OrdinalIgnoreCase);
+
+ public static bool IsDeny(string? mode) =>
+ string.Equals(NormalizeGlobalMode(mode), Deny, StringComparison.OrdinalIgnoreCase);
+
+ public static bool RequiresUserApproval(string? mode)
+ {
+ var normalized = NormalizeGlobalMode(mode);
+ return !string.Equals(normalized, Auto, StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(normalized, Deny, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/AxCopilot/Services/AppStateService.cs b/src/AxCopilot/Services/AppStateService.cs
index dc52f3f..0796de4 100644
--- a/src/AxCopilot/Services/AppStateService.cs
+++ b/src/AxCopilot/Services/AppStateService.cs
@@ -194,7 +194,7 @@ public sealed class AppStateService
Skills.Enabled = llm.EnableSkillSystem;
Skills.FolderPath = llm.SkillsFolderPath ?? "";
- Permissions.FilePermission = llm.FilePermission ?? "Ask";
+ Permissions.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(llm.FilePermission);
Permissions.AgentDecisionLevel = llm.AgentDecisionLevel ?? "detailed";
Permissions.PlanMode = llm.PlanMode ?? "off";
Permissions.ToolOverrideCount = llm.ToolPermissions?.Count ?? 0;
@@ -501,27 +501,31 @@ public sealed class AppStateService
public PermissionSummaryState GetPermissionSummary(ChatConversation? conversation = null)
{
- var effective = conversation?.Permission;
- if (string.IsNullOrWhiteSpace(effective))
- effective = Permissions.FilePermission;
+ var effective = PermissionModeCatalog.NormalizeGlobalMode(conversation?.Permission);
+ var defaultMode = PermissionModeCatalog.NormalizeGlobalMode(Permissions.FilePermission);
+ if (string.IsNullOrWhiteSpace(conversation?.Permission))
+ effective = defaultMode;
- var risk = string.Equals(effective, "Auto", StringComparison.OrdinalIgnoreCase)
+ var risk = string.Equals(effective, PermissionModeCatalog.Auto, StringComparison.OrdinalIgnoreCase)
? "high"
- : string.Equals(effective, "Deny", StringComparison.OrdinalIgnoreCase)
+ : string.Equals(effective, PermissionModeCatalog.Deny, StringComparison.OrdinalIgnoreCase)
? "locked"
+ : string.Equals(effective, PermissionModeCatalog.Plan, StringComparison.OrdinalIgnoreCase)
+ ? "guarded"
: "normal";
var description = effective switch
{
"Auto" => "파일 작업을 자동 허용합니다.",
"Deny" => "파일 작업을 차단합니다.",
+ "Plan" => "계획/승인 흐름을 우선 적용한 뒤 파일 작업을 진행합니다.",
_ => "파일 작업 전마다 사용자 확인을 요청합니다.",
};
return new PermissionSummaryState
{
- EffectiveMode = effective ?? "Ask",
- DefaultMode = Permissions.FilePermission,
+ EffectiveMode = effective,
+ DefaultMode = defaultMode,
OverrideCount = Permissions.ToolOverrideCount,
RiskLevel = risk,
Description = description,
diff --git a/src/AxCopilot/Services/SettingsService.cs b/src/AxCopilot/Services/SettingsService.cs
index 1170087..e295e9e 100644
--- a/src/AxCopilot/Services/SettingsService.cs
+++ b/src/AxCopilot/Services/SettingsService.cs
@@ -1,6 +1,7 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Models;
+using AxCopilot.Services.Agent;
namespace AxCopilot.Services;
@@ -173,6 +174,15 @@ public class SettingsService
private void NormalizeRuntimeSettings()
{
+ _settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(_settings.Llm.FilePermission);
+ _settings.Llm.DefaultAgentPermission = PermissionModeCatalog.NormalizeGlobalMode(_settings.Llm.DefaultAgentPermission);
+ if (_settings.Llm.ToolPermissions != null && _settings.Llm.ToolPermissions.Count > 0)
+ {
+ var keys = _settings.Llm.ToolPermissions.Keys.ToList();
+ foreach (var key in keys)
+ _settings.Llm.ToolPermissions[key] = PermissionModeCatalog.NormalizeToolOverride(_settings.Llm.ToolPermissions[key]);
+ }
+
NormalizeLlmThresholds(_settings.Llm);
}
@@ -262,3 +272,4 @@ public class SettingsService
};
}
}
+
diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs
index 0939cb5..ee7bed8 100644
--- a/src/AxCopilot/ViewModels/SettingsViewModel.cs
+++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs
@@ -7,6 +7,7 @@ using System.Windows.Forms;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
+using AxCopilot.Services.Agent;
using AxCopilot.Themes;
using AxCopilot.Views;
@@ -252,7 +253,7 @@ public class SettingsViewModel : INotifyPropertyChanged
public string DefaultAgentPermission
{
get => _defaultAgentPermission;
- set { _defaultAgentPermission = value; OnPropertyChanged(); }
+ set { _defaultAgentPermission = PermissionModeCatalog.NormalizeGlobalMode(value); OnPropertyChanged(); }
}
// ── 코워크/에이전트 고급 설정 ──
@@ -981,7 +982,7 @@ public class SettingsViewModel : INotifyPropertyChanged
_llmMaxContextTokens = llm.MaxContextTokens;
_llmRetentionDays = llm.RetentionDays;
_llmTemperature = llm.Temperature;
- _defaultAgentPermission = llm.DefaultAgentPermission;
+ _defaultAgentPermission = PermissionModeCatalog.NormalizeGlobalMode(llm.DefaultAgentPermission);
_defaultOutputFormat = llm.DefaultOutputFormat;
_defaultMood = string.IsNullOrEmpty(llm.DefaultMood) ? "modern" : llm.DefaultMood;
_autoPreview = llm.AutoPreview;
@@ -1405,7 +1406,7 @@ public class SettingsViewModel : INotifyPropertyChanged
s.Llm.MaxContextTokens = _llmMaxContextTokens;
s.Llm.RetentionDays = _llmRetentionDays;
s.Llm.Temperature = _llmTemperature;
- s.Llm.DefaultAgentPermission = _defaultAgentPermission;
+ s.Llm.DefaultAgentPermission = PermissionModeCatalog.NormalizeGlobalMode(_defaultAgentPermission);
s.Llm.DefaultOutputFormat = _defaultOutputFormat;
s.Llm.DefaultMood = _defaultMood;
s.Llm.AutoPreview = _autoPreview;
diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs
index 31f058e..326d0c1 100644
--- a/src/AxCopilot/Views/ChatWindow.xaml.cs
+++ b/src/AxCopilot/Views/ChatWindow.xaml.cs
@@ -1282,7 +1282,7 @@ public partial class ChatWindow : Window
var llm = _settings.Settings.Llm;
if (conv != null && conv.Permission != null)
- _settings.Settings.Llm.FilePermission = conv.Permission;
+ _settings.Settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(conv.Permission);
_folderDataUsage = conv?.DataUsage ?? llm.FolderDataUsage ?? "active";
_selectedMood = conv?.Mood ?? llm.DefaultMood ?? "modern";
@@ -1304,7 +1304,7 @@ public partial class ChatWindow : Window
}
else
{
- conv.Permission = _settings.Settings.Llm.FilePermission;
+ conv.Permission = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission);
conv.DataUsage = _folderDataUsage;
conv.Mood = _selectedMood;
_storage.Save(conv);
@@ -1485,10 +1485,11 @@ public partial class ChatWindow : Window
var levels = new (string Level, string Sym, string Desc, string Color)[] {
("Ask", "\uE8D7", "매번 확인 — 파일 접근 시 사용자에게 묻습니다", "#4B5EFC"),
+ ("Plan", "\uE7C3", "계획/승인 중심 — 실행 전 계획과 사용자 확인 흐름을 우선합니다", "#4338CA"),
("Auto", "\uE73E", "자동 허용 — 파일을 자동으로 읽고 씁니다", "#DD6B20"),
("Deny", "\uE711", "접근 차단 — 파일 접근을 허용하지 않습니다", "#C50F1F"),
};
- var current = _settings.Settings.Llm.FilePermission;
+ var current = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.FilePermission);
foreach (var (level, sym, desc, color) in levels)
{
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
@@ -1548,7 +1549,7 @@ public partial class ChatWindow : Window
var capturedLevel = level;
btn.Click += (_, _) =>
{
- _settings.Settings.Llm.FilePermission = capturedLevel;
+ _settings.Settings.Llm.FilePermission = PermissionModeCatalog.NormalizeGlobalMode(capturedLevel);
UpdatePermissionUI();
SaveConversationSettings();
PermissionPopup.IsOpen = false;
@@ -1571,7 +1572,7 @@ public partial class ChatWindow : Window
}
else
{
- toolPermissions[existingKey ?? toolName] = mode;
+ toolPermissions[existingKey ?? toolName] = PermissionModeCatalog.NormalizeToolOverride(mode);
}
try { _settings.Save(); } catch { }
@@ -1601,10 +1602,11 @@ public partial class ChatWindow : Window
ChatConversation? currentConversation;
lock (_convLock) currentConversation = _currentConversation;
var summary = _appState.GetPermissionSummary(currentConversation);
- var perm = summary.EffectiveMode;
+ var perm = PermissionModeCatalog.NormalizeGlobalMode(summary.EffectiveMode);
PermissionLabel.Text = perm;
PermissionIcon.Text = perm switch
{
+ "Plan" => "\uE7C3",
"Auto" => "\uE73E",
"Deny" => "\uE711",
_ => "\uE8D7",
@@ -1629,8 +1631,12 @@ public partial class ChatWindow : Window
{
_autoWarningDismissed = false; // Auto가 아닌 모드로 전환하면 리셋
var defaultFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
- var iconFg = perm == "Deny" ? new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F))
- : new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); // Ask = 파란색
+ var iconFg = perm switch
+ {
+ "Deny" => new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F)),
+ "Plan" => new SolidColorBrush(Color.FromRgb(0x43, 0x38, 0xCA)),
+ _ => new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)), // Ask = 파란색
+ };
PermissionLabel.Foreground = defaultFg;
PermissionIcon.Foreground = iconFg;
if (AutoPermissionWarning != null)
@@ -1739,11 +1745,11 @@ public partial class ChatWindow : Window
if (_activeTab == "Chat")
{
// Chat 탭: 경고 배너 숨기고 기본 Ask 모드로 복원
- _settings.Settings.Llm.FilePermission = "Ask";
+ _settings.Settings.Llm.FilePermission = PermissionModeCatalog.Ask;
UpdatePermissionUI();
return;
}
- var defaultPerm = _settings.Settings.Llm.DefaultAgentPermission;
+ var defaultPerm = PermissionModeCatalog.NormalizeGlobalMode(_settings.Settings.Llm.DefaultAgentPermission);
if (!string.IsNullOrEmpty(defaultPerm))
{
_settings.Settings.Llm.FilePermission = defaultPerm;
@@ -10318,9 +10324,10 @@ public partial class ChatWindow : Window
"detailed" => "높음",
_ => "중간",
};
- private static string NextPermission(string current) => (current ?? "Ask").ToLowerInvariant() switch
+ private static string NextPermission(string current) => PermissionModeCatalog.NormalizeGlobalMode(current).ToLowerInvariant() switch
{
- "ask" => "Auto",
+ "ask" => "Plan",
+ "plan" => "Auto",
"auto" => "Deny",
_ => "Ask",
};
diff --git a/src/AxCopilot/Views/SettingsWindow.xaml b/src/AxCopilot/Views/SettingsWindow.xaml
index 92d8487..f6c0986 100644
--- a/src/AxCopilot/Views/SettingsWindow.xaml
+++ b/src/AxCopilot/Views/SettingsWindow.xaml
@@ -4346,6 +4346,7 @@
Width="160" SelectedValue="{Binding DefaultAgentPermission, Mode=TwoWay}"
SelectedValuePath="Tag">
+