mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
feat(memory): add Auto Memory inbox flow with canonical-patch contract (#26338)
This commit is contained in:
@@ -140,7 +140,11 @@ import type { GenerateContentParameters } from '@google/genai';
|
||||
export type { MCPOAuthConfig, AnyToolInvocation, AnyDeclarativeTool };
|
||||
import type { AnyToolInvocation, AnyDeclarativeTool } from '../tools/tools.js';
|
||||
import { WorkspaceContext } from '../utils/workspaceContext.js';
|
||||
import { getWorkspaceContextOverride } from './scoped-config.js';
|
||||
import {
|
||||
getWorkspaceContextOverride,
|
||||
hasScopedAutoMemoryExtractionWriteAccess,
|
||||
hasScopedMemoryInboxAccess,
|
||||
} from './scoped-config.js';
|
||||
import { Storage } from './storage.js';
|
||||
import type { ShellExecutionConfig } from '../services/shellExecutionService.js';
|
||||
import { FileExclusions } from '../utils/ignorePatterns.js';
|
||||
@@ -3063,6 +3067,52 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.ideMode = value;
|
||||
}
|
||||
|
||||
private isScopedMemoryInboxPatchPathAllowed(
|
||||
absolutePath: string,
|
||||
resolvedPath: string,
|
||||
inboxRoot: string,
|
||||
): boolean {
|
||||
if (!hasScopedMemoryInboxAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedPath = path.resolve(absolutePath);
|
||||
const isCanonicalPatchPath = (['private', 'global'] as const).some(
|
||||
(kind) =>
|
||||
normalizedPath === path.resolve(inboxRoot, kind, 'extraction.patch'),
|
||||
);
|
||||
if (!isCanonicalPatchPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedMemoryRoot = resolveToRealPath(
|
||||
this.storage.getProjectMemoryTempDir(),
|
||||
);
|
||||
return isSubpath(resolvedMemoryRoot, resolvedPath);
|
||||
}
|
||||
|
||||
private isScopedAutoMemoryExtractionWritePathAllowed(
|
||||
absolutePath: string,
|
||||
resolvedPath: string,
|
||||
): boolean {
|
||||
if (!hasScopedAutoMemoryExtractionWriteAccess()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedSkillsMemoryDir = resolveToRealPath(
|
||||
this.storage.getProjectSkillsMemoryDir(),
|
||||
);
|
||||
if (isSubpath(resolvedSkillsMemoryDir, resolvedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.isScopedMemoryInboxPatchPathAllowed(
|
||||
absolutePath,
|
||||
resolvedPath,
|
||||
path.join(this.storage.getProjectMemoryTempDir(), '.inbox'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current FileSystemService
|
||||
*/
|
||||
@@ -3077,12 +3127,48 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
* file (the latter is the only file under `~/.gemini/` that is reachable —
|
||||
* settings, credentials, keybindings, etc. remain disallowed).
|
||||
*
|
||||
* One subtree is *carved back out*: `<projectMemoryDir>/.inbox/` is owned by
|
||||
* the auto-memory extraction agent and the `/memory inbox` review flow. The
|
||||
* main agent is denied access to it even though it falls inside the project
|
||||
* temp dir; the extraction agent receives a narrow execution-scoped exception
|
||||
* for `.inbox/{private,global}/extraction.patch`.
|
||||
*
|
||||
* @param absolutePath The absolute path to check.
|
||||
* @returns true if the path is allowed, false otherwise.
|
||||
*/
|
||||
isPathAllowed(absolutePath: string): boolean {
|
||||
const resolvedPath = resolveToRealPath(absolutePath);
|
||||
|
||||
// The auto-memory inbox (`<projectMemoryDir>/.inbox/`) is owned by the
|
||||
// background extraction agent and the `/memory inbox` review flow. The
|
||||
// main agent must NOT drop files into it directly (that would let the
|
||||
// model bypass review). Deny first, even if the path also satisfies the
|
||||
// workspace or project-temp allowlists below.
|
||||
const inboxRoot = path.join(
|
||||
this.storage.getProjectMemoryTempDir(),
|
||||
'.inbox',
|
||||
);
|
||||
const resolvedInboxRoot = resolveToRealPath(inboxRoot);
|
||||
const normalizedPath = path.resolve(absolutePath);
|
||||
const normalizedInboxRoot = path.resolve(inboxRoot);
|
||||
if (
|
||||
resolvedPath === resolvedInboxRoot ||
|
||||
isSubpath(resolvedInboxRoot, resolvedPath) ||
|
||||
normalizedPath === normalizedInboxRoot ||
|
||||
isSubpath(normalizedInboxRoot, normalizedPath)
|
||||
) {
|
||||
if (
|
||||
this.isScopedMemoryInboxPatchPathAllowed(
|
||||
absolutePath,
|
||||
resolvedPath,
|
||||
inboxRoot,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const workspaceContext = this.getWorkspaceContext();
|
||||
if (workspaceContext.isPathWithinWorkspace(resolvedPath)) {
|
||||
return true;
|
||||
@@ -3122,6 +3208,19 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
absolutePath: string,
|
||||
checkType: 'read' | 'write' = 'write',
|
||||
): string | null {
|
||||
if (checkType === 'write' && hasScopedAutoMemoryExtractionWriteAccess()) {
|
||||
const resolvedPath = resolveToRealPath(absolutePath);
|
||||
if (
|
||||
this.isScopedAutoMemoryExtractionWritePathAllowed(
|
||||
absolutePath,
|
||||
resolvedPath,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return `Auto-memory extraction write denied: Attempted path "${absolutePath}" is outside the extraction write allowlist. Extraction may only write extracted skills under ${this.storage.getProjectSkillsMemoryDir()} and canonical inbox patches under ${path.join(this.storage.getProjectMemoryTempDir(), '.inbox', '{private,global}', 'extraction.patch')}.`;
|
||||
}
|
||||
|
||||
// For read operations, check read-only paths first
|
||||
if (checkType === 'read') {
|
||||
if (this.getWorkspaceContext().isPathReadable(absolutePath)) {
|
||||
|
||||
Reference in New Issue
Block a user