From 31ade0e0dc2a27cffc2d71207360fe7ea83c707c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Hou=C5=A1ka?= Date: Thu, 29 Oct 2026 21:05:06 +0100 Subject: [PATCH] fix(coding-agent): honor saved project trust for projects without trust-requiring resources A saved Do not trust decision was ignored when the project had no trust-requiring resources, so ctx.isProjectTrusted() returned true. The saved decision now takes precedence over auto-trust. closes #10947 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/project-trust.ts | 2 +- packages/coding-agent/src/main.ts | 4 +- .../test/project-trust-saved-decision.test.ts | 150 ++++++++++++++++++ 4 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/coding-agent/test/project-trust-saved-decision.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 413020cbe..4eab460b3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixed +- Fixed a saved "Do not trust" decision being ignored for projects without trust-requiring resources, for example a folder with only `AGENTS.md`. pi auto-trusted such projects, so `ctx.isProjectTrusted()` returned true and package commands treated them as trusted. A saved decision in `trust.json` now takes precedence; auto-trust applies only when no decision is saved ([#10947](https://github.com/earendil-works/pi/issues/10947)) - Fixed `Shift+Enter` queueing a follow-up instead of inserting a newline in Apple Terminal setups that send `ESC` + Return. pi now checks the local Shift key state for that sequence, as it already did for plain Return; `Alt+Enter` without Shift still queues a follow-up ([#11559](https://github.com/earendil-works/pi/issues/11559)) - Fixed the working status (OSC 7501) disappearing in Rex and iTerm2 during runs in regular TUI mode. Rendered messages wrote OSC 133 prompt markers to the terminal, which the terminal treats as a new shell prompt; regular mode no longer writes them ([#11519](https://github.com/earendil-works/pi/issues/11519)) - Fixed compaction, branch summaries, and bug-report summaries in SDK sessions passing the stored OAuth token back to the model runtime as an explicit API key. Provider auth that treats explicit keys differently from stored OAuth now sees the OAuth login, as normal prompts do ([#11520](https://github.com/earendil-works/pi/issues/11520)) diff --git a/packages/coding-agent/src/core/project-trust.ts b/packages/coding-agent/src/core/project-trust.ts index efb14be4f..3920922b8 100644 --- a/packages/coding-agent/src/core/project-trust.ts +++ b/packages/coding-agent/src/core/project-trust.ts @@ -49,7 +49,7 @@ export async function resolveProjectTrusted(options: ResolveProjectTrustedOption return options.trustOverride; } if (!hasTrustRequiringProjectResources(options.cwd)) { - return true; + return options.trustStore.get(options.cwd) ?? true; } if (options.extensionsResult) { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index faba51f48..88aa3c5d3 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -756,9 +756,7 @@ export async function main(args: string[], options?: MainOptions) { parsed.projectTrustOverride === undefined && cachedProjectTrust === undefined && hasTrustRequiringResources; const projectTrusted = shouldResolveProjectTrust ? false - : (cachedProjectTrust ?? - parsed.projectTrustOverride ?? - (!hasTrustRequiringResources || trustStore.get(cwd) === true)); + : (cachedProjectTrust ?? parsed.projectTrustOverride ?? trustStore.get(cwd) ?? !hasTrustRequiringResources); const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted }); const services = await createAgentSessionServices({ cwd, diff --git a/packages/coding-agent/test/project-trust-saved-decision.test.ts b/packages/coding-agent/test/project-trust-saved-decision.test.ts new file mode 100644 index 000000000..f356e44a3 --- /dev/null +++ b/packages/coding-agent/test/project-trust-saved-decision.test.ts @@ -0,0 +1,150 @@ +import { spawn } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { createProjectTrustContext } from "../src/cli/project-trust.ts"; +import { ENV_AGENT_DIR } from "../src/config.ts"; +import { resolveProjectTrusted } from "../src/core/project-trust.ts"; +import { SettingsManager } from "../src/core/settings-manager.ts"; +import { ProjectTrustStore } from "../src/core/trust-manager.ts"; + +const cliPath = resolve(__dirname, "../src/cli.ts"); +// --import takes a module specifier, not a filesystem path. +const sourceResolverUrl = pathToFileURL(resolve(__dirname, "../src/experimental/source-resolver.ts")).href; +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +interface Dirs { + agentDir: string; + projectDir: string; + probeFile: string; + extensionFile: string; +} + +function setup(): Dirs { + const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), "pi-project-trust-saved-"))); + tempDirs.push(tempRoot); + const dirs = { + agentDir: join(tempRoot, "agent"), + projectDir: join(tempRoot, "project"), + probeFile: join(tempRoot, "probe.json"), + extensionFile: join(tempRoot, "probe-extension.ts"), + }; + mkdirSync(dirs.agentDir, { recursive: true }); + mkdirSync(dirs.projectDir, { recursive: true }); + // AGENTS.md alone is not a trust-requiring project resource. + writeFileSync(join(dirs.projectDir, "AGENTS.md"), "# untrusted\n"); + writeFileSync( + dirs.extensionFile, + `import { writeFileSync } from "node:fs"; +export default function (pi) { + pi.registerProvider("trust-probe", { + baseUrl: "https://trust-probe.invalid/v1", + apiKey: "trust-probe-key", + api: "openai-completions", + models: [{ + id: "trust-probe-model", + name: "trust-probe-model", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 10000, + maxTokens: 1000, + }], + }); + pi.on("session_start", (_event, ctx) => { + writeFileSync(${JSON.stringify(dirs.probeFile)}, JSON.stringify({ trusted: ctx.isProjectTrusted() })); + process.exit(0); + }); +} +`, + ); + return dirs; +} + +async function probeProjectTrust(dirs: Dirs): Promise { + const child = spawn( + process.execPath, + [ + "--import", + sourceResolverUrl, + cliPath, + "--no-session", + "-e", + dirs.extensionFile, + "--provider", + "trust-probe", + "--model", + "trust-probe-model", + "-p", + "hi", + ], + { + cwd: dirs.projectDir, + env: { ...process.env, [ENV_AGENT_DIR]: dirs.agentDir, PI_OFFLINE: "1" }, + stdio: ["ignore", "ignore", "pipe"], + }, + ); + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += chunk.toString(); + }); + const code = await new Promise((resolvePromise, reject) => { + const timeout = setTimeout(() => child.kill("SIGKILL"), 20_000); + child.on("error", (error) => { + clearTimeout(timeout); + reject(error); + }); + child.on("close", (exitCode) => { + clearTimeout(timeout); + resolvePromise(exitCode); + }); + }); + expect(code, stderr).toBe(0); + return (JSON.parse(readFileSync(dirs.probeFile, "utf8")) as { trusted: boolean }).trusted; +} + +// Regression tests for https://github.com/earendil-works/pi/issues/10947 +describe("saved project trust for projects without trust-requiring resources", () => { + it("reports a saved Do not trust decision to extensions", async () => { + const dirs = setup(); + new ProjectTrustStore(dirs.agentDir).set(dirs.projectDir, false); + + expect(await probeProjectTrust(dirs)).toBe(false); + }, 30_000); + + it("auto-trusts when no decision is saved", async () => { + const dirs = setup(); + + expect(await probeProjectTrust(dirs)).toBe(true); + }, 30_000); + + it("resolveProjectTrusted honors the saved decision before auto-trust", async () => { + const dirs = setup(); + const trustStore = new ProjectTrustStore(dirs.agentDir); + const resolve = () => + resolveProjectTrusted({ + cwd: dirs.projectDir, + trustStore, + projectTrustContext: createProjectTrustContext({ + cwd: dirs.projectDir, + mode: "print", + settingsManager: SettingsManager.inMemory(), + hasUI: false, + }), + }); + + expect(await resolve()).toBe(true); + trustStore.set(dirs.projectDir, false); + expect(await resolve()).toBe(false); + trustStore.set(dirs.projectDir, true); + expect(await resolve()).toBe(true); + }); +});