fix(coding-agent): avoid project trust prompt for update (#5674)

This commit is contained in:
Armin Ronacher
2026-06-12 23:37:16 +02:00
committed by GitHub
parent a7cdc679e7
commit b4bff7f0d0
15 changed files with 185 additions and 125 deletions

View File

@@ -3,7 +3,7 @@ import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/typ
import type { DefaultProjectTrust } from "./settings-manager.ts";
import {
getProjectTrustOptions,
hasProjectTrustInputs,
hasTrustRequiringProjectResources,
type ProjectTrustOption,
type ProjectTrustStore,
} from "./trust-manager.ts";
@@ -46,7 +46,7 @@ export async function resolveProjectTrusted(options: ResolveProjectTrustedOption
if (options.trustOverride !== undefined) {
return options.trustOverride;
}
if (!hasProjectTrustInputs(options.cwd)) {
if (!hasTrustRequiringProjectResources(options.cwd)) {
return true;
}

View File

@@ -1,4 +1,5 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import lockfile from "proper-lockfile";
import { CONFIG_DIR_NAME } from "../config.ts";
@@ -25,6 +26,16 @@ export interface ProjectTrustOption {
type TrustFile = Record<string, boolean | null | undefined>;
const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
"settings.json",
"extensions",
"skills",
"prompts",
"themes",
"SYSTEM.md",
"APPEND_SYSTEM.md",
] as const;
function normalizeCwd(cwd: string): string {
return canonicalizePath(resolvePath(cwd));
}
@@ -45,18 +56,14 @@ function findNearestTrustEntry(data: TrustFile, cwd: string): ProjectTrustStoreE
}
}
export function getProjectTrustPath(cwd: string): string {
return normalizeCwd(cwd);
}
export function getProjectTrustParentPath(cwd: string): string | undefined {
const trustPath = getProjectTrustPath(cwd);
const trustPath = normalizeCwd(cwd);
const parentDir = dirname(trustPath);
return parentDir === trustPath ? undefined : parentDir;
}
export function getProjectTrustOptions(cwd: string, options?: { includeSessionOnly?: boolean }): ProjectTrustOption[] {
const trustPath = getProjectTrustPath(cwd);
const trustPath = normalizeCwd(cwd);
const trustOptions: ProjectTrustOption[] = [
{ label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },
];
@@ -167,18 +174,26 @@ function withTrustFileLock<T>(path: string, fn: () => T): T {
}
}
export function hasProjectConfigDir(cwd: string): boolean {
return existsSync(join(canonicalizePath(resolvePath(cwd)), CONFIG_DIR_NAME));
}
export function hasProjectTrustInputs(cwd: string): boolean {
/**
* Returns true when cwd has project-local resources that must be gated by
* project trust: trust-requiring entries under cwd/.pi, or .agents/skills in
* cwd or one of its ancestors. Returns false when no such project resources
* exist. The user/global ~/.agents/skills directory is always treated as a
* trusted user resource and is ignored here, even when cwd is $HOME.
*/
export function hasTrustRequiringProjectResources(cwd: string): boolean {
const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
const userAgentsSkillsDir = join(homeDir, ".agents", "skills");
let currentDir = canonicalizePath(resolvePath(cwd));
if (hasProjectConfigDir(currentDir)) {
const configDir = join(currentDir, CONFIG_DIR_NAME);
if (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) {
return true;
}
while (true) {
if (existsSync(join(currentDir, ".agents", "skills"))) {
const agentsSkillsDir = join(currentDir, ".agents", "skills");
if (agentsSkillsDir !== userAgentsSkillsDir && existsSync(agentsSkillsDir)) {
return true;
}

View File

@@ -289,7 +289,7 @@ export {
withFileMutationQueue,
} from "./core/tools/index.ts";
export {
hasProjectTrustInputs,
hasTrustRequiringProjectResources,
type ProjectTrustDecision,
ProjectTrustStore,
type ProjectTrustStoreEntry,

View File

@@ -41,7 +41,7 @@ import {
import { assertValidSessionId, SessionManager } from "./core/session-manager.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { printTimings, resetTimings, time } from "./core/timings.ts";
import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts";
import { runMigrations, showDeprecationWarnings } from "./migrations.ts";
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts";
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts";
@@ -572,7 +572,9 @@ export async function main(args: string[], options?: MainOptions) {
const trustStore = new ProjectTrustStore(agentDir);
const sessionCwd = sessionManager.getCwd();
const autoTrustOnReloadCwd =
parsed.projectTrustOverride === undefined && !hasProjectTrustInputs(sessionCwd) ? sessionCwd : undefined;
parsed.projectTrustOverride === undefined && !hasTrustRequiringProjectResources(sessionCwd)
? sessionCwd
: undefined;
const trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode;
const projectTrustByCwd = new Map<string, boolean>();
@@ -591,12 +593,14 @@ export async function main(args: string[], options?: MainOptions) {
const isInitialRuntime = sessionStartEvent === undefined;
const projectTrustDiagnostics: AgentSessionRuntimeDiagnostic[] = [];
const cachedProjectTrust = projectTrustByCwd.get(cwd);
const hasTrustInputs = hasProjectTrustInputs(cwd);
const hasTrustRequiringResources = hasTrustRequiringProjectResources(cwd);
const shouldResolveProjectTrust =
parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustInputs;
parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources;
const projectTrusted = shouldResolveProjectTrust
? false
: (cachedProjectTrust ?? parsed.projectTrustOverride ?? (!hasTrustInputs || trustStore.get(cwd) === true));
: (cachedProjectTrust ??
parsed.projectTrustOverride ??
(!hasTrustRequiringResources || trustStore.get(cwd) === true));
const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
const services = await createAgentSessionServices({
cwd,

View File

@@ -1,7 +1,6 @@
import { Container, getKeybindings, Spacer, Text } from "@earendil-works/pi-tui";
import {
getProjectTrustOptions,
getProjectTrustPath,
type ProjectTrustOption,
type ProjectTrustStoreEntry,
} from "../../../core/trust-manager.ts";
@@ -19,12 +18,12 @@ export interface TrustSelectorOptions {
onCancel: () => void;
}
function formatDecision(cwd: string, decision: ProjectTrustStoreEntry | null): string {
function formatDecision(trustPath: string | undefined, decision: ProjectTrustStoreEntry | null): string {
if (decision === null) {
return "none";
}
const label = decision.decision ? "trusted" : "untrusted";
if (decision.path !== getProjectTrustPath(cwd)) {
if (trustPath !== undefined && decision.path !== trustPath) {
return `${label} (inherited from ${decision.path})`;
}
return `${label} (${decision.path})`;
@@ -56,7 +55,14 @@ export class TrustSelectorComponent extends Container {
this.addChild(new Text(theme.fg("muted", options.cwd), 1, 0));
this.addChild(new Spacer(1));
this.addChild(
new Text(theme.fg("muted", `Saved decision: ${formatDecision(options.cwd, options.savedDecision)}`), 1, 0),
new Text(
theme.fg(
"muted",
`Saved decision: ${formatDecision(this.trustOptions[0]?.savedPath, options.savedDecision)}`,
),
1,
0,
),
);
this.addChild(
new Text(theme.fg("muted", `Current session: ${options.projectTrusted ? "trusted" : "untrusted"}`), 1, 0),

View File

@@ -86,7 +86,7 @@ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts";
import type { SourceInfo } from "../../core/source-info.ts";
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
import type { TruncationResult } from "../../core/tools/truncate.ts";
import { hasProjectConfigDir, hasProjectTrustInputs, ProjectTrustStore } from "../../core/trust-manager.ts";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts";
import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.ts";
import { copyToClipboard } from "../../utils/clipboard.ts";
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts";
@@ -3271,7 +3271,7 @@ export class InteractiveMode {
}
private renderProjectTrustWarningIfNeeded(): void {
if (this.settingsManager.isProjectTrusted() || !hasProjectTrustInputs(this.sessionManager.getCwd())) {
if (this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(this.sessionManager.getCwd())) {
return;
}
@@ -4198,7 +4198,7 @@ export class InteractiveMode {
if (this.autoTrustOnReloadCwd !== cwd) {
return false;
}
if (!this.settingsManager.isProjectTrusted() || !hasProjectConfigDir(cwd)) {
if (!this.settingsManager.isProjectTrusted() || !hasTrustRequiringProjectResources(cwd)) {
return false;
}

View File

@@ -18,7 +18,7 @@ import { DefaultPackageManager } from "./core/package-manager.ts";
import { type AppMode, resolveProjectTrusted } from "./core/project-trust.ts";
import { DefaultResourceLoader } from "./core/resource-loader.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts";
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "./core/trust-manager.ts";
import { spawnProcess } from "./utils/child-process.ts";
import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts";
import {
@@ -452,13 +452,21 @@ async function createCommandSettingsManager(options: {
cwd: string;
agentDir: string;
projectTrustOverride?: boolean;
useSavedProjectTrustOnly?: boolean;
extensionFactories?: ExtensionFactory[];
}): Promise<CommandSettingsResult> {
const settingsManager = SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false });
const projectTrustWarnings: string[] = [];
const trustStore = new ProjectTrustStore(options.agentDir);
if (options.useSavedProjectTrustOnly) {
const savedProjectTrusted = trustStore.get(options.cwd) === true;
settingsManager.setProjectTrusted(options.projectTrustOverride ?? savedProjectTrusted);
return { settingsManager, projectTrustWarnings };
}
const appMode = getCommandAppMode();
const extensionsResult =
options.projectTrustOverride === undefined && hasProjectTrustInputs(options.cwd)
options.projectTrustOverride === undefined && hasTrustRequiringProjectResources(options.cwd)
? await new DefaultResourceLoader({
cwd: options.cwd,
agentDir: options.agentDir,
@@ -472,7 +480,7 @@ async function createCommandSettingsManager(options: {
const projectTrusted = await resolveProjectTrusted({
cwd: options.cwd,
trustStore: new ProjectTrustStore(options.agentDir),
trustStore,
trustOverride: options.projectTrustOverride,
defaultProjectTrust: settingsManager.getDefaultProjectTrust(),
extensionsResult,
@@ -576,6 +584,7 @@ export async function handlePackageCommand(
cwd,
agentDir,
projectTrustOverride: options.projectTrustOverride,
useSavedProjectTrustOnly: options.command === "update",
extensionFactories: runtimeOptions.extensionFactories,
});
reportProjectTrustWarnings(projectTrustWarnings);