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:
@@ -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 {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user