initial redirection changes

This commit is contained in:
Your Name
2026-02-03 23:07:23 +00:00
parent 0012d95848
commit 26d274ffb4
10 changed files with 142 additions and 7 deletions
+7 -1
View File
@@ -21,6 +21,7 @@ export enum MessageBusType {
TOOL_CALLS_UPDATE = 'tool-calls-update',
ASK_USER_REQUEST = 'ask-user-request',
ASK_USER_RESPONSE = 'ask-user-response',
ALLOW_SESSION_REDIRECTION = 'allow-session-redirection',
}
export interface ToolCallsUpdateMessage {
@@ -171,6 +172,10 @@ export interface AskUserResponse {
cancelled?: boolean;
}
export interface AllowSessionRedirection {
type: MessageBusType.ALLOW_SESSION_REDIRECTION;
}
export type Message =
| ToolConfirmationRequest
| ToolConfirmationResponse
@@ -180,4 +185,5 @@ export type Message =
| UpdatePolicy
| AskUserRequest
| AskUserResponse
| ToolCallsUpdateMessage;
| ToolCallsUpdateMessage
| AllowSessionRedirection;
+4
View File
@@ -329,6 +329,10 @@ export function createPolicyUpdater(
policyEngine: PolicyEngine,
messageBus: MessageBus,
) {
messageBus.subscribe(MessageBusType.ALLOW_SESSION_REDIRECTION, () => {
policyEngine.setAllowSessionRedirection(true);
});
messageBus.subscribe(
MessageBusType.UPDATE_POLICY,
async (message: UpdatePolicy) => {
@@ -87,6 +87,7 @@ export class PolicyEngine {
private readonly nonInteractive: boolean;
private readonly checkerRunner?: CheckerRunner;
private approvalMode: ApprovalMode;
private allowSessionRedirection = false;
constructor(config: PolicyEngineConfig = {}, checkerRunner?: CheckerRunner) {
this.rules = (config.rules ?? []).sort(
@@ -118,11 +119,19 @@ export class PolicyEngine {
return this.approvalMode;
}
/**
* Set whether redirection is allowed for the current session.
*/
setAllowSessionRedirection(allowed: boolean): void {
this.allowSessionRedirection = allowed;
}
private shouldDowngradeForRedirection(
command: string,
allowRedirection?: boolean,
): boolean {
return (
!this.allowSessionRedirection &&
!allowRedirection &&
hasRedirection(command) &&
this.approvalMode !== ApprovalMode.AUTO_EDIT &&
@@ -130,6 +130,17 @@ describe('createPolicyUpdater', () => {
expect(parsed.rule).toHaveLength(1);
expect(parsed.rule![0].commandPrefix).toEqual(['echo', 'ls']);
});
it('should call setAllowSessionRedirection on ALLOW_SESSION_REDIRECTION message', async () => {
vi.spyOn(policyEngine, 'setAllowSessionRedirection');
createPolicyUpdater(policyEngine, messageBus);
await messageBus.publish({
type: MessageBusType.ALLOW_SESSION_REDIRECTION,
});
expect(policyEngine.setAllowSessionRedirection).toHaveBeenCalledWith(true);
});
});
describe('ShellToolInvocation Policy Update', () => {
@@ -0,0 +1,61 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { PolicyEngine } from './policy-engine.js';
import { ApprovalMode, PolicyDecision } from './types.js';
import { MessageBus } from '../confirmation-bus/message-bus.js';
import { MessageBusType } from '../confirmation-bus/types.js';
import { createPolicyUpdater } from './config.js';
describe('PolicyEngine Redirection Session', () => {
let policyEngine: PolicyEngine;
let messageBus: MessageBus;
beforeEach(() => {
policyEngine = new PolicyEngine({
defaultDecision: PolicyDecision.ALLOW,
approvalMode: ApprovalMode.DEFAULT,
});
messageBus = new MessageBus(policyEngine);
createPolicyUpdater(policyEngine, messageBus);
});
it('should downgrade for redirection by default', async () => {
const result = await policyEngine.check(
{ name: 'run_shell_command', args: { command: 'echo test > file.txt' } },
undefined,
);
expect(result.decision).toBe(PolicyDecision.ASK_USER);
});
it('should NOT downgrade for redirection after ALLOW_SESSION_REDIRECTION message', async () => {
// Publish the allow session redirection message
await messageBus.publish({
type: MessageBusType.ALLOW_SESSION_REDIRECTION,
});
const result = await policyEngine.check(
{ name: 'run_shell_command', args: { command: 'echo test > file.txt' } },
undefined,
);
expect(result.decision).toBe(PolicyDecision.ALLOW);
});
it('should still downgrade for redirection if another session starts (new PolicyEngine)', async () => {
// Simulate new session/engine
const newEngine = new PolicyEngine({
defaultDecision: PolicyDecision.ALLOW,
approvalMode: ApprovalMode.DEFAULT,
});
const result = await newEngine.check(
{ name: 'run_shell_command', args: { command: 'echo test > file.txt' } },
undefined,
);
expect(result.decision).toBe(PolicyDecision.ASK_USER);
});
});
@@ -5,7 +5,11 @@
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { BaseToolInvocation, type ToolResult } from './tools.js';
import {
BaseToolInvocation,
type ToolResult,
ToolConfirmationOutcome,
} from './tools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import {
type Message,
@@ -132,4 +136,19 @@ describe('BaseToolInvocation', () => {
// ignore abort error
}
});
it('should publish ALLOW_SESSION_REDIRECTION message when outcome is ProceedAlwaysRedirection', async () => {
const tool = new TestBaseToolInvocation({}, messageBus, 'test-tool');
// Access protected method for testing
await (
tool as unknown as {
publishPolicyUpdate(outcome: ToolConfirmationOutcome): Promise<void>;
}
).publishPolicyUpdate(ToolConfirmationOutcome.ProceedAlwaysRedirection);
expect(messageBus.publish).toHaveBeenCalledWith({
type: MessageBusType.ALLOW_SESSION_REDIRECTION,
});
});
});
+8
View File
@@ -141,6 +141,13 @@ export abstract class BaseToolInvocation<
protected async publishPolicyUpdate(
outcome: ToolConfirmationOutcome,
): Promise<void> {
if (outcome === ToolConfirmationOutcome.ProceedAlwaysRedirection) {
void this.messageBus.publish({
type: MessageBusType.ALLOW_SESSION_REDIRECTION,
});
return;
}
if (
outcome === ToolConfirmationOutcome.ProceedAlways ||
outcome === ToolConfirmationOutcome.ProceedAlwaysAndSave
@@ -777,6 +784,7 @@ export enum ToolConfirmationOutcome {
ProceedAlwaysAndSave = 'proceed_always_and_save',
ProceedAlwaysServer = 'proceed_always_server',
ProceedAlwaysTool = 'proceed_always_tool',
ProceedAlwaysRedirection = 'proceed_always_redirection',
ModifyWithEditor = 'modify_with_editor',
Cancel = 'cancel',
}