Skip to content

feat(tools): report pre-execution authorization decisions - #1745

Merged
Aaronontheweb merged 2 commits into
devfrom
feat/tool-authorization-reason
Aug 3, 2026
Merged

feat(tools): report pre-execution authorization decisions#1745
Aaronontheweb merged 2 commits into
devfrom
feat/tool-authorization-reason

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add one internal authorization decision at the dispatcher boundary.
  • Report Allowed, RequiresApproval, or Denied before execution or a user prompt.
  • Include an allow reason only for an allowed decision.
  • Include a deny reason only for a denied decision.
  • Include prompt context only when the decision requires approval.
  • Include structured session and persistent grant matches.

Pipeline

The dispatcher now evaluates one complete decision for each attempt.

Execution methods consume that decision through the existing exception adapter.
The session pipeline still owns the user prompt and its response.
A retry creates a new authorization decision through the same evaluator.

Telemetry

The evaluator emits telemetry when it makes the decision.

  • Allowed uses Debug and includes the allow reason and its explanation.
  • RequiresApproval uses Information and includes the outcome.
  • Denied uses Warning and includes the stable deny reason.

The telemetry does not include commands, paths, patterns, or grant details.

Scope

This PR does not change authorization behavior.
It does not test a user interaction.
It does not change model-facing tool results.
It does not change the IToolExecutor contract.
It keeps the existing public ToolAccessDecision API.

This contract gives the later approval matrix a direct result to assert.
The matrix does not need to execute a tool or drive an approval prompt.

Validation

  • Solution build passed with zero warnings and errors.
  • All 2,655 actor tests passed.
  • All 85 focused authorization tests passed.
  • Slopwatch found zero issues.
  • File header verification passed.

Copilot AI review requested due to automatic review settings August 3, 2026 19:35
@Aaronontheweb Aaronontheweb added security Security-related changes observability cleanup Code quality improvements and tech debt reduction labels Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a typed allow reason for each successful tool authorization.
It records the final allow reason in the per-attempt approval context.
It provides one exhaustive helper that maps each reason to a short operator explanation.
It adds the reason and the explanation to existing tool completion and failure logs.

Changes:

  • Add ToolAllowReason plus GetDescription() for stable operator-facing explanations.
  • Replace applied-decision strings with ToolApprovalAttempt.AllowReason and optional ApprovalEvidence.
  • Thread allow reasons through ToolAccessDecision and log them in DispatchingToolExecutor.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Netclaw.Tools.Abstractions/ToolExecutionContext.cs Add ToolAllowReason, store allow reason and approval evidence on ToolApprovalAttempt, and add a description helper.
src/Netclaw.Actors/Tools/ToolAccessPolicy.cs Add AllowReason to ToolAccessDecision and set reasons for policy auto-allow and safe-verb short-circuit.
src/Netclaw.Actors/Tools/DispatchingToolExecutor.cs Capture allow reason from authorization and include it in success and failure logs.
src/Netclaw.Actors.Tests/Tools/ToolExecutionValueObjectTests.cs Add a test that enforces a unique, non-empty description per allow reason.
src/Netclaw.Actors.Tests/Tools/ToolApprovalGateTests.cs Assert allow reasons for auto-allow and safe-verb short-circuit paths.
src/Netclaw.Actors.Tests/Tools/DispatchingToolExecutorTests.cs Assert allow reasons are recorded on context and emitted in executor logs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Netclaw.Tools.Abstractions/ToolExecutionContext.cs Outdated
Comment thread src/Netclaw.Actors.Tests/Tools/DispatchingToolExecutorTests.cs Fixed
Comment on lines +91 to +93
var projectDirectory = Path.Combine(
Path.GetTempPath(),
$"netclaw-safe-reason-{Guid.NewGuid():N}");

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working through it

Comment thread src/Netclaw.Tools.Abstractions/ToolExecutionContext.cs Outdated
Comment thread src/Netclaw.Tools.Abstractions/ToolExecutionContext.cs Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 20:29
@Aaronontheweb
Aaronontheweb force-pushed the feat/tool-authorization-reason branch from 749deac to 9afc4c2 Compare August 3, 2026 20:29
@Aaronontheweb Aaronontheweb changed the title feat(tools): report authorization allow reasons feat(tools): report pre-execution authorization decisions Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/Netclaw.Actors/Tools/ToolAccessPolicy.cs:612

  • The AllowReason property comment says it applies only to a policy result, but the executor also sets it for StoredApproval and OneTimeApproval. Update the summary so it matches actual usage.
    /// <summary>
    /// Gets the allow rule for an allowed policy result.
    /// </summary>
    internal ToolAllowReason? AllowReason { get; private init; }

Comment on lines 332 to 346
var candidatesForCheck = approvalContext.Candidates is { Count: > 0 } candidates
? candidates
.Where(c => !ApprovalPatternMatching.IsPureSideEffect(c))
.ToList()
: approvalContext.CandidateVerbs
.Select(verb => new ApprovalCandidate(verb, Directory: null))
.ToList();

if (candidatesForCheck.Count == 0)
{
// Every candidate is side-effect-only — auto-allow.
accessDecision = ToolAccessDecision.Allow();
accessDecision = ToolAccessDecision.Allow(ToolAllowReason.ApprovalExemptShellCandidates);
}
else
{

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. This fail-open branch already exists on dev; this pull request only exposes its reason.

We will fix it in #1746 immediately after this pull request merges. The fix will require at least one extracted candidate before the exemption applies. A zero-candidate result will remain RequiresApproval.

Copilot AI review requested due to automatic review settings August 3, 2026 20:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/Netclaw.Actors/Tools/DispatchingToolExecutor.cs:469

  • The ArgumentOutOfRangeException uses nameof(decision) as paramName, but the out-of-range value is decision.Outcome. This makes the exception message less precise for diagnostics.
            default:
                throw new ArgumentOutOfRangeException(nameof(decision), decision.Outcome, "Unknown authorization outcome.");

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the decision-making telemetry from the final stage of the tool approval process to the middle part of it, so we can better see what the internal plumbing is deciding. It's not interesting to test "tools approved by user was approved" - which is what would happen if we tested the TOP of the approval pipeline. Better to see what the software decided to do automatically based off a combination of ShellSyntaxTree, netclaw approvals, and some other auto-rules like denying known dangerous commands.

/// This method returns expected authorization outcomes instead of exceptions.
/// Execution adapters translate the result into the existing pipeline exceptions.
/// </remarks>
internal async Task<ToolAuthorizationDecision> EvaluateAuthorizationAsync(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - this should help make #1733 more testable when the time comes

Comment on lines 332 to 346
var candidatesForCheck = approvalContext.Candidates is { Count: > 0 } candidates
? candidates
.Where(c => !ApprovalPatternMatching.IsPureSideEffect(c))
.ToList()
: approvalContext.CandidateVerbs
.Select(verb => new ApprovalCandidate(verb, Directory: null))
.ToList();

if (candidatesForCheck.Count == 0)
{
// Every candidate is side-effect-only — auto-allow.
accessDecision = ToolAccessDecision.Allow();
accessDecision = ToolAccessDecision.Allow(ToolAllowReason.ApprovalExemptShellCandidates);
}
else
{

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. This fail-open branch already exists on dev; this pull request only exposes its reason.

We will fix it in #1746 immediately after this pull request merges. The fix will require at least one extracted candidate before the exemption applies. A zero-candidate result will remain RequiresApproval.

@Aaronontheweb
Aaronontheweb merged commit b77f8d2 into dev Aug 3, 2026
22 checks passed
@Aaronontheweb
Aaronontheweb deleted the feat/tool-authorization-reason branch August 3, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup Code quality improvements and tech debt reduction observability security Security-related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants