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:
@@ -1216,16 +1216,15 @@ function generateApprovalObserverScript(_port: number): string {
|
||||
});
|
||||
|
||||
// ── 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)
|
||||
@@ -1934,29 +1933,37 @@ 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: string) => f.endsWith('.json'));
|
||||
let resolvedCount = 0;
|
||||
let primaryCommand = '';
|
||||
const pendingFiles = fs.readdirSync(path.join(bridgePath, 'pending')).filter((f: string) => f.endsWith('.json'));
|
||||
const nowMs = Date.now();
|
||||
for (const pf of pendingFiles) {
|
||||
const pfPath = path.join(bridgePath, 'pending', pf);
|
||||
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)
|
||||
const ageMs = nowMs - (pd.timestamp * 1000);
|
||||
const isMatch = 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)}\``);
|
||||
}
|
||||
try {
|
||||
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
||||
if (pd.status !== 'pending') continue;
|
||||
if (pd.project_name && pd.project_name !== projectName) continue;
|
||||
// Limit to same session AND (same step or recent)
|
||||
const ageMs = nowMs - (pd.timestamp * 1000);
|
||||
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');
|
||||
resolvedCount++;
|
||||
const cmd = pd.command || '';
|
||||
if (cmd.length > primaryCommand.length && cmd !== 'Deny' && !cmd.includes('Allow')) {
|
||||
primaryCommand = cmd;
|
||||
} else if (!primaryCommand) {
|
||||
primaryCommand = cmd;
|
||||
}
|
||||
}
|
||||
} catch (e: any) { logToFile(`[AUTO-RESOLVE] parse error for ${pf}: ${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)}\``);
|
||||
}
|
||||
} catch (e: any) { logToFile(`[AUTO-RESOLVE] error: ${e.message}`); }
|
||||
lastPendingStepIndex = -1;
|
||||
}
|
||||
consecutiveIdleCount = 0;
|
||||
@@ -2411,40 +2418,60 @@ function setupResponseWatcher() {
|
||||
fs.mkdirSync(responseDir, { recursive: true });
|
||||
}
|
||||
|
||||
const processAnyResponse = (filename: string) => {
|
||||
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');
|
||||
} catch (e: any) {
|
||||
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: string) {
|
||||
|
||||
Reference in New Issue
Block a user