fix(bridge): system audit + 5-file bug fix — PATS Deny trigger removal, auto_resolved chat dedup, UUID filenames, IP rate limit leak, bot.py deque

This commit is contained in:
2026-03-15 22:59:47 +09:00
parent 429cae47b7
commit c9f44afcf1
9 changed files with 193 additions and 107 deletions

View File

@@ -1222,16 +1222,15 @@ function generateApprovalObserverScript(_port) {
});
// ── Button patterns to detect (order matters: first match wins per scan) ──
// ONLY positive triggers should initiate a pending request group.
// Negative/secondary buttons (Deny, Reject, Dismiss) will be collected as siblings.
var PATS=[
{re:/^Run/i, type:'terminal_command'},
{re:/^Accept all$/i, type:'diff_review'},
{re:/^Reject all$/i, type:'diff_review'},
{re:/^Accept$/i, type:'agent_step'},
{re:/^Allow/i, type:'permission'},
{re:/^Approve/i, type:'agent_step'},
{re:/^Deny$/i, type:'permission'},
{re:/^Retry$/i, type:'error_recovery'},
{re:/^Dismiss$/i, type:'error_recovery'},
];
// ALL actionable button patterns (for grouping siblings in same container)
@@ -1932,33 +1931,42 @@ function setupMonitor() {
// Steps progressed — if we had a pending approval, it was handled in AG directly
if (!sawRunningAfterPending && lastPendingStepIndex >= 0) {
// Mark pending as auto_resolved so bot can update Discord message
try {
const pendingFiles = fs.readdirSync(path.join(bridgePath, 'pending'))
.filter((f) => f.endsWith('.json'));
const nowMs = Date.now();
for (const pf of pendingFiles) {
const pfPath = path.join(bridgePath, 'pending', pf);
let resolvedCount = 0;
let primaryCommand = '';
const pendingFiles = fs.readdirSync(path.join(bridgePath, 'pending')).filter((f) => f.endsWith('.json'));
const nowMs = Date.now();
for (const pf of pendingFiles) {
const pfPath = path.join(bridgePath, 'pending', pf);
try {
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
if (pd.status !== 'pending')
continue;
// Skip other projects' pendings
if (pd.project_name && pd.project_name !== projectName)
continue;
// Match by step_index OR by recency (< 60s, any source)
// Limit to same session AND (same step or recent)
const ageMs = nowMs - (pd.timestamp * 1000);
const isMatch = pd.step_index === lastPendingStepIndex
|| (ageMs < 60_000 && ageMs >= 0);
const isMatch = (pd.conversation_id === activeSessionId) &&
(pd.step_index === lastPendingStepIndex || (ageMs < 60_000 && ageMs >= 0));
if (isMatch) {
pd.status = 'auto_resolved';
fs.writeFileSync(pfPath, JSON.stringify(pd, null, 2), 'utf-8');
logToFile(`[AUTO-RESOLVE] step=${lastPendingStepIndex} progressed → marked ${pf} (age=${Math.round(ageMs / 1000)}s)`);
// FIX #3: Notify Discord that user approved locally
writeChatSnapshot(`✅ **AG에서 직접 승인됨** (step ${lastPendingStepIndex})\n\n\`${(pd.command || '').substring(0, 200)}\``);
resolvedCount++;
const cmd = pd.command || '';
if (cmd.length > primaryCommand.length && cmd !== 'Deny' && !cmd.includes('Allow')) {
primaryCommand = cmd;
}
else if (!primaryCommand) {
primaryCommand = cmd;
}
}
}
catch (e) {
logToFile(`[AUTO-RESOLVE] parse error for ${pf}: ${e.message}`);
}
}
catch (e) {
logToFile(`[AUTO-RESOLVE] error: ${e.message}`);
if (resolvedCount > 0) {
logToFile(`[AUTO-RESOLVE] step=${lastPendingStepIndex} progressed → marked ${resolvedCount} pending(s)`);
writeChatSnapshot(`✅ **AG에서 직접 진행됨** (step ${lastPendingStepIndex})\n\n\`${primaryCommand.substring(0, 200)}\``);
}
lastPendingStepIndex = -1;
}
@@ -2437,37 +2445,53 @@ function setupResponseWatcher() {
if (!fs.existsSync(responseDir)) {
fs.mkdirSync(responseDir, { recursive: true });
}
const processAnyResponse = (filename) => {
const fp = path.join(responseDir, filename);
if (fs.existsSync(fp)) {
// Check if this response belongs to our project
const rid = filename.replace('.json', '');
const pendingFile = path.join(bridgePath, 'pending', `${rid}.json`);
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pending.project_name && pending.project_name !== projectName) {
// logToFile(`[RESPONSE] skip ${rid} (project=${pending.project_name}, we=${projectName})`);
return; // Not our project
}
}
catch { }
}
else {
// Pending file missing (deleted or auto_resolved) — check response data itself
try {
const respData = JSON.parse(fs.readFileSync(fp, 'utf-8'));
if (respData.project_name && respData.project_name !== projectName) {
// logToFile(`[RESPONSE] skip (from resp data) ${rid} (project=${respData.project_name}, we=${projectName})`);
return;
}
}
catch { }
}
setTimeout(() => processResponseFile(fp), 300);
}
};
const pollAllResponses = () => {
try {
if (!fs.existsSync(responseDir))
return;
for (const f of fs.readdirSync(responseDir)) {
if (f.endsWith('.json')) {
processAnyResponse(f);
}
}
}
catch { }
};
pollAllResponses(); // Process any existing responses on startup
try {
responseWatcher = fs.watch(responseDir, (event, filename) => {
if (filename && filename.endsWith('.json') && event === 'rename') {
const fp = path.join(responseDir, filename);
if (fs.existsSync(fp)) {
// Check if this response belongs to our project
const rid = filename.replace('.json', '');
const pendingFile = path.join(bridgePath, 'pending', `${rid}.json`);
if (fs.existsSync(pendingFile)) {
try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
if (pending.project_name && pending.project_name !== projectName) {
logToFile(`[RESPONSE] skip ${rid} (project=${pending.project_name}, we=${projectName})`);
return; // Not our project
}
}
catch { }
}
else {
// Pending file missing (deleted or auto_resolved) — check response data itself
try {
const respData = JSON.parse(fs.readFileSync(fp, 'utf-8'));
if (respData.project_name && respData.project_name !== projectName) {
logToFile(`[RESPONSE] skip (from resp data) ${rid} (project=${respData.project_name}, we=${projectName})`);
return;
}
}
catch { }
}
setTimeout(() => processResponseFile(fp), 300);
}
processAnyResponse(filename);
}
});
console.log('Gravity Bridge: response watcher started');
@@ -2475,6 +2499,8 @@ function setupResponseWatcher() {
catch (e) {
console.log(`Gravity Bridge: response watcher failed: ${e.message}`);
}
// Polling fallback: fs.watch on Windows can silently fail
setInterval(pollAllResponses, 3000);
}
async function processResponseFile(filePath) {
try {