feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
90
dexto/packages/server/src/approval/approval-coordinator.ts
Normal file
90
dexto/packages/server/src/approval/approval-coordinator.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { ApprovalRequest, ApprovalResponse } from '@dexto/core';
|
||||
|
||||
/**
|
||||
* Event coordinator for approval request/response flow between handler and server.
|
||||
*
|
||||
* Provides explicit separation between agent lifecycle events (on AgentEventBus)
|
||||
* and server-mode coordination events (on ApprovalCoordinator).
|
||||
*
|
||||
* Used by:
|
||||
* - ManualApprovalHandler: Emits requests, listens for responses
|
||||
* - Streaming endpoints: Listens for requests, helps emit responses
|
||||
* - Approval routes: Emits responses from client submissions
|
||||
*/
|
||||
export class ApprovalCoordinator extends EventEmitter {
|
||||
// Track approvalId -> sessionId mapping for multi-client SSE routing
|
||||
private approvalSessions = new Map<string, string | undefined>();
|
||||
|
||||
/**
|
||||
* Emit an approval request.
|
||||
* Called by ManualApprovalHandler when tool/command needs approval.
|
||||
*/
|
||||
public emitRequest(request: ApprovalRequest): void {
|
||||
// Store sessionId mapping for later lookup when client submits response
|
||||
this.approvalSessions.set(request.approvalId, request.sessionId);
|
||||
this.emit('approval:request', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an approval response.
|
||||
* Called by API routes when user submits decision.
|
||||
*/
|
||||
public emitResponse(response: ApprovalResponse): void {
|
||||
this.emit('approval:response', response);
|
||||
// Clean up the mapping after response is emitted
|
||||
this.approvalSessions.delete(response.approvalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sessionId associated with an approval request.
|
||||
* Used by API routes to attach sessionId to responses for SSE routing.
|
||||
*/
|
||||
public getSessionId(approvalId: string): string | undefined {
|
||||
return this.approvalSessions.get(approvalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to approval requests.
|
||||
* Used by streaming endpoints to forward requests to SSE clients.
|
||||
*
|
||||
* @param handler Callback to handle approval requests
|
||||
* @param options Optional AbortSignal for cleanup
|
||||
*/
|
||||
public onRequest(
|
||||
handler: (request: ApprovalRequest) => void,
|
||||
options?: { signal?: AbortSignal }
|
||||
): void {
|
||||
const listener = (request: ApprovalRequest) => handler(request);
|
||||
this.on('approval:request', listener);
|
||||
|
||||
// Cleanup on abort signal
|
||||
if (options?.signal) {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
this.off('approval:request', listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to approval responses.
|
||||
* Used by ManualApprovalHandler to resolve pending approval promises.
|
||||
*
|
||||
* @param handler Callback to handle approval responses
|
||||
* @param options Optional AbortSignal for cleanup
|
||||
*/
|
||||
public onResponse(
|
||||
handler: (response: ApprovalResponse) => void,
|
||||
options?: { signal?: AbortSignal }
|
||||
): void {
|
||||
const listener = (response: ApprovalResponse) => handler(response);
|
||||
this.on('approval:response', listener);
|
||||
|
||||
// Cleanup on abort signal
|
||||
if (options?.signal) {
|
||||
options.signal.addEventListener('abort', () => {
|
||||
this.off('approval:response', listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import type { ApprovalRequest, ApprovalResponse } from '@dexto/core';
|
||||
import { ApprovalType, ApprovalStatus, DenialReason } from '@dexto/core';
|
||||
import { createManualApprovalHandler } from './manual-approval-handler.js';
|
||||
import type { ApprovalCoordinator } from './approval-coordinator.js';
|
||||
|
||||
describe('createManualApprovalHandler', () => {
|
||||
let mockCoordinator: ApprovalCoordinator;
|
||||
let listeners: Map<string, ((response: ApprovalResponse) => void)[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
listeners = new Map();
|
||||
|
||||
mockCoordinator = {
|
||||
on: vi.fn((event: string, listener: (response: ApprovalResponse) => void) => {
|
||||
const eventListeners = listeners.get(event) || [];
|
||||
eventListeners.push(listener);
|
||||
listeners.set(event, eventListeners);
|
||||
}),
|
||||
off: vi.fn((event: string, listener: (response: ApprovalResponse) => void) => {
|
||||
const eventListeners = listeners.get(event) || [];
|
||||
const index = eventListeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
eventListeners.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
emitRequest: vi.fn(),
|
||||
emitResponse: vi.fn(),
|
||||
} as unknown as ApprovalCoordinator;
|
||||
});
|
||||
|
||||
describe('Timeout Configuration', () => {
|
||||
it('should not timeout when timeout is undefined (infinite wait)', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-infinite-1',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
// No timeout - should wait indefinitely
|
||||
metadata: {
|
||||
toolName: 'test_tool',
|
||||
toolCallId: 'test-call-id',
|
||||
args: {},
|
||||
},
|
||||
};
|
||||
|
||||
// Start the approval request (won't resolve until we emit a response)
|
||||
const approvalPromise = handler(request);
|
||||
|
||||
// Verify the request was emitted
|
||||
expect(mockCoordinator.emitRequest).toHaveBeenCalledWith(request);
|
||||
|
||||
// Wait a bit to ensure no timeout occurred
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Manually resolve by emitting a response
|
||||
const eventListeners = listeners.get('approval:response') || [];
|
||||
eventListeners.forEach((listener) => {
|
||||
listener({
|
||||
approvalId: 'test-infinite-1',
|
||||
status: ApprovalStatus.APPROVED,
|
||||
});
|
||||
});
|
||||
|
||||
const response = await approvalPromise;
|
||||
expect(response.status).toBe(ApprovalStatus.APPROVED);
|
||||
});
|
||||
|
||||
it('should timeout when timeout is specified', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-timeout-1',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
timeout: 50, // 50ms timeout
|
||||
metadata: {
|
||||
toolName: 'test_tool',
|
||||
toolCallId: 'test-call-id',
|
||||
args: {},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await handler(request);
|
||||
|
||||
expect(response.status).toBe(ApprovalStatus.CANCELLED);
|
||||
expect(response.reason).toBe(DenialReason.TIMEOUT);
|
||||
expect(response.message).toContain('timed out');
|
||||
expect(response.timeoutMs).toBe(50);
|
||||
});
|
||||
|
||||
it('should emit timeout response to coordinator when timeout occurs', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-timeout-emit',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
timeout: 50,
|
||||
metadata: {
|
||||
toolName: 'test_tool',
|
||||
toolCallId: 'test-call-id',
|
||||
args: {},
|
||||
},
|
||||
};
|
||||
|
||||
await handler(request);
|
||||
|
||||
// Verify coordinator received the timeout response
|
||||
expect(mockCoordinator.emitResponse).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
approvalId: 'test-timeout-emit',
|
||||
status: ApprovalStatus.CANCELLED,
|
||||
reason: DenialReason.TIMEOUT,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear timeout when response is received before timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-clear-timeout',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
timeout: 5000, // 5 second timeout
|
||||
metadata: {
|
||||
toolName: 'test_tool',
|
||||
toolCallId: 'test-call-id',
|
||||
args: {},
|
||||
},
|
||||
};
|
||||
|
||||
const approvalPromise = handler(request);
|
||||
|
||||
// Emit response before timeout
|
||||
const eventListeners = listeners.get('approval:response') || [];
|
||||
eventListeners.forEach((listener) => {
|
||||
listener({
|
||||
approvalId: 'test-clear-timeout',
|
||||
status: ApprovalStatus.APPROVED,
|
||||
});
|
||||
});
|
||||
|
||||
const response = await approvalPromise;
|
||||
expect(response.status).toBe(ApprovalStatus.APPROVED);
|
||||
|
||||
// Advance time past the timeout - should not cause any issues
|
||||
vi.advanceTimersByTime(6000);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should handle elicitation with no timeout (infinite wait)', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-elicitation-infinite',
|
||||
type: ApprovalType.ELICITATION,
|
||||
timestamp: new Date(),
|
||||
// No timeout for elicitation
|
||||
metadata: {
|
||||
schema: { type: 'object' as const, properties: {} },
|
||||
prompt: 'Enter data',
|
||||
serverName: 'TestServer',
|
||||
},
|
||||
};
|
||||
|
||||
const approvalPromise = handler(request);
|
||||
|
||||
// Wait briefly
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Resolve the elicitation
|
||||
const eventListeners = listeners.get('approval:response') || [];
|
||||
eventListeners.forEach((listener) => {
|
||||
listener({
|
||||
approvalId: 'test-elicitation-infinite',
|
||||
status: ApprovalStatus.APPROVED,
|
||||
data: { formData: { name: 'test' } },
|
||||
});
|
||||
});
|
||||
|
||||
const response = await approvalPromise;
|
||||
expect(response.status).toBe(ApprovalStatus.APPROVED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cancellation Support', () => {
|
||||
it('should support cancelling pending approvals', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-cancel-1',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
toolName: 'test_tool',
|
||||
toolCallId: 'test-call-id',
|
||||
args: {},
|
||||
},
|
||||
};
|
||||
|
||||
const approvalPromise = handler(request);
|
||||
|
||||
// Cancel the approval
|
||||
handler.cancel?.('test-cancel-1');
|
||||
|
||||
const response = await approvalPromise;
|
||||
expect(response.status).toBe(ApprovalStatus.CANCELLED);
|
||||
expect(response.reason).toBe(DenialReason.SYSTEM_CANCELLED);
|
||||
});
|
||||
|
||||
it('should track pending approvals', () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request1: ApprovalRequest = {
|
||||
approvalId: 'pending-1',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
metadata: { toolName: 'tool1', toolCallId: 'test-call-id-1', args: {} },
|
||||
};
|
||||
|
||||
const request2: ApprovalRequest = {
|
||||
approvalId: 'pending-2',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
metadata: { toolName: 'tool2', toolCallId: 'test-call-id-2', args: {} },
|
||||
};
|
||||
|
||||
// Start both requests (don't await)
|
||||
handler(request1);
|
||||
handler(request2);
|
||||
|
||||
const pending = handler.getPending?.() || [];
|
||||
expect(pending).toContain('pending-1');
|
||||
expect(pending).toContain('pending-2');
|
||||
});
|
||||
|
||||
it('should cancel all pending approvals', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request1: ApprovalRequest = {
|
||||
approvalId: 'cancel-all-1',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
metadata: { toolName: 'tool1', toolCallId: 'test-call-id-1', args: {} },
|
||||
};
|
||||
|
||||
const request2: ApprovalRequest = {
|
||||
approvalId: 'cancel-all-2',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
metadata: { toolName: 'tool2', toolCallId: 'test-call-id-2', args: {} },
|
||||
};
|
||||
|
||||
const promise1 = handler(request1);
|
||||
const promise2 = handler(request2);
|
||||
|
||||
// Cancel all
|
||||
handler.cancelAll?.();
|
||||
|
||||
const [response1, response2] = await Promise.all([promise1, promise2]);
|
||||
|
||||
expect(response1.status).toBe(ApprovalStatus.CANCELLED);
|
||||
expect(response2.status).toBe(ApprovalStatus.CANCELLED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Response Handling', () => {
|
||||
it('should only handle responses for matching approvalId', async () => {
|
||||
const handler = createManualApprovalHandler(mockCoordinator);
|
||||
|
||||
const request: ApprovalRequest = {
|
||||
approvalId: 'test-match-1',
|
||||
type: ApprovalType.TOOL_CONFIRMATION,
|
||||
timestamp: new Date(),
|
||||
metadata: { toolName: 'test_tool', toolCallId: 'test-call-id', args: {} },
|
||||
};
|
||||
|
||||
const approvalPromise = handler(request);
|
||||
|
||||
// Emit response for different approvalId - should be ignored
|
||||
const eventListeners = listeners.get('approval:response') || [];
|
||||
eventListeners.forEach((listener) => {
|
||||
listener({
|
||||
approvalId: 'different-id',
|
||||
status: ApprovalStatus.APPROVED,
|
||||
});
|
||||
});
|
||||
|
||||
// Wait a bit - request should still be pending
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Now emit correct response
|
||||
eventListeners.forEach((listener) => {
|
||||
listener({
|
||||
approvalId: 'test-match-1',
|
||||
status: ApprovalStatus.DENIED,
|
||||
reason: DenialReason.USER_DENIED,
|
||||
});
|
||||
});
|
||||
|
||||
const response = await approvalPromise;
|
||||
expect(response.status).toBe(ApprovalStatus.DENIED);
|
||||
expect(response.reason).toBe(DenialReason.USER_DENIED);
|
||||
});
|
||||
});
|
||||
});
|
||||
195
dexto/packages/server/src/approval/manual-approval-handler.ts
Normal file
195
dexto/packages/server/src/approval/manual-approval-handler.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { ApprovalHandler, ApprovalRequest, ApprovalResponse } from '@dexto/core';
|
||||
import { ApprovalStatus, DenialReason } from '@dexto/core';
|
||||
import type { ApprovalCoordinator } from './approval-coordinator.js';
|
||||
|
||||
/**
|
||||
* Creates a manual approval handler that uses ApprovalCoordinator for server communication.
|
||||
*
|
||||
* This handler emits `approval:request` and waits for `approval:response` via the coordinator,
|
||||
* enabling SSE-based approval flows where:
|
||||
* 1. Handler emits approval:request → Coordinator → SSE endpoint forwards to client
|
||||
* 2. Client sends decision via POST /api/approvals/{approvalId}
|
||||
* 3. API route emits approval:response → Coordinator → Handler resolves
|
||||
*
|
||||
* The returned handler implements the optional cancellation methods (cancel, cancelAll, getPending)
|
||||
* for managing pending approval requests.
|
||||
*
|
||||
* Timeouts are handled per-request using the timeout value from ApprovalRequest, which
|
||||
* is set by ApprovalManager based on the request type (tool confirmation vs elicitation).
|
||||
*
|
||||
* @param coordinator The approval coordinator for request/response communication
|
||||
* @returns ApprovalHandler with cancellation support
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const coordinator = new ApprovalCoordinator();
|
||||
* const handler = createManualApprovalHandler(coordinator);
|
||||
* agent.setApprovalHandler(handler);
|
||||
*
|
||||
* // Later, cancel a specific approval (if handler supports it)
|
||||
* handler.cancel?.('approval-id-123');
|
||||
* ```
|
||||
*/
|
||||
export function createManualApprovalHandler(coordinator: ApprovalCoordinator): ApprovalHandler {
|
||||
// Track pending approvals for cancellation support
|
||||
const pendingApprovals = new Map<
|
||||
string,
|
||||
{
|
||||
cleanup: () => void;
|
||||
resolve: (response: ApprovalResponse) => void;
|
||||
request: ApprovalRequest;
|
||||
}
|
||||
>();
|
||||
|
||||
const handleApproval = (request: ApprovalRequest): Promise<ApprovalResponse> => {
|
||||
return new Promise<ApprovalResponse>((resolve) => {
|
||||
// Use per-request timeout (optional - undefined means no timeout)
|
||||
// - Tool confirmations use config.toolConfirmation.timeout
|
||||
// - Elicitations use config.elicitation.timeout
|
||||
const effectiveTimeout = request.timeout;
|
||||
|
||||
// Set timeout timer ONLY if timeout is specified
|
||||
// If undefined, wait indefinitely for user response
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
if (effectiveTimeout !== undefined) {
|
||||
timer = setTimeout(() => {
|
||||
cleanup();
|
||||
pendingApprovals.delete(request.approvalId);
|
||||
|
||||
// Emit timeout response so UI/clients can dismiss the prompt
|
||||
const timeoutResponse: ApprovalResponse = {
|
||||
approvalId: request.approvalId,
|
||||
status: ApprovalStatus.CANCELLED,
|
||||
sessionId: request.sessionId,
|
||||
reason: DenialReason.TIMEOUT,
|
||||
message: `Approval request timed out after ${effectiveTimeout}ms`,
|
||||
timeoutMs: effectiveTimeout,
|
||||
};
|
||||
coordinator.emitResponse(timeoutResponse);
|
||||
|
||||
// Resolve with CANCELLED response (not reject) to match auto-approve/deny behavior
|
||||
// Callers can uniformly check response.status instead of handling exceptions
|
||||
resolve(timeoutResponse);
|
||||
}, effectiveTimeout);
|
||||
}
|
||||
|
||||
// Cleanup function to remove listener and clear timeout
|
||||
let cleanupListener: (() => void) | null = null;
|
||||
const cleanup = () => {
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (cleanupListener) {
|
||||
cleanupListener();
|
||||
cleanupListener = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for approval:response events
|
||||
const listener = (res: ApprovalResponse) => {
|
||||
// Only handle responses for this specific approval
|
||||
if (res.approvalId === request.approvalId) {
|
||||
cleanup();
|
||||
pendingApprovals.delete(request.approvalId);
|
||||
resolve(res);
|
||||
}
|
||||
};
|
||||
|
||||
// Register listener
|
||||
coordinator.on('approval:response', listener);
|
||||
cleanupListener = () => coordinator.off('approval:response', listener);
|
||||
|
||||
// Store for cancellation support
|
||||
pendingApprovals.set(request.approvalId, {
|
||||
cleanup,
|
||||
resolve,
|
||||
request,
|
||||
});
|
||||
|
||||
// Emit the approval:request event via coordinator
|
||||
// SSE endpoints will subscribe to coordinator and forward to clients
|
||||
coordinator.emitRequest(request);
|
||||
});
|
||||
};
|
||||
|
||||
const handler: ApprovalHandler = Object.assign(handleApproval, {
|
||||
cancel: (approvalId: string): void => {
|
||||
const pending = pendingApprovals.get(approvalId);
|
||||
if (pending) {
|
||||
pending.cleanup();
|
||||
pendingApprovals.delete(approvalId);
|
||||
|
||||
// Create cancellation response
|
||||
const cancelResponse: ApprovalResponse = {
|
||||
approvalId,
|
||||
status: ApprovalStatus.CANCELLED,
|
||||
sessionId: pending.request.sessionId,
|
||||
reason: DenialReason.SYSTEM_CANCELLED,
|
||||
message: 'Approval request was cancelled',
|
||||
};
|
||||
|
||||
// Emit cancellation event so UI listeners can dismiss the prompt
|
||||
coordinator.emitResponse(cancelResponse);
|
||||
|
||||
// Resolve with CANCELLED response (not reject) to match auto-approve/deny behavior
|
||||
// Callers can uniformly check response.status instead of handling exceptions
|
||||
pending.resolve(cancelResponse);
|
||||
}
|
||||
},
|
||||
|
||||
cancelAll: (): void => {
|
||||
for (const [approvalId] of pendingApprovals) {
|
||||
handler.cancel?.(approvalId);
|
||||
}
|
||||
},
|
||||
|
||||
getPending: (): string[] => {
|
||||
return Array.from(pendingApprovals.keys());
|
||||
},
|
||||
|
||||
getPendingRequests: (): ApprovalRequest[] => {
|
||||
return Array.from(pendingApprovals.values()).map((p) => p.request);
|
||||
},
|
||||
|
||||
/**
|
||||
* Auto-approve pending requests that match a predicate.
|
||||
* Used when a pattern is remembered to auto-approve other parallel requests
|
||||
* that would now match the same pattern.
|
||||
*/
|
||||
autoApprovePending: (
|
||||
predicate: (request: ApprovalRequest) => boolean,
|
||||
responseData?: Record<string, unknown>
|
||||
): number => {
|
||||
let count = 0;
|
||||
|
||||
// Find all pending approvals that match the predicate
|
||||
for (const [approvalId, pending] of pendingApprovals) {
|
||||
if (predicate(pending.request)) {
|
||||
// Clean up the pending state
|
||||
pending.cleanup();
|
||||
pendingApprovals.delete(approvalId);
|
||||
|
||||
// Create auto-approval response
|
||||
const autoApproveResponse: ApprovalResponse = {
|
||||
approvalId,
|
||||
status: ApprovalStatus.APPROVED,
|
||||
sessionId: pending.request.sessionId,
|
||||
message: 'Auto-approved due to matching remembered pattern',
|
||||
data: responseData,
|
||||
};
|
||||
|
||||
// Emit response so UI can update
|
||||
coordinator.emitResponse(autoApproveResponse);
|
||||
|
||||
// Resolve the pending promise
|
||||
pending.resolve(autoApproveResponse);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
},
|
||||
});
|
||||
|
||||
return handler;
|
||||
}
|
||||
Reference in New Issue
Block a user