feat: proto-based RPC approval for Run commands via Discord
Decoded HandleCascadeUserInteractionRequest protobuf schema from AG's extension.js (message #162, base64 FileDescriptor 78KB). Working payload (variant PROTO-0): cascadeId + interaction.{trajectoryId, stepIndex, runCommand.confirm} Changes: - extension.ts: Added Strategy 0-PROTO with decoded proto RPC call - extension.ts: Fixed processResponseFile to call tryApprovalStrategies() instead of direct clickTrigger (was bypassing all strategies) - extension.ts: Fixed false positive Run detection (sessionStalled reset when step_probe confirms no WAITING) - extension.ts: Moved lastPendingStepIndex to module scope - extension.ts: Added activeTrajectoryId tracking from session init - bot.py: Added MERGE detection + Discord message edit for command updates - bot.py: Added _sent_commands tracking for merge detection Proto RE methodology: 1. Found schema exports in AG extension.js 2. Located fileDesc() with base64 protobuf descriptor 3. Decoded 58KB raw proto, found message names 4. Extracted CascadeRunCommandInteraction.confirm field 5. Tested camelCase JSON via ConnectRPC = SUCCESS
This commit is contained in:
49
bot.py
49
bot.py
@@ -165,6 +165,7 @@ class GravityBot(commands.Bot):
|
||||
self.session_status_messages: dict[str, int] = {} # conv_id → msg_id
|
||||
self._sent_approval_ids: set[str] = set()
|
||||
self._deferred_ids: dict[str, int] = {} # request_id → defer count
|
||||
self._sent_commands: dict[str, str] = {} # request_id → command text (for MERGE edit detection)
|
||||
self._ready_event = asyncio.Event()
|
||||
self._channel_lock = asyncio.Lock()
|
||||
self.bridge = BridgeProtocol()
|
||||
@@ -542,6 +543,7 @@ class GravityBot(commands.Bot):
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
self._sent_approval_ids.add(req.request_id)
|
||||
self._sent_commands[req.request_id] = req.command
|
||||
await self._send_approval_request(channel, req)
|
||||
|
||||
# ── Check for auto_resolved pendings (approved directly in AG) ──
|
||||
@@ -567,6 +569,53 @@ class GravityBot(commands.Bot):
|
||||
pass
|
||||
f.unlink()
|
||||
self._deferred_ids.pop(data.get("request_id", ""), None)
|
||||
self._sent_commands.pop(data.get("request_id", ""), None)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
# ── Check for MERGE updates (step_probe updated command in already-sent pending) ──
|
||||
for f in self.bridge.pending_dir.glob("*.json"):
|
||||
try:
|
||||
data = json.loads(f.read_text(encoding="utf-8-sig"))
|
||||
rid = data.get("request_id", "")
|
||||
if rid not in self._sent_approval_ids:
|
||||
continue
|
||||
if data.get("status") != "pending":
|
||||
continue
|
||||
msg_id = data.get("discord_message_id", 0)
|
||||
if not msg_id:
|
||||
continue
|
||||
# Check if command was updated via MERGE
|
||||
new_cmd = data.get("command", "")
|
||||
old_cmd = self._sent_commands.get(rid, "")
|
||||
if new_cmd and new_cmd != old_cmd and len(new_cmd) > len(old_cmd):
|
||||
# MERGE detected — edit Discord message
|
||||
self._sent_commands[rid] = new_cmd
|
||||
project = data.get("project_name", Config.PROJECT_NAME)
|
||||
channel = await self._get_channel(project)
|
||||
if channel:
|
||||
try:
|
||||
msg = await channel.fetch_message(msg_id)
|
||||
# Rebuild embed with full command
|
||||
buttons = data.get("buttons")
|
||||
desc_parts = [f"**명령어:**\n```\n{new_cmd[:1000]}\n```"]
|
||||
if buttons and len(buttons) > 1:
|
||||
btn_names = [b.get("text", "?") for b in buttons]
|
||||
desc_parts.append(f"**선택지:** {' / '.join(btn_names)}")
|
||||
desc = data.get("description", "")
|
||||
if desc:
|
||||
desc_parts.append(desc[:500])
|
||||
embed = discord.Embed(
|
||||
title="⚠️ 승인 요청",
|
||||
description="\n".join(desc_parts),
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
embed.set_footer(text=f"ID: {rid}")
|
||||
await msg.edit(embed=embed)
|
||||
logger.info(f"MERGE edit: {rid[:12]} cmd='{new_cmd[:60]}'")
|
||||
except discord.NotFound:
|
||||
pass
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user