권한 코어를 claude-code 기준으로 재구성하고 slash palette 상태 분리를 시작\n\n- Default/AcceptEdits/Plan/BypassPermissions/DontAsk/Deny 권한 모드를 추가하고 기존 Ask/Auto 호환을 유지\n- deny 우선 패턴 규칙, allow/override, 글로벌 모드 순서의 권한 해석 체계를 정리\n- file_write/file_edit/file_manage와 process/build_run/test_loop/snippet_runner/spawn_agent 계열을 권한 클래스별로 분리\n- AcceptEdits는 파일 편집 도구 자동 허용, process 계열은 계속 확인하도록 조정\n- Plan은 쓰기 도구를 차단하고 읽기 중심 진행이 되도록 보강\n- BypassPermissions와 DontAsk는 권한 확인을 생략하는 경로로 정규화\n- AX Agent 권한 팝업, 상단 배너, slash 명령 결과를 새 권한 체계에 맞게 정리\n- /permissions, /allowed-tools, /sandbox-toggle 사용법과 상태 출력을 갱신\n- ChatWindow의 slash palette 상태를 전용 SlashPaletteState로 분리해 이후 composer 개편 기반을 마련\n- AppState, 설정 모델, 테스트를 새 권한 체계에 맞게 갱신\n- dotnet build 경고 0 / 오류 0, dotnet test 436 통과를 확인
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
This commit is contained in:
@@ -95,8 +95,22 @@ public class AgentContext
|
||||
private static readonly HashSet<string> DangerousAutoTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"process",
|
||||
"build_run",
|
||||
"spawn_agent",
|
||||
"snippet_runner",
|
||||
"test_loop",
|
||||
};
|
||||
private static readonly HashSet<string> WriteTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_write", "file_edit", "file_manage",
|
||||
"html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create",
|
||||
"chart_create", "script_create", "document_assemble", "format_convert", "template_render", "checkpoint",
|
||||
"todo_write", "skill_manager", "project_rule", "task_create", "task_update", "task_stop",
|
||||
"team_create", "team_delete", "cron_create", "cron_delete", "zip",
|
||||
};
|
||||
private static readonly HashSet<string> ProcessLikeTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"process", "build_run", "test_loop", "snippet_runner", "spawn_agent", "git_tool",
|
||||
};
|
||||
|
||||
private readonly object _permissionLock = new();
|
||||
@@ -104,10 +118,10 @@ public class AgentContext
|
||||
/// <summary>작업 폴더 경로.</summary>
|
||||
public string WorkFolder { get; set; } = "";
|
||||
|
||||
/// <summary>파일 접근 권한. Ask | Plan | Auto | Deny</summary>
|
||||
public string Permission { get; init; } = "Ask";
|
||||
/// <summary>파일 접근 권한. Default | AcceptEdits | Plan | BypassPermissions | DontAsk | Deny</summary>
|
||||
public string Permission { get; init; } = "Default";
|
||||
|
||||
/// <summary>도구별 권한 오버라이드. 키: 도구명, 값: "ask" | "auto" | "deny".</summary>
|
||||
/// <summary>도구별 권한 오버라이드. 키: 도구명 또는 tool@pattern, 값: 권한 모드.</summary>
|
||||
public Dictionary<string, string> ToolPermissions { get; init; } = new();
|
||||
|
||||
/// <summary>차단 경로 패턴 목록.</summary>
|
||||
@@ -201,22 +215,22 @@ public class AgentContext
|
||||
var normalizedToolName = toolName.Trim();
|
||||
|
||||
if (TryResolvePatternPermission(toolName, target, out var patternPermission))
|
||||
return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(patternPermission));
|
||||
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(patternPermission));
|
||||
|
||||
if (ToolPermissions.TryGetValue(toolName, out var toolPerm) &&
|
||||
!string.IsNullOrWhiteSpace(toolPerm))
|
||||
return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(toolPerm));
|
||||
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(toolPerm));
|
||||
if (ToolPermissions.TryGetValue("*", out var wildcardPerm) &&
|
||||
!string.IsNullOrWhiteSpace(wildcardPerm))
|
||||
return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(wildcardPerm));
|
||||
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(wildcardPerm));
|
||||
if (ToolPermissions.TryGetValue("default", out var defaultPerm) &&
|
||||
!string.IsNullOrWhiteSpace(defaultPerm))
|
||||
return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(defaultPerm));
|
||||
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(defaultPerm));
|
||||
|
||||
var fallback = SensitiveTools.Contains(toolName)
|
||||
? PermissionModeCatalog.NormalizeGlobalMode(Permission)
|
||||
: PermissionModeCatalog.Auto;
|
||||
return ApplyDangerousAutoGuard(normalizedToolName, fallback);
|
||||
: PermissionModeCatalog.AcceptEdits;
|
||||
return ResolveModeForTool(normalizedToolName, fallback);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckToolPermissionAsync(string toolName, string target)
|
||||
@@ -256,6 +270,30 @@ public class AgentContext
|
||||
var normalizedTool = toolName.Trim();
|
||||
var normalizedTarget = target.Trim();
|
||||
|
||||
foreach (var kv in ToolPermissions)
|
||||
{
|
||||
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
||||
&& string.Equals(ruleTool, normalizedTool, StringComparison.OrdinalIgnoreCase)
|
||||
&& WildcardMatch(normalizedTarget, rulePattern)
|
||||
&& PermissionModeCatalog.IsDeny(kv.Value))
|
||||
{
|
||||
permission = kv.Value.Trim();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kv in ToolPermissions)
|
||||
{
|
||||
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
||||
&& string.Equals(ruleTool, "*", StringComparison.Ordinal)
|
||||
&& WildcardMatch(normalizedTarget, rulePattern)
|
||||
&& PermissionModeCatalog.IsDeny(kv.Value))
|
||||
{
|
||||
permission = kv.Value.Trim();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kv in ToolPermissions)
|
||||
{
|
||||
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
||||
@@ -308,15 +346,63 @@ public class AgentContext
|
||||
return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
|
||||
private string ResolveModeForTool(string toolName, string mode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
return mode;
|
||||
|
||||
var normalizedMode = PermissionModeCatalog.NormalizeGlobalMode(mode);
|
||||
if (PermissionModeCatalog.IsDeny(normalizedMode))
|
||||
return PermissionModeCatalog.Deny;
|
||||
|
||||
if (PermissionModeCatalog.IsBypassPermissions(normalizedMode) || PermissionModeCatalog.IsDontAsk(normalizedMode))
|
||||
return normalizedMode;
|
||||
|
||||
if (PermissionModeCatalog.IsPlan(normalizedMode))
|
||||
{
|
||||
if (IsWriteTool(toolName))
|
||||
return PermissionModeCatalog.Deny;
|
||||
|
||||
return IsProcessLikeTool(toolName)
|
||||
? PermissionModeCatalog.Default
|
||||
: PermissionModeCatalog.AcceptEdits;
|
||||
}
|
||||
|
||||
if (PermissionModeCatalog.IsAcceptEdits(normalizedMode))
|
||||
{
|
||||
if (IsWriteTool(toolName))
|
||||
return PermissionModeCatalog.AcceptEdits;
|
||||
|
||||
return IsProcessLikeTool(toolName)
|
||||
? PermissionModeCatalog.Default
|
||||
: ApplyDangerousAutoGuard(toolName, PermissionModeCatalog.AcceptEdits);
|
||||
}
|
||||
|
||||
if (PermissionModeCatalog.IsDefault(normalizedMode))
|
||||
{
|
||||
if (!SensitiveTools.Contains(toolName))
|
||||
return PermissionModeCatalog.AcceptEdits;
|
||||
|
||||
return ApplyDangerousAutoGuard(toolName, PermissionModeCatalog.Default);
|
||||
}
|
||||
|
||||
return ApplyDangerousAutoGuard(toolName, normalizedMode);
|
||||
}
|
||||
|
||||
private static bool IsWriteTool(string toolName) => WriteTools.Contains(toolName);
|
||||
|
||||
private static bool IsProcessLikeTool(string toolName) => ProcessLikeTools.Contains(toolName);
|
||||
|
||||
private string ApplyDangerousAutoGuard(string toolName, string permission)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
return permission;
|
||||
|
||||
if (PermissionModeCatalog.IsAuto(permission)
|
||||
&& PermissionModeCatalog.IsAuto(Permission)
|
||||
&& !PermissionModeCatalog.IsBypassPermissions(permission)
|
||||
&& !PermissionModeCatalog.IsDontAsk(permission)
|
||||
&& DangerousAutoTools.Contains(toolName))
|
||||
return "ask";
|
||||
return PermissionModeCatalog.Default;
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
@@ -6,16 +6,22 @@
|
||||
/// </summary>
|
||||
public static class PermissionModeCatalog
|
||||
{
|
||||
public const string Ask = "Ask";
|
||||
public const string Default = "Default";
|
||||
public const string Ask = Default;
|
||||
public const string AcceptEdits = "AcceptEdits";
|
||||
public const string Plan = "Plan";
|
||||
public const string Auto = "Auto";
|
||||
public const string BypassPermissions = "BypassPermissions";
|
||||
public const string DontAsk = "DontAsk";
|
||||
public const string Auto = AcceptEdits;
|
||||
public const string Deny = "Deny";
|
||||
|
||||
public static readonly IReadOnlyList<string> UserSelectableModes = new[]
|
||||
{
|
||||
Ask,
|
||||
Default,
|
||||
AcceptEdits,
|
||||
Plan,
|
||||
Auto,
|
||||
BypassPermissions,
|
||||
DontAsk,
|
||||
Deny,
|
||||
};
|
||||
|
||||
@@ -26,15 +32,23 @@ public static class PermissionModeCatalog
|
||||
public static string NormalizeGlobalMode(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return Ask;
|
||||
return Default;
|
||||
|
||||
return value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"default" => Default,
|
||||
"ask" => Ask,
|
||||
"plan" => Plan,
|
||||
"acceptedits" => AcceptEdits,
|
||||
"accept" => AcceptEdits,
|
||||
"auto" => Auto,
|
||||
"plan" => Plan,
|
||||
"bypasspermissions" => BypassPermissions,
|
||||
"bypass" => BypassPermissions,
|
||||
"dontask" => DontAsk,
|
||||
"don't ask" => DontAsk,
|
||||
"allow" => AcceptEdits,
|
||||
"deny" => Deny,
|
||||
_ => Ask,
|
||||
_ => Default,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,14 +61,32 @@ public static class PermissionModeCatalog
|
||||
var mode = NormalizeGlobalMode(value);
|
||||
return mode switch
|
||||
{
|
||||
Auto => "auto",
|
||||
Deny => "deny",
|
||||
_ => "ask",
|
||||
AcceptEdits => AcceptEdits,
|
||||
Plan => Plan,
|
||||
BypassPermissions => BypassPermissions,
|
||||
DontAsk => DontAsk,
|
||||
Deny => Deny,
|
||||
_ => Default,
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsAuto(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), Auto, StringComparison.OrdinalIgnoreCase);
|
||||
IsAcceptEdits(mode) || IsBypassPermissions(mode) || IsDontAsk(mode);
|
||||
|
||||
public static bool IsDefault(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), Default, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsAcceptEdits(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), AcceptEdits, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsPlan(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), Plan, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsBypassPermissions(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), BypassPermissions, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsDontAsk(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), DontAsk, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsDeny(string? mode) =>
|
||||
string.Equals(NormalizeGlobalMode(mode), Deny, StringComparison.OrdinalIgnoreCase);
|
||||
@@ -62,7 +94,24 @@ public static class PermissionModeCatalog
|
||||
public static bool RequiresUserApproval(string? mode)
|
||||
{
|
||||
var normalized = NormalizeGlobalMode(mode);
|
||||
return !string.Equals(normalized, Auto, StringComparison.OrdinalIgnoreCase)
|
||||
return !IsAcceptEdits(normalized)
|
||||
&& !IsBypassPermissions(normalized)
|
||||
&& !IsDontAsk(normalized)
|
||||
&& !string.Equals(normalized, Deny, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string ToDisplayLabel(string? mode)
|
||||
{
|
||||
var normalized = NormalizeGlobalMode(mode);
|
||||
return normalized switch
|
||||
{
|
||||
Default => "소극 활용",
|
||||
AcceptEdits => "적극 활용",
|
||||
Plan => "Plan",
|
||||
BypassPermissions => "Bypass",
|
||||
DontAsk => "DontAsk",
|
||||
Deny => "활용하지 않음",
|
||||
_ => normalized,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class AppStateService
|
||||
|
||||
public sealed class PermissionPolicyState
|
||||
{
|
||||
public string FilePermission { get; set; } = "Ask";
|
||||
public string FilePermission { get; set; } = "Default";
|
||||
public string AgentDecisionLevel { get; set; } = "detailed";
|
||||
public string PlanMode { get; set; } = "off";
|
||||
public int ToolOverrideCount { get; set; }
|
||||
@@ -506,7 +506,9 @@ public sealed class AppStateService
|
||||
if (string.IsNullOrWhiteSpace(conversation?.Permission))
|
||||
effective = defaultMode;
|
||||
|
||||
var risk = string.Equals(effective, PermissionModeCatalog.Auto, StringComparison.OrdinalIgnoreCase)
|
||||
var risk = PermissionModeCatalog.IsBypassPermissions(effective) || PermissionModeCatalog.IsDontAsk(effective)
|
||||
? "critical"
|
||||
: PermissionModeCatalog.IsAcceptEdits(effective)
|
||||
? "high"
|
||||
: string.Equals(effective, PermissionModeCatalog.Deny, StringComparison.OrdinalIgnoreCase)
|
||||
? "locked"
|
||||
@@ -516,9 +518,11 @@ public sealed class AppStateService
|
||||
|
||||
var description = effective switch
|
||||
{
|
||||
"Auto" => "파일 작업을 자동 허용합니다.",
|
||||
"Deny" => "파일 작업을 차단합니다.",
|
||||
"AcceptEdits" => "파일 편집 도구는 자동 허용하고 명령 실행은 계속 확인합니다.",
|
||||
"Deny" => "파일 읽기만 허용하고 생성/수정/삭제는 차단합니다.",
|
||||
"Plan" => "계획/승인 흐름을 우선 적용한 뒤 파일 작업을 진행합니다.",
|
||||
"BypassPermissions" => "모든 권한 확인을 생략합니다. 주의해서 사용해야 합니다.",
|
||||
"DontAsk" => "권한 질문 없이 진행합니다. 자동 실행 범위를 반드시 점검해야 합니다.",
|
||||
_ => "파일 작업 전마다 사용자 확인을 요청합니다.",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user