fix(bridge): auto-approve crash — DOM observer Deny filter + bot reject-word guard + AGENT rule
This commit is contained in:
@@ -19,6 +19,7 @@ description: 모든 작업에 자동 적용되는 에이전트 행동 규칙.
|
|||||||
7. NEVER truncate error messages — always show the full error output
|
7. NEVER truncate error messages — always show the full error output
|
||||||
8. NEVER say "구현 완료" or "동작 확인" without ACTUAL end-to-end test — import/문법 통과는 검증이 아님
|
8. NEVER say "구현 완료" or "동작 확인" without ACTUAL end-to-end test — import/문법 통과는 검증이 아님
|
||||||
9. NEVER confuse "코드가 논리적으로 맞음" with "실제로 동작함" — 실행 로그가 없으면 미검증
|
9. NEVER confuse "코드가 논리적으로 맞음" with "실제로 동작함" — 실행 로그가 없으면 미검증
|
||||||
|
10. NEVER fix code by looking at only the immediate file — always trace the full data flow across ALL affected files (producer → consumer → side effects) before proposing a fix
|
||||||
|
|
||||||
## ALWAYS (필수)
|
## ALWAYS (필수)
|
||||||
|
|
||||||
|
|||||||
33
bot.py
33
bot.py
@@ -564,12 +564,41 @@ class GravityBot(commands.Bot):
|
|||||||
|
|
||||||
# ── Auto-approve: if project has auto enabled, approve immediately ──
|
# ── Auto-approve: if project has auto enabled, approve immediately ──
|
||||||
if project in self.auto_approve_projects:
|
if project in self.auto_approve_projects:
|
||||||
|
# Defence: reject-word commands should NEVER be auto-approved
|
||||||
|
# (DOM observer may create standalone "Deny" pending from file_permission UI)
|
||||||
|
reject_commands = {"deny", "reject", "cancel", "decline", "dismiss", "stop"}
|
||||||
|
if req.command.strip().lower() in reject_commands:
|
||||||
|
logger.warning(f"Auto-approve BLOCKED: command='{req.command}' is reject-word — skipping")
|
||||||
self._sent_approval_ids.add(req.request_id)
|
self._sent_approval_ids.add(req.request_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._sent_approval_ids.add(req.request_id)
|
||||||
|
|
||||||
|
# Smart button_index: read buttons array from pending file
|
||||||
|
# file_permission buttons = [Allow Once(0), Allow This Conv(1), Deny(2)]
|
||||||
|
# MUST pick non-reject button for safety
|
||||||
|
approve_btn_index = 0
|
||||||
|
pending_file = self.bridge.pending_dir / f"{req.request_id}.json"
|
||||||
|
if pending_file.exists():
|
||||||
|
try:
|
||||||
|
pdata = json.loads(pending_file.read_text(encoding="utf-8-sig"))
|
||||||
|
btns = pdata.get("buttons")
|
||||||
|
if btns and len(btns) > 1:
|
||||||
|
reject_words = {"deny", "reject", "cancel", "reject all",
|
||||||
|
"decline", "dismiss", "stop"}
|
||||||
|
for b in btns:
|
||||||
|
txt = b.get("text", "").lower().strip()
|
||||||
|
if txt not in reject_words:
|
||||||
|
approve_btn_index = b.get("index", 0)
|
||||||
|
break
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
# Write auto-approve response for Extension
|
# Write auto-approve response for Extension
|
||||||
self.bridge.write_response(UserResponse(
|
self.bridge.write_response(UserResponse(
|
||||||
request_id=req.request_id,
|
request_id=req.request_id,
|
||||||
approved=True,
|
approved=True,
|
||||||
button_index=0, # first button (Allow Once / Run)
|
button_index=approve_btn_index,
|
||||||
step_type=getattr(req, 'step_type', ''),
|
step_type=getattr(req, 'step_type', ''),
|
||||||
project_name=project,
|
project_name=project,
|
||||||
))
|
))
|
||||||
@@ -583,7 +612,7 @@ class GravityBot(commands.Bot):
|
|||||||
)
|
)
|
||||||
embed.set_footer(text=f"auto-approve | {req.request_id[:12]}")
|
embed.set_footer(text=f"auto-approve | {req.request_id[:12]}")
|
||||||
await channel.send(embed=embed)
|
await channel.send(embed=embed)
|
||||||
logger.info(f"Auto-approved: {req.request_id[:12]} project={project}")
|
logger.info(f"Auto-approved: {req.request_id[:12]} project={project} btn_idx={approve_btn_index}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Defer short-command pendings (e.g. "Run") by 4 cycles (~12s)
|
# Defer short-command pendings (e.g. "Run") by 4 cycles (~12s)
|
||||||
|
|||||||
@@ -742,7 +742,7 @@ function startObserverHttpBridge() {
|
|||||||
const data = JSON.parse(body);
|
const data = JSON.parse(body);
|
||||||
// ── Server-side false positive filter ──
|
// ── Server-side false positive filter ──
|
||||||
const cmd = (data.command || '').trim();
|
const cmd = (data.command || '').trim();
|
||||||
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it)$/i;
|
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Deny|Allow Once|Allow This Conversation|Dismiss|Decline)$/i;
|
||||||
if (FALSE_POSITIVE_RE.test(cmd)) {
|
if (FALSE_POSITIVE_RE.test(cmd)) {
|
||||||
logToFile(`[HTTP] filtered false positive: "${cmd}"`);
|
logToFile(`[HTTP] filtered false positive: "${cmd}"`);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -739,7 +739,7 @@ function startObserverHttpBridge(): Promise<number> {
|
|||||||
|
|
||||||
// ── Server-side false positive filter ──
|
// ── Server-side false positive filter ──
|
||||||
const cmd = (data.command || '').trim();
|
const cmd = (data.command || '').trim();
|
||||||
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it)$/i;
|
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Deny|Allow Once|Allow This Conversation|Dismiss|Decline)$/i;
|
||||||
if (FALSE_POSITIVE_RE.test(cmd)) {
|
if (FALSE_POSITIVE_RE.test(cmd)) {
|
||||||
logToFile(`[HTTP] filtered false positive: "${cmd}"`);
|
logToFile(`[HTTP] filtered false positive: "${cmd}"`);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
|||||||
Reference in New Issue
Block a user