Skip to content

fix(profiles): nested refusal block_reason and explain move-client hint (Spec fix-nested-refusal) - #1468

Merged
github-actions[bot] merged 5 commits into
mainfrom
fix-nested-refusal-block-reason
Oct 2, 2026
Merged

github-actions[bot] merged 5 commits into
mainfrom
fix-nested-refusal-block-reason

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 2, 2026

Copy link
Copy Markdown
Member

Summary

Closes the two gaps the Spec 108 done-check found.

Nested profile refusals now carry block_reason (US1-5, FR-029). A call_tool or call_tools element that the caller's profile refuses inside code_execution already wrote a blocked tool_call child with parent_id, but the record had no block_reason. The child now carries profile_tier, profile_rule or profile_unannotated, the same value the top-level policy_decision carries, next to the same profile and profile_source. Every existing surface already reads EffectiveBlockReason(), so REST /activity, mcpproxy activity list|show, the Web UI drawer and the macOS Activity window show it with no change of their own. Refusals that are not profile tool-policy ones (server outside the profile, token scope or permission, quarantine, approval) are unchanged and carry no block_reason.

access explain move-client hint names the destination (FR-035). The fix read Move Cursor to work-full but the command printed mcpproxy client set-profile cursor <profile>. The fix now carries the destination slug in a new additive profile field (move_client only), and the CLI prints mcpproxy client set-profile cursor work-full. A CLI talking to a daemon that predates the field keeps the placeholder.

Decisions

  • The record stays a tool_call with status=blocked, like every other nested refusal, so ?parent_id= drill-down and usage aggregation keep working.
  • The reason crosses the jsruntime boundary through a second optional gate method, ProfilePolicyBlockReason(). ProfilePolicyRefusal() is untouched on purpose: the sandbox finds it by an anonymous interface assertion, so changing its signature would silently stop nested profile refusals from refusing. The method is pinned at compile time on the sandbox gate, and a jsruntime test proves a gate that only implements ProfilePolicyRefusal still refuses.
  • handleToolCallCompleted writes both record.BlockReason and Metadata["block_reason"], the same one-release rule as policy_decision. When there is no reason, Metadata is not allocated, so legacy records are byte-identical.
  • EmitActivityToolCallCompletedAttributed gains blockReason just before attr; the server side adds emitActivityToolCallCompletedWithBlockReason and keeps emitActivityToolCallCompleted as a delegate, and status stays at argument index 6.
  • Attribution (profile, profile_source, token, client) of the child was already correct through the sandbox's execution context; this PR adds tests that pin it.
  • The nested authz deny audit line still reports the Spec 107 reason, not the top-level other. Aligning them touches the Spec 107 audit contract and is left as a follow-up.
  • The frontend TypeScript fix type does not yet list profile. The Web UI navigates by target, so this is harmless; follow-up.

Spec

specs/108-profiles-v3 (tasks T166 to T168, Phase 17): contracts/refusals.md, contracts/mcp-tools.md, contracts/rest-api.md, contracts/cli.md, data-model.md, plan.md, tasks.md. User docs: docs/features/activity-log.md, docs/features/profiles.md, docs/cli/profile-commands.md. oas/swagger.yaml and oas/docs.go gain only runtime.Fix.profile.

Tests added

  • internal/server: TestCodeExecution_ProfileV3NestedCallBlockedBeforeUpstream (extended), TestCodeExecution_ProfileV3NestedRefusalReasons (tier, rule, unannotated, batch element, and the no-reason server-outside case, each with zero upstream dispatch), TestCodeExecution_ProfileV3NestedRefusalMatchesTopLevel, TestExplain_FixesOrderedByPreference and TestExplain_DanglingClientBindingMoveFixNamesDestination (extended and new), and the statusArgIndex guard for the new emit funnel.
  • internal/jsruntime: TestAuthzObserver_ProfileRefusalReportsBlockReason.
  • internal/runtime: TestActivityService_ToolCallCompletedPersistsBlockReason.
  • cmd/mcpproxy: TestAccessExplain_MoveClientFixNamesDestinationProfile.
  • internal/httpapi and internal/profile: the shared explain_blocked.json contract fixture now carries profile on the move_client fix.

…son (Spec 108 T166)

A call_tool refused by the caller's profile inside code_execution wrote its
blocked tool_call child without block_reason, so activity could not tell a tier
refusal from a rule or unannotated one. The sandbox gate now reports the typed
reason through a second optional method, and the child record persists it as
both the first-class field and the metadata key.
…profile (Spec 108 T167)

The move-client fix printed 'mcpproxy client set-profile <client> <profile>'
beside a label that named a concrete profile. Fix gains an additive,
move_client-only profile field and the CLI prints the real command, keeping the
placeholder for a daemon that predates the field.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4e1e404
Status: ✅  Deploy successful!
Preview URL: https://2ff3e22c.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-nested-refusal-block-rea.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 92.85714% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/server/mcp_code_execution.go 72.72% 2 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix-nested-refusal-block-reason

Available Artifacts

  • archive-darwin-amd64 (31 MB)
  • archive-darwin-arm64 (28 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (31 MB)
  • archive-windows-arm64 (27 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (27 MB)
  • installer-dmg-darwin-arm64 (24 MB)
  • smart-mcp-proxymcpproxy-goXQ9AE7.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 37068217635 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved (Model B): Paperclip review verdicts = ACCEPT and qa-gate green at this head SHA. Arming auto-merge; GitHub merges when all required checks pass.

@github-actions
github-actions Bot enabled auto-merge (squash) October 2, 2026 21:43
@github-actions
github-actions Bot merged commit a25f887 into main Oct 2, 2026
61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants