mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
feat(policy): support auto-add to policy by default and scoped persistence (#20361)
This commit is contained in:
@@ -20,11 +20,14 @@ import {
|
||||
type ToolLocation,
|
||||
type ToolResult,
|
||||
type ToolResultDisplay,
|
||||
type PolicyUpdateOptions,
|
||||
} from './tools.js';
|
||||
import { buildFilePathArgsPattern } from '../policy/utils.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||
import { isNodeError } from '../utils/errors.js';
|
||||
import { correctPath } from '../utils/pathCorrector.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { ApprovalMode } from '../policy/types.js';
|
||||
import { CoreToolCallStatus } from '../scheduler/types.js';
|
||||
@@ -44,7 +47,6 @@ import {
|
||||
logEditCorrectionEvent,
|
||||
} from '../telemetry/loggers.js';
|
||||
|
||||
import { correctPath } from '../utils/pathCorrector.js';
|
||||
import {
|
||||
EDIT_TOOL_NAME,
|
||||
READ_FILE_TOOL_NAME,
|
||||
@@ -442,6 +444,8 @@ class EditToolInvocation
|
||||
extends BaseToolInvocation<EditToolParams, ToolResult>
|
||||
implements ToolInvocation<EditToolParams, ToolResult>
|
||||
{
|
||||
private readonly resolvedPath: string;
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
params: EditToolParams,
|
||||
@@ -450,10 +454,31 @@ class EditToolInvocation
|
||||
displayName?: string,
|
||||
) {
|
||||
super(params, messageBus, toolName, displayName);
|
||||
if (!path.isAbsolute(this.params.file_path)) {
|
||||
const result = correctPath(this.params.file_path, this.config);
|
||||
if (result.success) {
|
||||
this.resolvedPath = result.correctedPath;
|
||||
} else {
|
||||
this.resolvedPath = path.resolve(
|
||||
this.config.getTargetDir(),
|
||||
this.params.file_path,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.resolvedPath = this.params.file_path;
|
||||
}
|
||||
}
|
||||
|
||||
override toolLocations(): ToolLocation[] {
|
||||
return [{ path: this.params.file_path }];
|
||||
return [{ path: this.resolvedPath }];
|
||||
}
|
||||
|
||||
override getPolicyUpdateOptions(
|
||||
_outcome: ToolConfirmationOutcome,
|
||||
): PolicyUpdateOptions | undefined {
|
||||
return {
|
||||
argsPattern: buildFilePathArgsPattern(this.params.file_path),
|
||||
};
|
||||
}
|
||||
|
||||
private async attemptSelfCorrection(
|
||||
@@ -471,7 +496,7 @@ class EditToolInvocation
|
||||
const initialContentHash = hashContent(currentContent);
|
||||
const onDiskContent = await this.config
|
||||
.getFileSystemService()
|
||||
.readTextFile(params.file_path);
|
||||
.readTextFile(this.resolvedPath);
|
||||
const onDiskContentHash = hashContent(onDiskContent.replace(/\r\n/g, '\n'));
|
||||
|
||||
if (initialContentHash !== onDiskContentHash) {
|
||||
@@ -582,7 +607,7 @@ class EditToolInvocation
|
||||
try {
|
||||
currentContent = await this.config
|
||||
.getFileSystemService()
|
||||
.readTextFile(params.file_path);
|
||||
.readTextFile(this.resolvedPath);
|
||||
originalLineEnding = detectLineEnding(currentContent);
|
||||
currentContent = currentContent.replace(/\r\n/g, '\n');
|
||||
fileExists = true;
|
||||
@@ -615,7 +640,7 @@ class EditToolInvocation
|
||||
isNewFile: false,
|
||||
error: {
|
||||
display: `File not found. Cannot apply edit. Use an empty old_string to create a new file.`,
|
||||
raw: `File not found: ${params.file_path}`,
|
||||
raw: `File not found: ${this.resolvedPath}`,
|
||||
type: ToolErrorType.FILE_NOT_FOUND,
|
||||
},
|
||||
originalLineEnding,
|
||||
@@ -630,7 +655,7 @@ class EditToolInvocation
|
||||
isNewFile: false,
|
||||
error: {
|
||||
display: `Failed to read content of file.`,
|
||||
raw: `Failed to read content of existing file: ${params.file_path}`,
|
||||
raw: `Failed to read content of existing file: ${this.resolvedPath}`,
|
||||
type: ToolErrorType.READ_CONTENT_FAILURE,
|
||||
},
|
||||
originalLineEnding,
|
||||
@@ -645,7 +670,7 @@ class EditToolInvocation
|
||||
isNewFile: false,
|
||||
error: {
|
||||
display: `Failed to edit. Attempted to create a file that already exists.`,
|
||||
raw: `File already exists, cannot create: ${params.file_path}`,
|
||||
raw: `File already exists, cannot create: ${this.resolvedPath}`,
|
||||
type: ToolErrorType.ATTEMPT_TO_CREATE_EXISTING_FILE,
|
||||
},
|
||||
originalLineEnding,
|
||||
@@ -727,7 +752,7 @@ class EditToolInvocation
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileName = path.basename(this.params.file_path);
|
||||
const fileName = path.basename(this.resolvedPath);
|
||||
const fileDiff = Diff.createPatch(
|
||||
fileName,
|
||||
editData.currentContent ?? '',
|
||||
@@ -739,14 +764,14 @@ class EditToolInvocation
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
const ideConfirmation =
|
||||
this.config.getIdeMode() && ideClient.isDiffingEnabled()
|
||||
? ideClient.openDiff(this.params.file_path, editData.newContent)
|
||||
? ideClient.openDiff(this.resolvedPath, editData.newContent)
|
||||
: undefined;
|
||||
|
||||
const confirmationDetails: ToolEditConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: `Confirm Edit: ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`,
|
||||
title: `Confirm Edit: ${shortenPath(makeRelative(this.resolvedPath, this.config.getTargetDir()))}`,
|
||||
fileName,
|
||||
filePath: this.params.file_path,
|
||||
filePath: this.resolvedPath,
|
||||
fileDiff,
|
||||
originalContent: editData.currentContent,
|
||||
newContent: editData.newContent,
|
||||
@@ -771,7 +796,7 @@ class EditToolInvocation
|
||||
|
||||
getDescription(): string {
|
||||
const relativePath = makeRelative(
|
||||
this.params.file_path,
|
||||
this.resolvedPath,
|
||||
this.config.getTargetDir(),
|
||||
);
|
||||
if (this.params.old_string === '') {
|
||||
@@ -797,11 +822,7 @@ class EditToolInvocation
|
||||
* @returns Result of the edit operation
|
||||
*/
|
||||
async execute(signal: AbortSignal): Promise<ToolResult> {
|
||||
const resolvedPath = path.resolve(
|
||||
this.config.getTargetDir(),
|
||||
this.params.file_path,
|
||||
);
|
||||
const validationError = this.config.validatePathAccess(resolvedPath);
|
||||
const validationError = this.config.validatePathAccess(this.resolvedPath);
|
||||
if (validationError) {
|
||||
return {
|
||||
llmContent: validationError,
|
||||
@@ -843,7 +864,7 @@ class EditToolInvocation
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ensureParentDirectoriesExistAsync(this.params.file_path);
|
||||
await this.ensureParentDirectoriesExistAsync(this.resolvedPath);
|
||||
let finalContent = editData.newContent;
|
||||
|
||||
// Restore original line endings if they were CRLF, or use OS default for new files
|
||||
@@ -856,15 +877,15 @@ class EditToolInvocation
|
||||
}
|
||||
await this.config
|
||||
.getFileSystemService()
|
||||
.writeTextFile(this.params.file_path, finalContent);
|
||||
.writeTextFile(this.resolvedPath, finalContent);
|
||||
|
||||
let displayResult: ToolResultDisplay;
|
||||
if (editData.isNewFile) {
|
||||
displayResult = `Created ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`;
|
||||
displayResult = `Created ${shortenPath(makeRelative(this.resolvedPath, this.config.getTargetDir()))}`;
|
||||
} else {
|
||||
// Generate diff for display, even though core logic doesn't technically need it
|
||||
// The CLI wrapper will use this part of the ToolResult
|
||||
const fileName = path.basename(this.params.file_path);
|
||||
const fileName = path.basename(this.resolvedPath);
|
||||
const fileDiff = Diff.createPatch(
|
||||
fileName,
|
||||
editData.currentContent ?? '', // Should not be null here if not isNewFile
|
||||
@@ -883,7 +904,7 @@ class EditToolInvocation
|
||||
displayResult = {
|
||||
fileDiff,
|
||||
fileName,
|
||||
filePath: this.params.file_path,
|
||||
filePath: this.resolvedPath,
|
||||
originalContent: editData.currentContent,
|
||||
newContent: editData.newContent,
|
||||
diffStat,
|
||||
@@ -893,8 +914,8 @@ class EditToolInvocation
|
||||
|
||||
const llmSuccessMessageParts = [
|
||||
editData.isNewFile
|
||||
? `Created new file: ${this.params.file_path} with provided content.`
|
||||
: `Successfully modified file: ${this.params.file_path} (${editData.occurrences} replacements).`,
|
||||
? `Created new file: ${this.resolvedPath} with provided content.`
|
||||
: `Successfully modified file: ${this.resolvedPath} (${editData.occurrences} replacements).`,
|
||||
];
|
||||
|
||||
// Return a diff of the file before and after the write so that the agent
|
||||
@@ -985,16 +1006,20 @@ export class EditTool
|
||||
return "The 'file_path' parameter must be non-empty.";
|
||||
}
|
||||
|
||||
let filePath = params.file_path;
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
// Attempt to auto-correct to an absolute path
|
||||
const result = correctPath(filePath, this.config);
|
||||
if (!result.success) {
|
||||
return result.error;
|
||||
let resolvedPath: string;
|
||||
if (!path.isAbsolute(params.file_path)) {
|
||||
const result = correctPath(params.file_path, this.config);
|
||||
if (result.success) {
|
||||
resolvedPath = result.correctedPath;
|
||||
} else {
|
||||
resolvedPath = path.resolve(
|
||||
this.config.getTargetDir(),
|
||||
params.file_path,
|
||||
);
|
||||
}
|
||||
filePath = result.correctedPath;
|
||||
} else {
|
||||
resolvedPath = params.file_path;
|
||||
}
|
||||
params.file_path = filePath;
|
||||
|
||||
const newPlaceholders = detectOmissionPlaceholders(params.new_string);
|
||||
if (newPlaceholders.length > 0) {
|
||||
@@ -1009,7 +1034,7 @@ export class EditTool
|
||||
}
|
||||
}
|
||||
|
||||
return this.config.validatePathAccess(params.file_path);
|
||||
return this.config.validatePathAccess(resolvedPath);
|
||||
}
|
||||
|
||||
protected createInvocation(
|
||||
|
||||
Reference in New Issue
Block a user