feat: add hook engine for sensitive operation confirmation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { HookAction, HookResult, PendingConfirmation, HookConfig } from './types.js';
|
||||
|
||||
export class HookEngine {
|
||||
private confirmPatterns: RegExp[];
|
||||
private logPatterns: RegExp[];
|
||||
private pendingConfirmations: Map<string, PendingConfirmation> = new Map();
|
||||
|
||||
constructor(config: HookConfig) {
|
||||
this.confirmPatterns = config.confirm.map(p => this.patternToRegex(p));
|
||||
this.logPatterns = config.log.map(p => this.patternToRegex(p));
|
||||
}
|
||||
|
||||
private patternToRegex(pattern: string): RegExp {
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
return new RegExp(`^${escaped}$`);
|
||||
}
|
||||
|
||||
getAction(tool: string): HookAction {
|
||||
if (this.confirmPatterns.some(p => p.test(tool))) {
|
||||
return 'confirm';
|
||||
}
|
||||
if (this.logPatterns.some(p => p.test(tool))) {
|
||||
return 'log';
|
||||
}
|
||||
return 'silent';
|
||||
}
|
||||
|
||||
async requestConfirmation(tool: string, args: Record<string, unknown>): Promise<HookResult> {
|
||||
const id = randomUUID();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const pending: PendingConfirmation = {
|
||||
id,
|
||||
tool,
|
||||
args,
|
||||
resolve,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.pendingConfirmations.set(id, pending);
|
||||
});
|
||||
}
|
||||
|
||||
resolveConfirmation(id: string, result: HookResult): boolean {
|
||||
const pending = this.pendingConfirmations.get(id);
|
||||
if (!pending) {
|
||||
return false;
|
||||
}
|
||||
|
||||
pending.resolve(result);
|
||||
this.pendingConfirmations.delete(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
getPendingConfirmations(): PendingConfirmation[] {
|
||||
return Array.from(this.pendingConfirmations.values());
|
||||
}
|
||||
|
||||
clearExpiredConfirmations(maxAgeMs: number = 5 * 60 * 1000): number {
|
||||
const now = Date.now();
|
||||
let cleared = 0;
|
||||
|
||||
for (const [id, pending] of this.pendingConfirmations) {
|
||||
if (now - pending.createdAt.getTime() > maxAgeMs) {
|
||||
pending.resolve({ approved: false, reason: 'Confirmation timed out' });
|
||||
this.pendingConfirmations.delete(id);
|
||||
cleared++;
|
||||
}
|
||||
}
|
||||
|
||||
return cleared;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user