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

View File

@@ -63,6 +63,15 @@ export const ToolConfirmationMessage: React.FC<
const allowPermanentApproval =
settings.merged.security.enablePermanentToolApproval;
const containsRedirection = useMemo(() => {
if (confirmationDetails.type !== 'exec') return false;
const commandsToDisplay =
confirmationDetails.commands && confirmationDetails.commands.length > 1
? confirmationDetails.commands
: [confirmationDetails.command];
return commandsToDisplay.some((cmd) => hasRedirection(cmd));
}, [confirmationDetails]);
const handlesOwnUI =
confirmationDetails.type === 'ask_user' ||
confirmationDetails.type === 'exit_plan_mode';
@@ -149,6 +158,13 @@ export const ToolConfirmationMessage: React.FC<
key: 'Allow once',
});
if (isTrustedFolder) {
if (containsRedirection) {
options.push({
label: 'Allow redirection for this session',
value: ToolConfirmationOutcome.ProceedAlwaysRedirection,
key: 'Allow redirection for this session',
});
}
options.push({
label: `Allow for this session`,
value: ToolConfirmationOutcome.ProceedAlways,
@@ -231,6 +247,7 @@ export const ToolConfirmationMessage: React.FC<
allowPermanentApproval,
config,
isDiffingEnabled,
containsRedirection,
]);
const availableBodyContentHeight = useCallback(() => {
@@ -344,9 +361,6 @@ export const ToolConfirmationMessage: React.FC<
executionProps.commands && executionProps.commands.length > 1
? executionProps.commands
: [executionProps.command];
const containsRedirection = commandsToDisplay.some((cmd) =>
hasRedirection(cmd),
);
let bodyContentHeight = availableBodyContentHeight();
let warnings: React.ReactNode = null;
@@ -461,6 +475,7 @@ export const ToolConfirmationMessage: React.FC<
availableBodyContentHeight,
terminalWidth,
handleConfirm,
containsRedirection,
]);
if (confirmationDetails.type === 'edit') {

View File

@@ -8,7 +8,8 @@ Tip: Toggle auto-edit (Shift+Tab) to allow redirection in the future.
Allow execution of: 'echo, redirection (>)'?
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
2. Allow redirection for this session
3. Allow for this session
4. No, suggest changes (esc)
"
`;

View File

@@ -683,6 +683,7 @@ export class Session {
case ToolConfirmationOutcome.ProceedAlwaysAndSave:
case ToolConfirmationOutcome.ProceedAlwaysServer:
case ToolConfirmationOutcome.ProceedAlwaysTool:
case ToolConfirmationOutcome.ProceedAlwaysRedirection:
case ToolConfirmationOutcome.ModifyWithEditor:
break;
default: {

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;

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) => {

View File

@@ -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 &&

View File

@@ -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', () => {

View File

@@ -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);
});
});

View File

@@ -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,
});
});
});

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',
}