permission_report misclassifies stale-runtime mutation blocks as missing permissions and recommends profile switching #897

Open
opened 2026-07-24 21:40:26 -05:00 by jcwalker3 · 1 comment
Owner

Problem

When a mutating tool is refused because the serving process is stale relative to master, the refusal is returned to the caller as a permission denial, carrying a permission_report that names a permission the active profile actually holds and instructs the agent to switch profiles. Both the diagnosis and the prescribed action are wrong, and the prescribed action is hazardous.

Observed shape: gitea_create_issue blocked while the daemon was behind live master returned a permission_report reporting missing_permission: gitea.issue.create and a next action of switching profile / MCP session — while gitea_whoami on the same session proved prgs-author allows gitea.issue.create.

Root cause (verified at master a4c73766f4b0cc32f7c3808688eceeb6fee74335)

  1. _profile_operation_gate (gitea_mcp_server.py:14080) returns stale-runtime reasons from _master_parity_block(op) (:14095) and runtime-mode reasons from _runtime_mode_block(op) (:14098) through the same return channel as genuine profile-permission denials.
  2. _profile_permission_block (:14344) treats any non-empty reason list as a permission denial and unconditionally attaches permission_report: _permission_block_report(required_operation) (:14363).
  3. _permission_block_report (:13823) never consults parity or runtime mode. It hardcodes missing_permission and required_permission to the requested operation (:13835:13836), then emits one of two next actions: Call gitea_activate_profile with a profile that allows ... (:13892) or Switch to the {role} MCP session ... (:13898).

The reason strings are therefore the only signal distinguishing a stale runtime from a real permission denial, and the structured field the agent is meant to trust contradicts them.

Why the prescribed action is hazardous

  • Profile switching does not clear a stale daemon; the sanctioned recovery is IDE/client reconnect, per the contract established in #685.
  • Cross-role profile activation mid-session is the exact failure #690 hardens against and #714 removed as an automatic behavior. This report reintroduces it as advice.
  • Because a real author profile already holds the permission, the agent has no way to satisfy the instruction, so runs terminate with a false root cause and no durable record of the real one.

Acceptance criteria

  • Stale-runtime and runtime-mode refusals are typed distinctly from profile-permission refusals, with distinct blocker kinds, rather than sharing one reason channel.
  • A refusal caused by staleness or runtime mode never emits a permission_report, and never names a permission the active profile holds.
  • _permission_block_report verifies the active profile actually lacks the operation before reporting it missing; if the profile holds it, the report fails closed as a diagnostic defect instead of fabricating a missing permission.
  • A stale-runtime refusal reports the sanctioned recovery only — client reconnect — and never recommends gitea_activate_profile or an MCP session switch.
  • The blocker payload states the observed heads, matching the fields returned by gitea_assess_master_parity, so the caller can confirm the classification.
  • Tests cover, for author, reviewer, merger, and reconciler profiles: stale runtime plus permitted operation (expect runtime blocker, no permission_report); fresh runtime plus forbidden operation (expect permission_report, no runtime blocker); stale runtime plus forbidden operation (both causes reported, unambiguously separated).
  • Regression: gitea_create_issue on a stale daemon under prgs-author never returns missing_permission: gitea.issue.create.

Linkage

  • #685 (closed) fixed the resolver to return typed runtime_reconnect_required; it never touched the _profile_operation_gate -> _permission_block_report path, which still misclassifies.
  • #690 (cross-role profile activation mid-session) and #714 (automatic profile substitution removed) are the safety work this report's advice contradicts.
  • #686 covers manual duplicate role-server provenance; #689 records transport-pinning to an obsolete cohort head; #708 covers namespaces reporting Connected while unattached. All three concern daemon lifecycle and attachment; none addresses how a staleness refusal is classified or reported.
  • Adjacent to the durable-failure-reporting child of epic #887, which needs the blocker classification to be correct before a report is worth persisting.

Duplicate verdict

NOT a duplicate. Verified against all open issues at master a4c73766f4b0cc32f7c3808688eceeb6fee74335. No open issue covers the classification of a staleness refusal as a permission denial, and #685, the only issue that touched adjacent code, is closed and scoped to the resolver.

## Problem When a mutating tool is refused because the serving process is stale relative to master, the refusal is returned to the caller as a **permission denial**, carrying a `permission_report` that names a permission the active profile actually holds and instructs the agent to switch profiles. Both the diagnosis and the prescribed action are wrong, and the prescribed action is hazardous. Observed shape: `gitea_create_issue` blocked while the daemon was behind live master returned a `permission_report` reporting `missing_permission: gitea.issue.create` and a next action of switching profile / MCP session — while `gitea_whoami` on the same session proved `prgs-author` allows `gitea.issue.create`. ## Root cause (verified at master `a4c73766f4b0cc32f7c3808688eceeb6fee74335`) 1. `_profile_operation_gate` (`gitea_mcp_server.py:14080`) returns stale-runtime reasons from `_master_parity_block(op)` (`:14095`) and runtime-mode reasons from `_runtime_mode_block(op)` (`:14098`) through the **same return channel** as genuine profile-permission denials. 2. `_profile_permission_block` (`:14344`) treats any non-empty reason list as a permission denial and unconditionally attaches `permission_report: _permission_block_report(required_operation)` (`:14363`). 3. `_permission_block_report` (`:13823`) never consults parity or runtime mode. It hardcodes `missing_permission` and `required_permission` to the requested operation (`:13835`–`:13836`), then emits one of two next actions: `Call gitea_activate_profile with a profile that allows ...` (`:13892`) or `Switch to the {role} MCP session ...` (`:13898`). The reason strings are therefore the only signal distinguishing a stale runtime from a real permission denial, and the structured field the agent is meant to trust contradicts them. ## Why the prescribed action is hazardous - Profile switching does not clear a stale daemon; the sanctioned recovery is IDE/client reconnect, per the contract established in #685. - Cross-role profile activation mid-session is the exact failure #690 hardens against and #714 removed as an automatic behavior. This report reintroduces it as advice. - Because a real author profile already holds the permission, the agent has no way to satisfy the instruction, so runs terminate with a false root cause and no durable record of the real one. ## Acceptance criteria - Stale-runtime and runtime-mode refusals are typed distinctly from profile-permission refusals, with distinct blocker kinds, rather than sharing one reason channel. - A refusal caused by staleness or runtime mode never emits a `permission_report`, and never names a permission the active profile holds. - `_permission_block_report` verifies the active profile actually lacks the operation before reporting it missing; if the profile holds it, the report fails closed as a diagnostic defect instead of fabricating a missing permission. - A stale-runtime refusal reports the sanctioned recovery only — client reconnect — and never recommends `gitea_activate_profile` or an MCP session switch. - The blocker payload states the observed heads, matching the fields returned by `gitea_assess_master_parity`, so the caller can confirm the classification. - Tests cover, for author, reviewer, merger, and reconciler profiles: stale runtime plus permitted operation (expect runtime blocker, no `permission_report`); fresh runtime plus forbidden operation (expect `permission_report`, no runtime blocker); stale runtime plus forbidden operation (both causes reported, unambiguously separated). - Regression: `gitea_create_issue` on a stale daemon under `prgs-author` never returns `missing_permission: gitea.issue.create`. ## Linkage - #685 (closed) fixed the *resolver* to return typed `runtime_reconnect_required`; it never touched the `_profile_operation_gate` -> `_permission_block_report` path, which still misclassifies. - #690 (cross-role profile activation mid-session) and #714 (automatic profile substitution removed) are the safety work this report's advice contradicts. - #686 covers manual duplicate role-server provenance; #689 records transport-pinning to an obsolete cohort head; #708 covers namespaces reporting Connected while unattached. All three concern daemon lifecycle and attachment; none addresses how a staleness refusal is classified or reported. - Adjacent to the durable-failure-reporting child of epic #887, which needs the blocker classification to be correct before a report is worth persisting. ## Duplicate verdict NOT a duplicate. Verified against all open issues at master `a4c73766f4b0cc32f7c3808688eceeb6fee74335`. No open issue covers the classification of a staleness refusal as a permission denial, and #685, the only issue that touched adjacent code, is closed and scoped to the resolver.
jcwalker3 added status:in-progress and removed status:ready labels 2026-07-25 00:47:48 -05:00
Author
Owner

Issue claim heartbeat

<!-- gitea-issue-claim-heartbeat:v1 --> **Issue claim heartbeat** - kind: claim - issue: #897 - branch: fix/issue-897-permission-stale-runtime-classification - phase: claimed - profile: prgs-author - pr: none - blocker: none - next_action: create worktree and begin implementation
jcwalker3 added status:pr-open and removed status:in-progress labels 2026-07-25 00:49:07 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#897