feat(coding-agent): add project trust gating

This commit is contained in:
Mario Zechner
2026-06-05 10:51:20 +02:00
parent db594d3a59
commit 89a92207f1
28 changed files with 1029 additions and 112 deletions

View File

@@ -14,6 +14,7 @@ import {
} from "./config.ts";
import { DefaultPackageManager } from "./core/package-manager.ts";
import { SettingsManager } from "./core/settings-manager.ts";
import { hasProjectTrustInputs, ProjectTrustStore } from "./core/trust-manager.ts";
import { spawnProcess } from "./utils/child-process.ts";
import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.ts";
import {
@@ -48,6 +49,7 @@ interface PackageCommandOptions {
updateTarget?: UpdateTarget;
local: boolean;
force: boolean;
projectTrustOverride?: boolean;
help: boolean;
invalidOption?: string;
invalidArgument?: string;
@@ -68,13 +70,13 @@ function reportSettingsErrors(settingsManager: SettingsManager, context: string)
function getPackageCommandUsage(command: PackageCommand): string {
switch (command) {
case "install":
return `${APP_NAME} install <source> [-l]`;
return `${APP_NAME} install <source> [-l] [--approve|--no-approve]`;
case "remove":
return `${APP_NAME} remove <source> [-l]`;
return `${APP_NAME} remove <source> [-l] [--approve|--no-approve]`;
case "update":
return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension <source>] [--force]`;
return `${APP_NAME} update [source|self|pi] [--self] [--extensions] [--extension <source>] [--approve|--no-approve] [--force]`;
case "list":
return `${APP_NAME} list`;
return `${APP_NAME} list [--approve|--no-approve]`;
}
}
@@ -87,7 +89,9 @@ function printPackageCommandHelp(command: PackageCommand): void {
Install a package and add it to settings.
Options:
-l, --local Install project-locally (.pi/settings.json)
-l, --local Install project-locally (.pi/settings.json)
-a, --approve Trust project-local files for this command
-na, --no-approve Ignore project-local files for this command
Examples:
${APP_NAME} install npm:@foo/bar
@@ -107,7 +111,9 @@ Remove a package and its source from settings.
Alias: ${APP_NAME} uninstall <source> [-l]
Options:
-l, --local Remove from project settings (.pi/settings.json)
-l, --local Remove from project settings (.pi/settings.json)
-a, --approve Trust project-local files for this command
-na, --no-approve Ignore project-local files for this command
Examples:
${APP_NAME} remove npm:@foo/bar
@@ -125,6 +131,8 @@ Options:
--self Update pi only
--extensions Update installed packages only
--extension <source> Update one package only
-a, --approve Trust project-local files for this command
-na, --no-approve Ignore project-local files for this command
--force Reinstall pi even if the current version is latest
Short forms:
@@ -139,6 +147,10 @@ Short forms:
${getPackageCommandUsage("list")}
List installed packages from user and project settings.
Options:
-a, --approve Trust project-local files for this command
-na, --no-approve Ignore project-local files for this command
`);
return;
}
@@ -158,6 +170,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
let local = false;
let force = false;
let projectTrustOverride: boolean | undefined;
let help = false;
let invalidOption: string | undefined;
let invalidArgument: string | undefined;
@@ -202,6 +215,16 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
continue;
}
if (arg === "--approve" || arg === "-a") {
projectTrustOverride = true;
continue;
}
if (arg === "--no-approve" || arg === "-na") {
projectTrustOverride = false;
continue;
}
if (arg === "--force") {
if (command === "update") {
force = true;
@@ -280,6 +303,7 @@ function parsePackageCommand(args: string[]): PackageCommandOptions | undefined
updateTarget,
local,
force,
projectTrustOverride,
help,
invalidOption,
invalidArgument,
@@ -389,6 +413,25 @@ function prepareWindowsNpmSelfUpdate(): void {
quarantineWindowsNativeDependencies(packageDir);
}
function parseProjectTrustOverride(args: readonly string[]): boolean | undefined {
let trustOverride: boolean | undefined;
for (const arg of args) {
if (arg === "--approve" || arg === "-a") {
trustOverride = true;
} else if (arg === "--no-approve" || arg === "-na") {
trustOverride = false;
}
}
return trustOverride;
}
function resolveProjectTrusted(cwd: string, agentDir: string, trustOverride: boolean | undefined): boolean {
if (trustOverride !== undefined) {
return trustOverride;
}
return !hasProjectTrustInputs(cwd) || new ProjectTrustStore(agentDir).get(cwd) === true;
}
export async function handleConfigCommand(args: string[]): Promise<boolean> {
if (args[0] !== "config") {
return false;
@@ -396,7 +439,8 @@ export async function handleConfigCommand(args: string[]): Promise<boolean> {
const cwd = process.cwd();
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir);
const projectTrusted = parseProjectTrustOverride(args) ?? true;
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
reportSettingsErrors(settingsManager, "config command");
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
const resolvedPaths = await packageManager.resolve();
@@ -460,7 +504,14 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
const cwd = process.cwd();
const agentDir = getAgentDir();
const settingsManager = SettingsManager.create(cwd, agentDir);
const writesProjectPackageConfig = (options.command === "install" || options.command === "remove") && options.local;
const projectTrusted = resolveProjectTrusted(cwd, agentDir, options.projectTrustOverride);
if (!projectTrusted && writesProjectPackageConfig) {
console.error(chalk.red("Project is not trusted. Use --approve to modify local package config."));
process.exitCode = 1;
return true;
}
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
reportSettingsErrors(settingsManager, "package command");
const selfUpdateNpmCommand = settingsManager.getGlobalSettings().npmCommand;