Skip to content

Critical: state-mutating commands wrongly classified as READ_ONLY (ALLOW_WRITES bypass)#1

Draft
Shimagon wants to merge 1 commit into
skrul:mainfrom
Shimagon:fix/read-only-commands-mutating-bypass-1
Draft

Critical: state-mutating commands wrongly classified as READ_ONLY (ALLOW_WRITES bypass)#1
Shimagon wants to merge 1 commit into
skrul:mainfrom
Shimagon:fix/read-only-commands-mutating-bypass-1

Conversation

@Shimagon
Copy link
Copy Markdown

@Shimagon Shimagon commented May 7, 2026

TL;DR

READ_ONLY_COMMANDS (in src/grpc/commands.ts) contains several commands that actually mutate Pro Tools state — most notably SetTrackMuteState, SetTrackSoloState, SetPlaybackMode, Copy, Undo, Redo, and SelectMemoryLocation.

Because isReadOnlyCommand() short-circuits the permission check before COMMAND_PERMISSION_GROUPS is consulted, these commands completely bypass the ALLOW_WRITES gate. An operator who launches the server with ALLOW_WRITES=memory (or any restricted set), intending to protect a client mix, can still have mute/solo flipped, the transport mode changed, or the timeline edited via undo/redo.

This is the exact situation that motivated the ALLOW_WRITES mechanism in the first place, so the bypass is high-impact for any read-only deployment.

Reproduction

  1. Start the MCP server with ALLOW_WRITES=memory in .mcp.json (read-only-ish, intended to protect tracks).
  2. From a client, call set_track_mute on any track.
  3. Observed: Pro Tools toggles the mute. Expected: the request should be rejected by the permission gate.

The same applies to set_track_solo, Copy, Undo, Redo, and SetPlaybackMode.

Fix

Move the offending commands out of READ_ONLY_COMMANDS and into COMMAND_PERMISSION_GROUPS:

  • SetTrackMuteState / SetTrackSoloStateTRACK_STATE (mute/solo are persisted in the .ptx).
  • SetPlaybackModeSESSION (Loop / Dynamic Transport persists in session prefs).
  • CopyCLIPBOARD (mutates the PT clipboard, observable side effect).
  • SelectMemoryLocation → already mapped to MEMORY; the duplicate READ_ONLY listing that short-circuited the gate is removed (this command moves the playhead and should respect the gate just like other memory ops).
  • Undo / Redo → new PermissionGroup.UNDO. They are session-mutating by definition, but I split them into a dedicated group so an operator can permit undo/redo without also unlocking SaveSession (SESSION is heavier than what undo really needs).
  • GetMemoryLocations was listed twice in READ_ONLY_COMMANDS; the duplicate is removed.

I also regrouped the surviving entries in READ_ONLY_COMMANDS with section comments explaining why each one is safe to leave gate-free (read-only query OR truly ephemeral UI state with no .ptx delta — SelectTracksByName, SetEditTool, SetTimelineSelection, TogglePlayState, PlayHalfSpeed).

Verification

  • npm run build (tsc) passes locally on this branch.
    • npm run lint is broken on main independently of this PR (ESLint v9 needs eslint.config.js); not addressed here.
  • Reviewed by 3 parallel pass (security / data-integrity / permission-map) before submission. The full audit covered more commands than this PR ships; everything outside the READ_ONLY bypass fix is intentionally deferred to keep this PR small and easy to merge.
  • Manual reproduction in my local Pro Tools 2025.10 setup: with ALLOW_WRITES=memory, set_track_mute is now rejected with a permission error. Would appreciate maintainer-side reproduction as well.

Out of scope (will be sent in follow-up PRs if welcome)

  • New PTSL command IDs (ImportAudioToClipList, SpotClipsByID, CreateAudioClips, BounceTrack).
  • Streaming RPC plumbing + subscribe_events / poll_events / unsubscribe_events MCP tools.
  • Error-payload sanitization (drop session_id / file paths from surfaced PTSL errors) for NDA-sensitive deployments.
  • Header schema fix on the streaming code path (command_idcommand).

Marked draft so the maintainer can chime in on the UNDO group naming and on whether they want the legacy getCommandPermissionLevel mapping touched (currently UNDO falls into NON_DESTRUCTIVE via the default arm, which seems right but is implicit).

…LOW_WRITES bypass)

`READ_ONLY_COMMANDS` contained several commands that mutate Pro Tools session
state. Because `isReadOnlyCommand()` short-circuits the permission check
before `COMMAND_PERMISSION_GROUPS` is consulted, these commands bypass the
`ALLOW_WRITES` gate entirely — even when the operator launches the server in
a read-only configuration (e.g. ALLOW_WRITES=memory) intending to protect a
client's mix.

Affected commands removed from READ_ONLY_COMMANDS and routed through the
permission system:

  - SetTrackMuteState / SetTrackSoloState -> TRACK_STATE
      (mute/solo flags are persisted in the .ptx session file)
  - SetPlaybackMode                       -> SESSION
      (Loop / Dynamic Transport setting persists in session prefs)
  - Copy                                  -> CLIPBOARD
      (mutates the Pro Tools clipboard, observable side effect)
  - SelectMemoryLocation                  -> MEMORY (no longer also listed
      in READ_ONLY, which used to short-circuit the gate; it moves the
      playhead, so it should require permission like other memory ops)
  - Undo / Redo                           -> new PermissionGroup.UNDO
      (mutate session by definition; isolated into its own group so
      operators can permit Undo without unlocking SaveSession)

Other minor cleanups in this file:

  - GetMemoryLocations was listed twice in READ_ONLY_COMMANDS (deduped).
  - READ_ONLY_COMMANDS regrouped with section comments describing why each
    remaining entry is safe (read-only query OR truly ephemeral UI state
    that produces no .ptx delta: SelectTracksByName, SetEditTool,
    SetTimelineSelection, TogglePlayState, PlayHalfSpeed).

Out of scope for this PR (will be sent separately):
  - New PTSL command IDs (ImportAudioToClipList, SpotClipsByID, BounceTrack, ...)
  - Streaming RPC / event-subscription tooling (SubscribeToEvents, PollEvents,
    UnsubscribeFromEvents)

Reproduction (before fix):
  1. Start the MCP server with `ALLOW_WRITES=memory` (intended read-only mix).
  2. Call set_track_mute on any track.
  3. Pro Tools mute toggles. Expected: request rejected by permission gate.

Reproduction (after fix):
  Step 2 returns a permission error (TRACK_STATE not granted).
@skrul
Copy link
Copy Markdown
Owner

skrul commented May 16, 2026

hey @Shimagon I made you a collaborator on this repo so feel free to make any changes you want, or just fork it and work in your own repo 😄

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