Skip to content

Commit 534adc0

Browse files
committed
Add MCP auto-approve and agnostic assessments
- Auto-approve previously approved commands (same cmd + dir) - Use Recommendation.None instead of IsAgnostic property - Regex returns agnostic when no rules match
1 parent 7f23fd4 commit 534adc0

File tree

9 files changed

+73
-35
lines changed

9 files changed

+73
-35
lines changed

src/PostSharp.Engineering.BuildTools/Mcp/Models/CommandRecord.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public sealed class CommandRecord
1313

1414
public required string Command { get; init; }
1515

16+
public required string WorkingDirectory { get; init; }
17+
1618
public required string ClaimedPurpose { get; init; }
1719

1820
public required bool Approved { get; init; }

src/PostSharp.Engineering.BuildTools/Mcp/Models/CommandRule.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,4 @@ public sealed class CommandRule
4040
/// If null, the rule applies whenever the pattern matches.
4141
/// </summary>
4242
public Func<CommandContext, bool>? Condition { get; init; }
43-
44-
/// <summary>
45-
/// Gets a value indicating whether this rule is agnostic (defers to AI analysis).
46-
/// When true, matching this rule will not influence the risk assessment - only AI will determine risk.
47-
/// </summary>
48-
public bool IsAgnostic { get; init; }
4943
}

src/PostSharp.Engineering.BuildTools/Mcp/Models/RiskAssessment.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public enum RiskLevel
2020
/// </summary>
2121
public enum Recommendation
2222
{
23+
None,
2324
Approve,
2425
Reject
2526
}
@@ -41,12 +42,6 @@ public sealed class RiskAssessment
4142
/// </summary>
4243
public string? RuleName { get; init; }
4344

44-
/// <summary>
45-
/// Gets a value indicating whether this assessment is agnostic (defers to AI analysis).
46-
/// When true, this regex-based assessment should be ignored and only AI assessment used.
47-
/// </summary>
48-
public bool IsAgnostic { get; init; }
49-
5045
public static RiskAssessment Default( string reason )
5146
{
5247
return new RiskAssessment

src/PostSharp.Engineering.BuildTools/Mcp/Services/ApprovalPrompter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ private static string GetRecommendationMarkup( Recommendation recommendation )
172172
{
173173
return recommendation switch
174174
{
175+
Recommendation.None => "[dim]NONE (agnostic)[/]",
175176
Recommendation.Approve => "[green]APPROVE[/]",
176177
Recommendation.Reject => "[red]REJECT[/]",
177178
_ => recommendation.ToString()

src/PostSharp.Engineering.BuildTools/Mcp/Services/CommandHistoryService.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public IReadOnlyList<CommandRecord> GetHistory( string sessionId )
3434
public void Record(
3535
string sessionId,
3636
string command,
37+
string workingDirectory,
3738
string claimedPurpose,
3839
bool approved,
3940
CommandResult result )
@@ -42,6 +43,7 @@ public void Record(
4243
{
4344
Timestamp = DateTime.UtcNow,
4445
Command = command,
46+
WorkingDirectory = workingDirectory,
4547
ClaimedPurpose = claimedPurpose,
4648
Approved = approved,
4749
ExitCode = result.ExitCode,
@@ -62,6 +64,22 @@ public void Record(
6264
} );
6365
}
6466

67+
public bool WasPreviouslyApproved( string sessionId, string command, string workingDirectory )
68+
{
69+
if ( !this._sessions.TryGetValue( sessionId, out var history ) )
70+
{
71+
return false;
72+
}
73+
74+
lock ( this._lock )
75+
{
76+
return history.Any( r =>
77+
r.Approved &&
78+
r.Command.Equals( command, StringComparison.Ordinal ) &&
79+
r.WorkingDirectory.Equals( workingDirectory, StringComparison.OrdinalIgnoreCase ) );
80+
}
81+
}
82+
6583
public void ClearSession( string sessionId )
6684
{
6785
this._sessions.TryRemove( sessionId, out _ );

src/PostSharp.Engineering.BuildTools/Mcp/Services/CommandRules.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,29 +291,26 @@ public static class CommandRules
291291
Name = "export-env-variables",
292292
Pattern = new Regex( @"\$env:\w+\s*=", RegexOptions.IgnoreCase ),
293293
RiskLevel = RiskLevel.Low,
294-
Recommendation = Recommendation.Approve,
295-
Reason = "Setting environment variables - deferring to AI to determine if secrets are exposed",
296-
IsAgnostic = true
294+
Recommendation = Recommendation.None,
295+
Reason = "Setting environment variables - deferring to AI to determine if secrets are exposed"
297296
},
298297

299298
new CommandRule
300299
{
301300
Name = "env-var-reference-powershell",
302301
Pattern = new Regex( @"\$env:\w+", RegexOptions.IgnoreCase ),
303302
RiskLevel = RiskLevel.Low,
304-
Recommendation = Recommendation.Approve,
305-
Reason = "Environment variable reference detected - deferring to AI to determine if leaked",
306-
IsAgnostic = true
303+
Recommendation = Recommendation.None,
304+
Reason = "Environment variable reference detected - deferring to AI to determine if leaked"
307305
},
308306

309307
new CommandRule
310308
{
311309
Name = "env-var-reference-bash",
312310
Pattern = new Regex( @"\$\{?\w+\}?", RegexOptions.None ),
313311
RiskLevel = RiskLevel.Low,
314-
Recommendation = Recommendation.Approve,
315-
Reason = "Potential environment variable reference detected - deferring to AI analysis",
316-
IsAgnostic = true
312+
Recommendation = Recommendation.None,
313+
Reason = "Potential environment variable reference detected - deferring to AI analysis"
317314
}
318315
};
319316

src/PostSharp.Engineering.BuildTools/Mcp/Services/RegexRuleEngine.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,16 @@ public Task<RiskAssessment> EvaluateAsync(
5858
Level = rule.RiskLevel,
5959
Recommendation = rule.Recommendation,
6060
Reason = rule.Reason,
61-
RuleName = rule.Name,
62-
IsAgnostic = rule.IsAgnostic
61+
RuleName = rule.Name
6362
} );
6463
}
6564

66-
// No rules matched - return default LOW/APPROVE
65+
// No rules matched - return agnostic (defer to AI analysis)
6766
return Task.FromResult( new RiskAssessment
6867
{
6968
Level = RiskLevel.Low,
70-
Recommendation = Recommendation.Approve,
71-
Reason = "No regex rules matched - command appears safe",
69+
Recommendation = Recommendation.None,
70+
Reason = "No regex rules matched - deferring to AI analysis",
7271
RuleName = null
7372
} );
7473
}

src/PostSharp.Engineering.BuildTools/Mcp/Services/RiskCombiner.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ public static class RiskCombiner
1818
/// <returns>A combined risk assessment with the maximum risk level and most restrictive recommendation.</returns>
1919
public static RiskAssessment Combine( RiskAssessment aiAssessment, RiskAssessment regexAssessment )
2020
{
21-
// If regex assessment is agnostic, use only AI assessment
22-
if ( regexAssessment.IsAgnostic )
21+
// If regex assessment has no recommendation (agnostic), use only AI assessment
22+
if ( regexAssessment.Recommendation == Recommendation.None )
2323
{
2424
return aiAssessment;
2525
}

src/PostSharp.Engineering.BuildTools/Mcp/Tools/ExecuteCommandTool.cs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,42 @@ public async Task<CommandResult> ExecuteCommand(
7676

7777
try
7878
{
79-
// 1. Get session history
79+
// 1. Check if this exact command was previously approved
80+
if ( this._history.WasPreviouslyApproved( sessionId, command, workingDirectory ) )
81+
{
82+
AnsiConsole.MarkupLine( "[green]Auto-approved (previously approved)[/]" );
83+
AnsiConsole.WriteLine();
84+
AnsiConsole.Write( new Rule( "[blue]Executing Request[/]" ) );
85+
AnsiConsole.WriteLine();
86+
87+
// Pleasant single beep for auto-approve (same as LOW risk)
88+
try
89+
{
90+
#pragma warning disable CA1416 // Platform compatibility - we handle non-Windows in catch
91+
Console.Beep( 1200, 150 );
92+
#pragma warning restore CA1416
93+
}
94+
catch
95+
{
96+
// Beep may not be supported on all systems
97+
}
98+
99+
var autoApprovedResult = await this._executor.ExecuteAsync( command, workingDirectory, cancellationToken );
100+
101+
AnsiConsole.WriteLine();
102+
AnsiConsole.Write( new Rule( "[blue]Request Completed[/]" ) );
103+
AnsiConsole.WriteLine();
104+
105+
// Record in history
106+
this._history.Record( sessionId, command, workingDirectory, claimedPurpose, approved: true, autoApprovedResult );
107+
108+
return autoApprovedResult;
109+
}
110+
111+
// 2. Get session history
80112
var sessionHistory = this._history.GetHistory( sessionId );
81113

82-
// 2. Risk analysis - run both AI and Regex analyzers in parallel
114+
// 3. Risk analysis - run both AI and Regex analyzers in parallel
83115
var aiTask = this._analyzer.AnalyzeAsync(
84116
command,
85117
claimedPurpose,
@@ -98,10 +130,10 @@ public async Task<CommandResult> ExecuteCommand(
98130
var aiAssessment = assessments[0];
99131
var regexAssessment = assessments[1];
100132

101-
// 3. Combine assessments (take maximum risk)
133+
// 4. Combine assessments (take maximum risk)
102134
var assessment = RiskCombiner.Combine( aiAssessment, regexAssessment );
103135

104-
// 4. Prompt user for approval (pass both assessments for display)
136+
// 5. Prompt user for approval (pass both assessments for display)
105137
var approved = await this._prompter.RequestApprovalAsync(
106138
command,
107139
claimedPurpose,
@@ -110,7 +142,7 @@ public async Task<CommandResult> ExecuteCommand(
110142
aiAssessment,
111143
regexAssessment );
112144

113-
// 5. Execute if approved
145+
// 6. Execute if approved
114146
CommandResult result;
115147

116148
if ( approved )
@@ -134,8 +166,8 @@ public async Task<CommandResult> ExecuteCommand(
134166
result = CommandResult.Rejected();
135167
}
136168

137-
// 6. Record in history
138-
this._history.Record( sessionId, command, claimedPurpose, approved, result );
169+
// 7. Record in history
170+
this._history.Record( sessionId, command, workingDirectory, claimedPurpose, approved, result );
139171

140172
return result;
141173
}

0 commit comments

Comments
 (0)