mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
refactor(coding-agent): simplify project trust approvals
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user