Skip to content

Commit b8df8b2

Browse files
feat(core): wire up UI for ASK_USER policy decisions in message bus (#10630)
1 parent 8a937eb commit b8df8b2

File tree

10 files changed

+429
-97
lines changed

10 files changed

+429
-97
lines changed

packages/a2a-server/src/utils/testing_utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export function createMockConfig(
5050
getEmbeddingModel: vi.fn().mockReturnValue('text-embedding-004'),
5151
getSessionId: vi.fn().mockReturnValue('test-session-id'),
5252
getUserTier: vi.fn(),
53+
getEnableMessageBusIntegration: vi.fn().mockReturnValue(false),
54+
getMessageBus: vi.fn(),
55+
getPolicyEngine: vi.fn(),
5356
...overrides,
5457
} as unknown as Config;
5558

packages/cli/src/ui/hooks/useToolScheduler.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const mockConfig = {
7070
getUseModelRouter: () => false,
7171
getGeminiClient: () => null, // No client needed for these tests
7272
getShellExecutionConfig: () => ({ terminalWidth: 80, terminalHeight: 24 }),
73+
getEnableMessageBusIntegration: () => false,
74+
getMessageBus: () => null,
75+
getPolicyEngine: () => null,
7376
} as unknown as Config;
7477

7578
const mockTool = new MockTool({

packages/core/src/confirmation-bus/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface ToolConfirmationResponse {
2424
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE;
2525
correlationId: string;
2626
confirmed: boolean;
27+
/**
28+
* When true, indicates that policy decision was ASK_USER and the tool should
29+
* show its legacy confirmation UI instead of auto-proceeding.
30+
*/
31+
requiresUserConfirmation?: boolean;
2732
}
2833

2934
export interface ToolPolicyRejection {

packages/core/src/core/coreToolScheduler.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ describe('CoreToolScheduler', () => {
255255
getUseSmartEdit: () => false,
256256
getUseModelRouter: () => false,
257257
getGeminiClient: () => null, // No client needed for these tests
258+
getEnableMessageBusIntegration: () => false,
259+
getMessageBus: () => null,
260+
getPolicyEngine: () => null,
258261
} as unknown as Config;
259262

260263
const scheduler = new CoreToolScheduler({
@@ -332,6 +335,9 @@ describe('CoreToolScheduler', () => {
332335
getUseSmartEdit: () => false,
333336
getUseModelRouter: () => false,
334337
getGeminiClient: () => null,
338+
getEnableMessageBusIntegration: () => false,
339+
getMessageBus: () => null,
340+
getPolicyEngine: () => null,
335341
} as unknown as Config;
336342

337343
const scheduler = new CoreToolScheduler({
@@ -365,15 +371,18 @@ describe('CoreToolScheduler', () => {
365371
describe('getToolSuggestion', () => {
366372
it('should suggest the top N closest tool names for a typo', () => {
367373
// Create mocked tool registry
374+
const mockToolRegistry = {
375+
getAllToolNames: () => ['list_files', 'read_file', 'write_file'],
376+
} as unknown as ToolRegistry;
368377
const mockConfig = {
369378
getToolRegistry: () => mockToolRegistry,
370379
getUseSmartEdit: () => false,
371380
getUseModelRouter: () => false,
372381
getGeminiClient: () => null, // No client needed for these tests
382+
getEnableMessageBusIntegration: () => false,
383+
getMessageBus: () => null,
384+
getPolicyEngine: () => null,
373385
} as unknown as Config;
374-
const mockToolRegistry = {
375-
getAllToolNames: () => ['list_files', 'read_file', 'write_file'],
376-
} as unknown as ToolRegistry;
377386

378387
// Create scheduler
379388
const scheduler = new CoreToolScheduler({
@@ -448,6 +457,9 @@ describe('CoreToolScheduler with payload', () => {
448457
getUseSmartEdit: () => false,
449458
getUseModelRouter: () => false,
450459
getGeminiClient: () => null, // No client needed for these tests
460+
getEnableMessageBusIntegration: () => false,
461+
getMessageBus: () => null,
462+
getPolicyEngine: () => null,
451463
} as unknown as Config;
452464

453465
const scheduler = new CoreToolScheduler({
@@ -768,6 +780,9 @@ describe('CoreToolScheduler edit cancellation', () => {
768780
getUseSmartEdit: () => false,
769781
getUseModelRouter: () => false,
770782
getGeminiClient: () => null, // No client needed for these tests
783+
getEnableMessageBusIntegration: () => false,
784+
getMessageBus: () => null,
785+
getPolicyEngine: () => null,
771786
} as unknown as Config;
772787

773788
const scheduler = new CoreToolScheduler({
@@ -874,6 +889,9 @@ describe('CoreToolScheduler YOLO mode', () => {
874889
getUseSmartEdit: () => false,
875890
getUseModelRouter: () => false,
876891
getGeminiClient: () => null, // No client needed for these tests
892+
getEnableMessageBusIntegration: () => false,
893+
getMessageBus: () => null,
894+
getPolicyEngine: () => null,
877895
} as unknown as Config;
878896

879897
const scheduler = new CoreToolScheduler({
@@ -981,6 +999,9 @@ describe('CoreToolScheduler request queueing', () => {
981999
getUseSmartEdit: () => false,
9821000
getUseModelRouter: () => false,
9831001
getGeminiClient: () => null, // No client needed for these tests
1002+
getEnableMessageBusIntegration: () => false,
1003+
getMessageBus: () => null,
1004+
getPolicyEngine: () => null,
9841005
} as unknown as Config;
9851006

9861007
const scheduler = new CoreToolScheduler({
@@ -1113,6 +1134,9 @@ describe('CoreToolScheduler request queueing', () => {
11131134
getUseSmartEdit: () => false,
11141135
getUseModelRouter: () => false,
11151136
getGeminiClient: () => null, // No client needed for these tests
1137+
getEnableMessageBusIntegration: () => false,
1138+
getMessageBus: () => null,
1139+
getPolicyEngine: () => null,
11161140
} as unknown as Config;
11171141

11181142
const scheduler = new CoreToolScheduler({
@@ -1215,6 +1239,9 @@ describe('CoreToolScheduler request queueing', () => {
12151239
getUseSmartEdit: () => false,
12161240
getUseModelRouter: () => false,
12171241
getGeminiClient: () => null, // No client needed for these tests
1242+
getEnableMessageBusIntegration: () => false,
1243+
getMessageBus: () => null,
1244+
getPolicyEngine: () => null,
12181245
} as unknown as Config;
12191246

12201247
const scheduler = new CoreToolScheduler({
@@ -1287,6 +1314,9 @@ describe('CoreToolScheduler request queueing', () => {
12871314
getUseSmartEdit: () => false,
12881315
getUseModelRouter: () => false,
12891316
getGeminiClient: () => null, // No client needed for these tests
1317+
getEnableMessageBusIntegration: () => false,
1318+
getMessageBus: () => null,
1319+
getPolicyEngine: () => null,
12901320
} as unknown as Config;
12911321

12921322
const testTool = new TestApprovalTool(mockConfig);
@@ -1475,6 +1505,8 @@ describe('CoreToolScheduler Sequential Execution', () => {
14751505
getUseSmartEdit: () => false,
14761506
getUseModelRouter: () => false,
14771507
getGeminiClient: () => null,
1508+
getEnableMessageBusIntegration: () => false,
1509+
getMessageBus: () => null,
14781510
} as unknown as Config;
14791511

14801512
const scheduler = new CoreToolScheduler({
@@ -1595,6 +1627,8 @@ describe('CoreToolScheduler Sequential Execution', () => {
15951627
getUseSmartEdit: () => false,
15961628
getUseModelRouter: () => false,
15971629
getGeminiClient: () => null,
1630+
getEnableMessageBusIntegration: () => false,
1631+
getMessageBus: () => null,
15981632
} as unknown as Config;
15991633

16001634
const scheduler = new CoreToolScheduler({

packages/core/src/core/coreToolScheduler.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import * as path from 'node:path';
4242
import { doesToolInvocationMatch } from '../utils/tool-utils.js';
4343
import levenshtein from 'fast-levenshtein';
4444
import { ShellToolInvocation } from '../tools/shell.js';
45+
import type { ToolConfirmationRequest } from '../confirmation-bus/types.js';
46+
import { MessageBusType } from '../confirmation-bus/types.js';
4547

4648
export type ValidatingToolCall = {
4749
status: 'validating';
@@ -352,6 +354,15 @@ export class CoreToolScheduler {
352354
this.onToolCallsUpdate = options.onToolCallsUpdate;
353355
this.getPreferredEditor = options.getPreferredEditor;
354356
this.onEditorClose = options.onEditorClose;
357+
358+
// Subscribe to message bus for ASK_USER policy decisions
359+
if (this.config.getEnableMessageBusIntegration()) {
360+
const messageBus = this.config.getMessageBus();
361+
messageBus.subscribe(
362+
MessageBusType.TOOL_CONFIRMATION_REQUEST,
363+
this.handleToolConfirmationRequest.bind(this),
364+
);
365+
}
355366
}
356367

357368
private setStatusInternal(
@@ -1160,6 +1171,26 @@ export class CoreToolScheduler {
11601171
});
11611172
}
11621173

1174+
/**
1175+
* Handle tool confirmation requests from the message bus when policy decision is ASK_USER.
1176+
* This publishes a response with requiresUserConfirmation=true to signal the tool
1177+
* that it should fall back to its legacy confirmation UI.
1178+
*/
1179+
private handleToolConfirmationRequest(
1180+
request: ToolConfirmationRequest,
1181+
): void {
1182+
// When ASK_USER policy decision is made, the message bus emits the request here.
1183+
// We respond with requiresUserConfirmation=true to tell the tool to use its
1184+
// legacy confirmation flow (which will show diffs, URLs, etc in the UI).
1185+
const messageBus = this.config.getMessageBus();
1186+
messageBus.publish({
1187+
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
1188+
correlationId: request.correlationId,
1189+
confirmed: false, // Not auto-approved
1190+
requiresUserConfirmation: true, // Use legacy UI confirmation
1191+
});
1192+
}
1193+
11631194
private async autoApproveCompatiblePendingTools(
11641195
signal: AbortSignal,
11651196
triggeringCallId: string,

packages/core/src/core/nonInteractiveToolExecutor.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ describe('executeToolCall', () => {
6262
getUseSmartEdit: () => false,
6363
getUseModelRouter: () => false,
6464
getGeminiClient: () => null, // No client needed for these tests
65+
getEnableMessageBusIntegration: () => false,
66+
getMessageBus: () => null,
67+
getPolicyEngine: () => null,
6568
} as unknown as Config;
6669

6770
abortController = new AbortController();

packages/core/src/tools/message-bus-integration.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ class TestToolInvocation extends BaseToolInvocation<TestParams, TestResult> {
5252
testValue: this.params.testParam,
5353
};
5454
}
55+
56+
override async shouldConfirmExecute(
57+
abortSignal: AbortSignal,
58+
): Promise<false> {
59+
// This conditional is here to allow testing of the case where there is no message bus.
60+
if (this.messageBus) {
61+
const decision = await this.getMessageBusDecision(abortSignal);
62+
if (decision === 'ALLOW') {
63+
return false;
64+
}
65+
if (decision === 'DENY') {
66+
throw new Error('Tool execution denied by policy');
67+
}
68+
}
69+
return false;
70+
}
5571
}
5672

5773
class TestTool extends BaseDeclarativeTool<TestParams, TestResult> {
@@ -200,7 +216,7 @@ describe('Message Bus Integration', () => {
200216
abortController.abort();
201217

202218
await expect(confirmationPromise).rejects.toThrow(
203-
'Tool confirmation aborted',
219+
'Tool execution denied by policy',
204220
);
205221
});
206222

0 commit comments

Comments
 (0)