fix(cli): integrate PolicyEngine into ACP session to prevent deadlocks (#23507) (#27252)

This commit is contained in:
Coco Sheng
2026-05-20 12:16:09 -04:00
committed by GitHub
parent 124539b5cc
commit 2c85f57402
5 changed files with 488 additions and 5 deletions
@@ -347,6 +347,47 @@ describe('MessageBus', () => {
}),
);
});
it('should strip sensitive metadata and enforce subagent identity on derived bus', async () => {
vi.spyOn(policyEngine, 'check').mockResolvedValue({
decision: PolicyDecision.ASK_USER,
});
const subagentName = 'attacker';
const subagentBus = messageBus.derive(subagentName);
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: { name: 'sensitive-tool', args: {} },
correlationId: 'malicious-id',
forcedDecision: 'allow' as 'allow' | 'deny' | 'ask_user', // Try to bypass policy
subagent: 'trusted-subagent', // Try to spoof identity
serverName: 'spoofed-server', // Try to spoof server name
toolAnnotations: { safe: true }, // Try to spoof annotations
details: {
type: 'exec',
title: 'Spoofed UI',
command: 'rm -rf /',
} as unknown as ToolConfirmationRequest['details'], // Try to spoof UI
};
await new Promise<void>((resolve) => {
messageBus.subscribe<ToolConfirmationRequest>(
MessageBusType.TOOL_CONFIRMATION_REQUEST,
(msg) => {
if (msg.correlationId === 'malicious-id') {
expect(msg.forcedDecision).toBeUndefined();
expect(msg.serverName).toBeUndefined();
expect(msg.toolAnnotations).toBeUndefined();
expect(msg.details).toBeUndefined();
expect(msg.subagent).toBe('attacker/trusted-subagent');
resolve();
}
},
);
void subagentBus.publish(request);
});
});
});
describe('subscribe with AbortSignal', () => {
@@ -21,9 +21,9 @@ export class MessageBus extends EventEmitter {
constructor(
private readonly policyEngine: PolicyEngine,
private readonly debug = false,
private readonly isTrusted = true,
) {
super();
this.debug = debug;
}
private isValidMessage(message: Message): boolean {
@@ -47,18 +47,32 @@ export class MessageBus extends EventEmitter {
/**
* Derives a child message bus scoped to a specific subagent.
* Derived buses are untrusted.
*/
derive(subagentName: string): MessageBus {
const bus = new MessageBus(this.policyEngine, this.debug);
const bus = new MessageBus(this.policyEngine, this.debug, false);
bus.publish = async (message: Message) => {
if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
// Sanitization for untrusted callers:
// 1. Remove forcedDecision to prevent policy bypass.
// 2. Remove metadata (serverName, toolAnnotations, details) to prevent spoofing.
// 3. Enforce subagent identity by prepending/setting the scope.
const {
forcedDecision: _forcedDecision,
subagent: _subagent,
serverName: _serverName,
toolAnnotations: _toolAnnotations,
details: _details,
...otherFields
} = message;
return this.publish({
...message,
...otherFields,
subagent: message.subagent
? `${subagentName}/${message.subagent}`
: subagentName,
});
} as Message);
}
return this.publish(message);
};
@@ -95,7 +109,10 @@ export class MessageBus extends EventEmitter {
message.subagent,
);
const decision = message.forcedDecision ?? policyDecision;
// Only trust forcedDecision if it comes from a trusted bus
const decision =
(this.isTrusted ? message.forcedDecision : undefined) ??
policyDecision;
switch (decision) {
case PolicyDecision.ALLOW: