refactor(coding-agent): simplify project trust approvals

This commit is contained in:
Armin Ronacher
2026-06-03 00:48:01 +02:00
Unverified
parent 4e53a4141a
commit e4132d75d8
10 changed files with 146 additions and 211 deletions
+1 -1
View File
@@ -290,7 +290,7 @@ See [docs/settings.md](docs/settings.md) for all options.
### Project Trust
Interactive startup asks before loading `.pi` in a working directory whose trust has not been set. Decisions are stored in `~/.pi/agent/trust.json` by CWD: `true` loads project config, `false` skips it, and missing/null asks again. Use `/trust yes`, `/trust no`, `/trust reset`, or `/trust` to update the current CWD. Use `--force`/`-f` to load project config for one run regardless of trust.
Interactive startup asks before loading `.pi` in a working directory whose trust has not been set. Decisions are stored in `~/.pi/agent/trust.json` by CWD: `true` loads project config, `false` skips it, and a missing entry asks again. Use `/trust yes`, `/trust no`, `/trust reset`, or `/trust` to update the current CWD. Use `--force`/`-f` to load project config for one run regardless of trust.
### Telemetry and update checks
+1 -1
View File
@@ -11,7 +11,7 @@ Edit directly or use `/settings` for common options.
## Project Trust
Interactive startup asks before loading `.pi` in a working directory whose trust has not been set. Decisions are stored in `~/.pi/agent/trust.json` by CWD: `true` loads project config, `false` skips it, and missing/null asks again. Use `/trust yes`, `/trust no`, `/trust reset`, or `/trust` to update the current CWD. Use `--force`/`-f` to load project config for one run regardless of trust.
Interactive startup asks before loading `.pi` in a working directory whose trust has not been set. Decisions are stored in `~/.pi/agent/trust.json` by CWD: `true` loads project config, `false` skips it, and a missing entry asks again. Use `/trust yes`, `/trust no`, `/trust reset`, or `/trust` to update the current CWD. Use `--force`/`-f` to load project config for one run regardless of trust.
## All Settings
+1 -1
View File
@@ -113,7 +113,7 @@ Append to the default prompt without replacing it with `APPEND_SYSTEM.md` in tho
### Project Trust
Interactive startup asks before loading `.pi` in a working directory whose trust has not been set. Decisions are stored in `~/.pi/agent/trust.json` by CWD: `true` loads project config, `false` skips it, and missing/null asks again. Use `/trust yes`, `/trust no`, `/trust reset`, or `/trust` to update the current CWD. Use `--force`/`-f` to load project config for one run regardless of trust.
Interactive startup asks before loading `.pi` in a working directory whose trust has not been set. Decisions are stored in `~/.pi/agent/trust.json` by CWD: `true` loads project config, `false` skips it, and a missing entry asks again. Use `/trust yes`, `/trust no`, `/trust reset`, or `/trust` to update the current CWD. Use `--force`/`-f` to load project config for one run regardless of trust.
## Exporting and Sharing Sessions
@@ -2250,9 +2250,11 @@ export class DefaultPackageManager implements PackageManager {
themes: join(projectBaseDir, "themes"),
};
const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd).filter(
(dir) => resolve(dir) !== resolve(userAgentsSkillsDir),
);
const projectConfigTrusted = this.settingsManager.isProjectConfigTrusted();
const includeProjectScopedResources = projectConfigTrusted || !existsSync(projectBaseDir);
const projectAgentsSkillDirs = includeProjectScopedResources
? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir))
: [];
const addResources = (
resourceType: ResourceType,
@@ -2268,23 +2270,40 @@ export class DefaultPackageManager implements PackageManager {
}
};
// Project extensions from .pi/
addResources(
"extensions",
collectAutoExtensionEntries(projectDirs.extensions),
projectMetadata,
projectOverrides.extensions,
projectBaseDir,
);
if (projectConfigTrusted) {
// Project extensions from .pi/
addResources(
"extensions",
collectAutoExtensionEntries(projectDirs.extensions),
projectMetadata,
projectOverrides.extensions,
projectBaseDir,
);
// Project skills from .pi/
addResources(
"skills",
collectAutoSkillEntries(projectDirs.skills, "pi"),
projectMetadata,
projectOverrides.skills,
projectBaseDir,
);
// Project skills from .pi/
addResources(
"skills",
collectAutoSkillEntries(projectDirs.skills, "pi"),
projectMetadata,
projectOverrides.skills,
projectBaseDir,
);
addResources(
"prompts",
collectAutoPromptEntries(projectDirs.prompts),
projectMetadata,
projectOverrides.prompts,
projectBaseDir,
);
addResources(
"themes",
collectAutoThemeEntries(projectDirs.themes),
projectMetadata,
projectOverrides.themes,
projectBaseDir,
);
}
// Project skills from .agents/ (each with its own baseDir)
for (const agentsSkillsDir of projectAgentsSkillDirs) {
@@ -2302,21 +2321,6 @@ export class DefaultPackageManager implements PackageManager {
);
}
addResources(
"prompts",
collectAutoPromptEntries(projectDirs.prompts),
projectMetadata,
projectOverrides.prompts,
projectBaseDir,
);
addResources(
"themes",
collectAutoThemeEntries(projectDirs.themes),
projectMetadata,
projectOverrides.themes,
projectBaseDir,
);
// User extensions from ~/.pi/agent/
addResources(
"extensions",
@@ -11,7 +11,7 @@ import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.ts";
import { createEventBus, type EventBus } from "./event-bus.ts";
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions } from "./extensions/loader.ts";
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./extensions/types.ts";
import { DefaultPackageManager, type MissingSourceAction, type PathMetadata } from "./package-manager.ts";
import { DefaultPackageManager, type PathMetadata } from "./package-manager.ts";
import type { PromptTemplate } from "./prompt-templates.ts";
import { loadPromptTemplates } from "./prompt-templates.ts";
import { SettingsManager } from "./settings-manager.ts";
@@ -145,7 +145,6 @@ export interface DefaultResourceLoaderOptions {
agentsFilesOverride?: (base: { agentsFiles: Array<{ path: string; content: string }> }) => {
agentsFiles: Array<{ path: string; content: string }>;
};
onMissingPackage?: (source: string) => Promise<MissingSourceAction>;
systemPromptOverride?: (base: string | undefined) => string | undefined;
appendSystemPromptOverride?: (base: string[]) => string[];
}
@@ -184,7 +183,6 @@ export class DefaultResourceLoader implements ResourceLoader {
private agentsFilesOverride?: (base: { agentsFiles: Array<{ path: string; content: string }> }) => {
agentsFiles: Array<{ path: string; content: string }>;
};
private onMissingPackage?: (source: string) => Promise<MissingSourceAction>;
private systemPromptOverride?: (base: string | undefined) => string | undefined;
private appendSystemPromptOverride?: (base: string[]) => string[];
@@ -232,7 +230,6 @@ export class DefaultResourceLoader implements ResourceLoader {
this.promptsOverride = options.promptsOverride;
this.themesOverride = options.themesOverride;
this.agentsFilesOverride = options.agentsFilesOverride;
this.onMissingPackage = options.onMissingPackage;
this.systemPromptOverride = options.systemPromptOverride;
this.appendSystemPromptOverride = options.appendSystemPromptOverride;
@@ -323,7 +320,7 @@ export class DefaultResourceLoader implements ResourceLoader {
async reload(): Promise<void> {
await this.settingsManager.reload();
const resolvedPaths = await this.packageManager.resolve(this.onMissingPackage);
const resolvedPaths = await this.packageManager.resolve();
const cliExtensionPaths = await this.packageManager.resolveExtensionSources(this.additionalExtensionPaths, {
temporary: true,
});
+34 -109
View File
@@ -45,12 +45,8 @@ import { runMigrations, showDeprecationWarnings } from "./migrations.ts";
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.ts";
import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.ts";
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.ts";
import {
handleConfigCommand,
handlePackageCommand,
packageCommandForcesProjectConfigTrust,
} from "./package-manager-cli.ts";
import { canonicalizePath, isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts";
import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.ts";
import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.ts";
import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.ts";
/**
@@ -441,18 +437,6 @@ function resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | u
return paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));
}
function getSessionTrustOverrideKey(cwd: string): string {
return canonicalizePath(resolvePath(cwd));
}
function isPackageCommandArg(arg: string | undefined): boolean {
return arg === "install" || arg === "remove" || arg === "uninstall" || arg === "update" || arg === "list";
}
function hasForceFlag(args: string[]): boolean {
return args.includes("--force") || args.includes("-f");
}
async function showStartupSelector<T>(
settingsManager: SettingsManager,
title: string,
@@ -500,59 +484,45 @@ async function promptForMissingSessionCwd(
]);
}
interface ProjectTrustPromptResult {
trusted: boolean;
remember: boolean;
}
async function promptForProjectTrust(cwd: string, settingsManager: SettingsManager): Promise<ProjectTrustPromptResult> {
const selected = await showStartupSelector(
async function promptForProjectTrust(cwd: string, settingsManager: SettingsManager): Promise<boolean | undefined> {
return showStartupSelector(
settingsManager,
`Trust project configuration?\nLoad .pi from ${cwd}?\nWarning: Project extensions can execute code.`,
[
{ label: "Yes (remember)", value: { trusted: true, remember: true } },
{ label: "Yes (this session)", value: { trusted: true, remember: false } },
{ label: "No (remember)", value: { trusted: false, remember: true } },
{ label: "No (this session)", value: { trusted: false, remember: false } },
{ label: "Trust", value: true },
{ label: "Don't trust", value: false },
],
);
return selected ?? { trusted: false, remember: false };
}
interface ProjectTrustResolution {
trusted: boolean;
sessionOverride?: boolean;
}
async function resolveProjectConfigTrusted(options: {
cwd: string;
agentDir: string;
sessionTrustOverride: boolean | undefined;
trustStore: ProjectTrustStore;
force: boolean;
appMode: AppMode;
settingsManagerForPrompt: SettingsManager;
}): Promise<ProjectTrustResolution> {
if (options.sessionTrustOverride !== undefined) {
return { trusted: options.sessionTrustOverride };
}): Promise<boolean> {
if (options.force) {
return true;
}
if (!hasProjectConfig(options.cwd)) {
return { trusted: false };
return false;
}
const trustStore = new ProjectTrustStore(options.agentDir);
const decision = trustStore.get(options.cwd);
const decision = options.trustStore.get(options.cwd);
if (decision !== null) {
return { trusted: decision };
return decision;
}
if (options.appMode !== "interactive") {
return { trusted: false };
return false;
}
const result = await promptForProjectTrust(options.cwd, options.settingsManagerForPrompt);
if (result.remember) {
trustStore.set(options.cwd, result.trusted);
return { trusted: result.trusted };
const selected = await promptForProjectTrust(options.cwd, options.settingsManagerForPrompt);
if (selected !== undefined) {
options.trustStore.set(options.cwd, selected);
return selected;
}
return { trusted: result.trusted, sessionOverride: result.trusted };
return false;
}
export interface MainOptions {
@@ -571,27 +541,12 @@ export async function main(args: string[], options?: MainOptions) {
cleanupWindowsSelfUpdateQuarantine(getPackageDir());
}
if (isPackageCommandArg(args[0]) || args[0] === "config") {
const cwd = process.cwd();
const agentDir = getAgentDir();
const projectConfigExists = hasProjectConfig(cwd);
const forceProjectConfigTrusted =
args[0] === "config" ? hasForceFlag(args) : packageCommandForcesProjectConfigTrust(args);
const promptSettingsManager = SettingsManager.create(cwd, agentDir, { projectConfigTrusted: false });
const projectTrustResolution = await resolveProjectConfigTrusted({
cwd,
agentDir,
sessionTrustOverride: forceProjectConfigTrusted ? true : undefined,
appMode: process.stdin.isTTY ? "interactive" : "print",
settingsManagerForPrompt: promptSettingsManager,
});
const projectConfigTrusted = forceProjectConfigTrusted || projectTrustResolution.trusted;
if (await handlePackageCommand(args, { projectConfigTrusted, projectConfigExists })) {
return;
}
if (await handleConfigCommand(args, { projectConfigTrusted, projectConfigExists })) {
return;
}
if (await handlePackageCommand(args)) {
return;
}
if (await handleConfigCommand(args)) {
return;
}
const parsed = parseArgs(args);
@@ -643,21 +598,14 @@ export async function main(args: string[], options?: MainOptions) {
const promptSettingsManager = SettingsManager.create(cwd, agentDir, { projectConfigTrusted: false });
const trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode;
const forceProjectConfigTrusted = parsed.force === true;
const sessionTrustOverrides = new Map<string, boolean>();
const getSessionTrustOverride = (targetCwd: string): boolean | undefined => {
return forceProjectConfigTrusted ? true : sessionTrustOverrides.get(getSessionTrustOverrideKey(targetCwd));
};
const startupTrustResolution = await resolveProjectConfigTrusted({
const trustStore = new ProjectTrustStore(agentDir);
const startupProjectConfigTrusted = await resolveProjectConfigTrusted({
cwd,
agentDir,
sessionTrustOverride: getSessionTrustOverride(cwd),
trustStore,
force: forceProjectConfigTrusted,
appMode: trustPromptMode,
settingsManagerForPrompt: promptSettingsManager,
});
if (startupTrustResolution.sessionOverride !== undefined) {
sessionTrustOverrides.set(getSessionTrustOverrideKey(cwd), startupTrustResolution.sessionOverride);
}
const startupProjectConfigTrusted = startupTrustResolution.trusted;
// Legacy extension migrations are intentional filesystem housekeeping and run
// regardless of trust. Trust gates loading/executing project config, not moving
// old Pi config directories to the current layout.
@@ -704,24 +652,15 @@ export async function main(args: string[], options?: MainOptions) {
time("createSessionManager");
const initialRuntimeCwd = sessionManager.getCwd();
let runtimeProjectConfigTrusted = startupProjectConfigTrusted;
if (initialRuntimeCwd !== cwd) {
const runtimeTrustResolution = await resolveProjectConfigTrusted({
await resolveProjectConfigTrusted({
cwd: initialRuntimeCwd,
agentDir,
sessionTrustOverride: getSessionTrustOverride(initialRuntimeCwd),
trustStore,
force: forceProjectConfigTrusted,
appMode: trustPromptMode,
settingsManagerForPrompt: promptSettingsManager,
});
if (runtimeTrustResolution.sessionOverride !== undefined) {
sessionTrustOverrides.set(
getSessionTrustOverrideKey(initialRuntimeCwd),
runtimeTrustResolution.sessionOverride,
);
}
runtimeProjectConfigTrusted = runtimeTrustResolution.trusted;
}
const trustStore = new ProjectTrustStore(agentDir);
const resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);
const resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);
const resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);
@@ -733,9 +672,7 @@ export async function main(args: string[], options?: MainOptions) {
sessionManager,
sessionStartEvent,
}) => {
const projectConfigTrusted =
getSessionTrustOverride(cwd) ??
(cwd === initialRuntimeCwd ? runtimeProjectConfigTrusted : trustStore.get(cwd) === true);
const projectConfigTrusted = forceProjectConfigTrusted || (hasProjectConfig(cwd) && trustStore.get(cwd) === true);
const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectConfigTrusted });
const services = await createAgentSessionServices({
cwd,
@@ -897,18 +834,6 @@ export async function main(args: string[], options?: MainOptions) {
initialMessages: parsed.messages,
verbose: parsed.verbose,
forceProjectConfigTrust: forceProjectConfigTrusted,
setProjectConfigTrustOverride: (overrideCwd, trusted) => {
const key = getSessionTrustOverrideKey(overrideCwd);
if (trusted === undefined) {
sessionTrustOverrides.delete(key);
} else {
sessionTrustOverrides.set(key, trusted);
}
if (key === getSessionTrustOverrideKey(initialRuntimeCwd)) {
runtimeProjectConfigTrusted =
getSessionTrustOverride(initialRuntimeCwd) ?? trustStore.get(initialRuntimeCwd) === true;
}
},
});
if (startupBenchmark) {
await interactiveMode.init();
@@ -258,8 +258,6 @@ export interface InteractiveModeOptions {
verbose?: boolean;
/** Force project config trust for this process */
forceProjectConfigTrust?: boolean;
/** Update project config trust override for this session and cwd */
setProjectConfigTrustOverride?: (cwd: string, trusted: boolean | undefined) => void;
}
export class InteractiveMode {
@@ -4949,25 +4947,22 @@ export class InteractiveMode {
return "ask";
}
private async selectTrustDecision(): Promise<{ decision: ProjectTrustDecision; remember: boolean } | undefined> {
private async selectTrustDecision(): Promise<ProjectTrustDecision | undefined> {
const trustStore = new ProjectTrustStore(getAgentDir());
const cwd = this.sessionManager.getCwd();
const current = this.formatTrustDecision(trustStore.get(cwd));
const choice = await this.showExtensionSelector(
`Trust project configuration?\nCurrent setting: ${current}\nLoad .pi from ${cwd}?\nWarning: Project extensions can execute code.`,
["Yes (remember)", "Yes (this session)", "No (remember)", "No (this session)"],
["Trust", "Don't trust", "Reset"],
);
if (choice === "Yes (remember)") {
return { decision: true, remember: true };
if (choice === "Trust") {
return true;
}
if (choice === "Yes (this session)") {
return { decision: true, remember: false };
if (choice === "Don't trust") {
return false;
}
if (choice === "No (remember)") {
return { decision: false, remember: true };
}
if (choice === "No (this session)") {
return { decision: false, remember: false };
if (choice === "Reset") {
return null;
}
return undefined;
}
@@ -4983,40 +4978,31 @@ export class InteractiveMode {
}
const rawArg = text === "/trust" ? "" : text.slice("/trust".length).trim().toLowerCase();
let selection: { decision: ProjectTrustDecision; remember: boolean } | undefined;
let decision: ProjectTrustDecision | undefined;
if (!rawArg) {
selection = await this.selectTrustDecision();
decision = await this.selectTrustDecision();
} else if (rawArg === "yes") {
selection = { decision: true, remember: true };
decision = true;
} else if (rawArg === "no") {
selection = { decision: false, remember: true };
decision = false;
} else if (rawArg === "reset") {
selection = { decision: null, remember: true };
decision = null;
} else {
this.showError("Usage: /trust [yes|no|reset]");
return;
}
if (selection === undefined) {
if (decision === undefined) {
return;
}
const trustStore = new ProjectTrustStore(getAgentDir());
const cwd = this.sessionManager.getCwd();
if (selection.remember) {
trustStore.set(cwd, selection.decision);
this.options.setProjectConfigTrustOverride?.(cwd, undefined);
} else {
this.options.setProjectConfigTrustOverride?.(
cwd,
selection.decision === null ? undefined : selection.decision,
);
}
const projectConfigTrusted = this.options.forceProjectConfigTrust === true || selection.decision === true;
trustStore.set(cwd, decision);
const projectConfigTrusted = this.options.forceProjectConfigTrust === true || decision === true;
this.settingsManager.setProjectConfigTrusted(projectConfigTrusted);
await this.handleReloadCommand();
const suffix = selection.remember ? "" : " (this session)";
this.showStatus(`Project trust: ${this.formatTrustDecision(selection.decision)}${suffix}`);
this.showStatus(`Project trust: ${this.formatTrustDecision(decision)}`);
}
private async handleReloadCommand(): Promise<void> {
@@ -14,7 +14,7 @@ import {
} from "./config.ts";
import { DefaultPackageManager } from "./core/package-manager.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { hasProjectConfig } from "./core/trust-manager.ts";
import { hasProjectConfig, ProjectTrustStore } from "./core/trust-manager.ts";
import { spawnProcess } from "./utils/child-process.ts";
import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts";
import {
@@ -56,11 +56,6 @@ interface PackageCommandOptions {
conflictingOptions?: string;
}
interface ProjectConfigCommandContext {
projectConfigTrusted?: boolean;
projectConfigExists?: boolean;
}
function reportSettingsErrors(settingsManager: SettingsManager, context: string): void {
const errors = settingsManager.drainErrors();
for (const { scope, error } of errors) {
@@ -295,14 +290,6 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
};
}
export function packageCommandForcesProjectConfigTrust(args: string[]): boolean {
const options = parsePackageCommand(args);
return (
options?.force === true &&
(options.command === "install" || options.command === "remove" || options.command === "list")
);
}
function updateTargetIncludesSelf(target: UpdateTarget): boolean {
return target.type === "all" || target.type === "self";
}
@@ -404,16 +391,20 @@ function prepareWindowsNpmSelfUpdate(): void {
quarantineWindowsNativeDependencies(packageDir);
}
export async function handleConfigCommand(args: string[], context: ProjectConfigCommandContext = {}): Promise<boolean> {
export async function handleConfigCommand(args: string[]): Promise<boolean> {
if (args[0] !== "config") {
return false;
}
const cwd = process.cwd();
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir, {
projectConfigTrusted: context.projectConfigTrusted ?? true,
});
const projectConfigExists = hasProjectConfig(cwd);
const projectConfigTrusted =
!projectConfigExists ||
args.includes("--force") ||
args.includes("-f") ||
new ProjectTrustStore(agentDir).get(cwd) === true;
const settingsManager = SettingsManager.create(cwd, agentDir, { projectConfigTrusted });
reportSettingsErrors(settingsManager, "config command");
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
const resolvedPaths = await packageManager.resolve();
@@ -428,10 +419,7 @@ export async function handleConfigCommand(args: string[], context: ProjectConfig
process.exit(0);
}
export async function handlePackageCommand(
args: string[],
context: ProjectConfigCommandContext = {},
): Promise<boolean> {
export async function handlePackageCommand(args: string[]): Promise<boolean> {
const options = parsePackageCommand(args);
if (!options) {
return false;
@@ -479,20 +467,22 @@ export async function handlePackageCommand(
}
const cwd = process.cwd();
const projectConfigTrusted = context.projectConfigTrusted ?? true;
const projectConfigExists = context.projectConfigExists ?? hasProjectConfig(cwd);
const agentDir = getAgentDir();
const projectConfigExists = hasProjectConfig(cwd);
const writesProjectPackageConfig = (options.command === "install" || options.command === "remove") && options.local;
const commandForcesProjectConfigTrust =
options.force && (options.command === "install" || options.command === "remove" || options.command === "list");
const projectConfigTrusted =
commandForcesProjectConfigTrust ||
(projectConfigExists && new ProjectTrustStore(agentDir).get(cwd) === true) ||
(writesProjectPackageConfig && !projectConfigExists);
if (!projectConfigTrusted && projectConfigExists && writesProjectPackageConfig) {
console.error(chalk.red("Project config is not trusted. Use --force to modify local package config."));
process.exitCode = 1;
return true;
}
const agentDir = getAgentDir();
const effectiveProjectConfigTrusted = projectConfigTrusted || (writesProjectPackageConfig && !projectConfigExists);
const settingsManager = SettingsManager.create(cwd, agentDir, {
projectConfigTrusted: effectiveProjectConfigTrusted,
});
const settingsManager = SettingsManager.create(cwd, agentDir, { projectConfigTrusted });
reportSettingsErrors(settingsManager, "package command");
const selfUpdateNpmCommand = settingsManager.getGlobalSettings().npmCommand;
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
import { main } from "../src/main.ts";
describe("package commands", () => {
@@ -119,6 +120,25 @@ describe("package commands", () => {
}
});
it("uses remembered project trust for list", async () => {
mkdirSync(join(projectDir, ".pi"), { recursive: true });
writeFileSync(join(projectDir, ".pi", "settings.json"), JSON.stringify({ packages: ["npm:@project/pkg"] }));
new ProjectTrustStore(agentDir).set(projectDir, true);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
try {
await expect(main(["list"])).resolves.toBeUndefined();
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
expect(stdout).toContain("Project packages:");
expect(stdout).toContain("npm:@project/pkg");
expect(stdout).not.toContain("No packages installed.");
expect(process.exitCode).toBeUndefined();
} finally {
logSpy.mockRestore();
}
});
it("blocks local package changes when project config is untrusted", async () => {
mkdirSync(join(projectDir, ".pi"), { recursive: true });
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
@@ -342,6 +342,19 @@ Content`,
expect(loader.getSystemPrompt()).toBe("Global system prompt.");
});
it("should skip .pi extensions when project config is not trusted", async () => {
const extensionsDir = join(cwd, ".pi", "extensions");
mkdirSync(extensionsDir, { recursive: true });
writeFileSync(join(extensionsDir, "project.ts"), `throw new Error("should not load");`);
const settingsManager = SettingsManager.create(cwd, agentDir, { projectConfigTrusted: false });
const loader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });
await loader.reload();
expect(loader.getExtensions().extensions).toHaveLength(0);
expect(loader.getExtensions().errors).toEqual([]);
});
it("should discover APPEND_SYSTEM.md", async () => {
const piDir = join(cwd, ".pi");
mkdirSync(piDir, { recursive: true });