Module: Collavre::Comment::ClaudeChannelPermission

Extended by:
ActiveSupport::Concern
Included in:
Collavre::Comment
Defined in:
app/models/collavre/comment/claude_channel_permission.rb

Overview

A native Claude Code tool-permission prompt surfaced into a topic as a structured approval comment (built by Api::V1::AgentsController#notify when the relayed notify carries a permission_request_id). It reuses the native approval comment UI — approver gate, approve button, ✅/🚫 decided label —but a decision does NOT execute a tool server-side (the tool runs inside the remote Claude Code process). Instead approve/deny relays the decision over the agent stream so the MCP plugin resolves the paused tool call.

Defined Under Namespace

Classes: AlreadyDecided

Constant Summary collapse

ACTION_TYPE =
"claude_channel_permission"

Instance Method Summary collapse

Instance Method Details

#broadcast_claude_channel_permission_decision(behavior) ⇒ Object

Relay the decision to the suspended session over the agent stream. The MCP plugin matches request_id against the prompts it surfaced, so sibling sessions sharing this (shared) agent ignore a request_id they never raised. task_id is intentionally absent: the decision only unblocks the paused tool call; the in-flight delegated task completes later via /reply.



106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/models/collavre/comment/claude_channel_permission.rb', line 106

def broadcast_claude_channel_permission_decision(behavior)
  request_id = claude_channel_permission_request_id
  return false if request_id.blank? || user_id.blank?

  AgentChannel.broadcast_to_agent(user_id, {
    type: "permission_decision",
    request_id: request_id,
    behavior: behavior.to_s,
    agent_id: user_id
  })
  true
end

#claude_channel_permission?Boolean

True when this comment’s action payload is a Claude Channel permission prompt (vs. a native execute_tool/approve_tool action).

Returns:

  • (Boolean)


63
64
65
# File 'app/models/collavre/comment/claude_channel_permission.rb', line 63

def claude_channel_permission?
  claude_channel_permission_action.present?
end

#claude_channel_permission_denied?Boolean

The decision, once made, is persisted into the action payload so the rendered comment can distinguish ✅ approved from 🚫 denied.

Returns:

  • (Boolean)


73
74
75
# File 'app/models/collavre/comment/claude_channel_permission.rb', line 73

def claude_channel_permission_denied?
  claude_channel_permission_action&.dig("decision") == "deny"
end

#claude_channel_permission_request_idObject



67
68
69
# File 'app/models/collavre/comment/claude_channel_permission.rb', line 67

def claude_channel_permission_request_id
  claude_channel_permission_action&.dig("request_id")
end

#decide_claude_channel_permission!(behavior, by:) ⇒ Object

Atomically record the human’s allow/deny: stamp action_executed_at/by (which hides the buttons and marks the comment decided) and persist the decision into the action payload. Raises AlreadyDecided if a decision was already recorded.

Raises:

  • (ArgumentError)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'app/models/collavre/comment/claude_channel_permission.rb', line 81

def decide_claude_channel_permission!(behavior, by:)
  behavior = behavior.to_s
  raise ArgumentError, "behavior must be allow or deny" unless %w[allow deny].include?(behavior)

  with_lock do
    reload
    raise AlreadyDecided if action_executed_at.present?

    payload = claude_channel_permission_action
    raise ArgumentError, "not a Claude Channel permission comment" unless payload

    payload["decision"] = behavior
    update!(
      action: JSON.pretty_generate(payload),
      action_executed_at: Time.current,
      action_executed_by: by
    )
  end
end

#rebroadcast_claude_channel_permission_decisionObject

Replay this comment’s already-recorded decision (used by the resubscribe path). Unlike broadcast_claude_channel_permission_decision the behavior is read from the persisted payload rather than passed in, and only a comment that is both a permission prompt and decided re-broadcasts — a still- pending prompt has no decision to deliver.



124
125
126
127
128
129
130
131
# File 'app/models/collavre/comment/claude_channel_permission.rb', line 124

def rebroadcast_claude_channel_permission_decision
  return false unless claude_channel_permission?

  decision = claude_channel_permission_action&.dig("decision")
  return false if decision.blank?

  broadcast_claude_channel_permission_decision(decision)
end