fix(coding-agent): remove process-cwd tool singletons and use tool-name allowlists

- switch SDK/CLI tool selection to name-based allowlists
- apply allowlists across built-in, extension, and SDK tools
- remove ambient process.cwd defaults from core tooling and resource helpers
- update tests, examples, and TUI callers for explicit cwd plumbing
- add regression coverage for extension tool filtering

closes #3452
closes #2835
This commit is contained in:
Mario Zechner
2026-04-20 21:57:31 +02:00
parent 27c1544839
commit 5a4e22ea44
42 changed files with 254 additions and 201 deletions

View File

@@ -133,9 +133,6 @@ export class AgentSessionRuntime {
}
private apply(result: CreateAgentSessionRuntimeResult): void {
if (process.cwd() !== result.services.cwd) {
process.chdir(result.services.cwd);
}
this._session = result.session;
this._services = result.services;
this._diagnostics = result.diagnostics;
@@ -346,9 +343,6 @@ export async function createAgentSessionRuntime(
): Promise<AgentSessionRuntime> {
assertSessionCwdExists(options.sessionManager, options.cwd);
const result = await createRuntime(options);
if (process.cwd() !== result.services.cwd) {
process.chdir(result.services.cwd);
}
return new AgentSessionRuntime(
result.session,
result.services,

View File

@@ -12,7 +12,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import stripAnsi from "strip-ansi";
import { sanitizeBinaryOutput } from "../utils/shell.js";
import { type BashOperations, createLocalBashOperations } from "./tools/bash.js";
import type { BashOperations } from "./tools/bash.js";
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
// ============================================================================
@@ -43,23 +43,6 @@ export interface BashResult {
// Implementation
// ============================================================================
/**
* Execute a bash command with optional streaming and cancellation support.
*
* Uses the same local BashOperations backend as createBashTool() so interactive
* user bash and tool-invoked bash share the same process spawning behavior.
* Sanitization, newline normalization, temp-file capture, and truncation still
* happen in executeBashWithOperations(), so reusing the local backend does not
* change output processing behavior.
*
* @param command - The bash command to execute
* @param options - Optional streaming callback and abort signal
* @returns Promise resolving to execution result
*/
export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
return executeBashWithOperations(command, process.cwd(), createLocalBashOperations(), options);
}
/**
* Execute a bash command using custom BashOperations.
* Used for remote execution (SSH, containers, etc.).

View File

@@ -104,7 +104,7 @@ export type TerminalInputHandler = (data: string) => { consume?: boolean; data?:
/** Working indicator configuration for the interactive streaming loader. */
export interface WorkingIndicatorOptions {
/** Animation frames. Use an empty array to hide the indicator entirely. */
/** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */
frames?: string[];
/** Frame interval in milliseconds for animated indicators. */
intervalMs?: number;
@@ -142,6 +142,7 @@ export interface ExtensionUIContext {
* - Omit the argument to restore the default animated spinner.
* - Use `frames: ["●"]` for a static indicator.
* - Use `frames: []` to hide the indicator entirely.
* - Custom frames are rendered as provided, so extensions must add their own colors.
*/
setWorkingIndicator(options?: WorkingIndicatorOptions): void;

View File

@@ -101,7 +101,7 @@ export class FooterDataProvider {
private refreshPending = false;
private disposed = false;
constructor(cwd: string = process.cwd()) {
constructor(cwd: string) {
this.cwd = cwd;
this.gitPaths = findGitPaths(cwd);
this.setupGitWatcher();

View File

@@ -25,7 +25,7 @@ export {
createAgentSessionFromServices,
createAgentSessionServices,
} from "./agent-session-services.js";
export { type BashExecutorOptions, type BashResult, executeBash, executeBashWithOperations } from "./bash-executor.js";
export { type BashExecutorOptions, type BashResult, executeBashWithOperations } from "./bash-executor.js";
export type { CompactionResult } from "./compaction/index.js";
export { createEventBus, type EventBus, type EventBusController } from "./event-bus.js";
// Extensions system

View File

@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
import { homedir } from "os";
import { basename, dirname, isAbsolute, join, resolve, sep } from "path";
import { CONFIG_DIR_NAME, getPromptsDir } from "../config.js";
import { CONFIG_DIR_NAME } from "../config.js";
import { parseFrontmatter } from "../utils/frontmatter.js";
import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js";
@@ -175,14 +175,14 @@ function loadTemplatesFromDir(dir: string, getSourceInfo: (filePath: string) =>
}
export interface LoadPromptTemplatesOptions {
/** Working directory for project-local templates. Default: process.cwd() */
cwd?: string;
/** Agent config directory for global templates. Default: from getPromptsDir() */
agentDir?: string;
/** Explicit prompt template paths (files or directories) */
promptPaths?: string[];
/** Include default prompt directories. Default: true */
includeDefaults?: boolean;
/** Working directory for project-local templates. */
cwd: string;
/** Agent config directory for global templates. */
agentDir: string;
/** Explicit prompt template paths (files or directories). */
promptPaths: string[];
/** Include default prompt directories. */
includeDefaults: boolean;
}
function normalizePath(input: string): string {
@@ -204,11 +204,11 @@ function resolvePromptPath(p: string, cwd: string): string {
* 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/
* 3. Explicit prompt paths
*/
export function loadPromptTemplates(options: LoadPromptTemplatesOptions = {}): PromptTemplate[] {
const resolvedCwd = options.cwd ?? process.cwd();
const resolvedAgentDir = options.agentDir ?? getPromptsDir();
const promptPaths = options.promptPaths ?? [];
const includeDefaults = options.includeDefaults ?? true;
export function loadPromptTemplates(options: LoadPromptTemplatesOptions): PromptTemplate[] {
const resolvedCwd = options.cwd;
const resolvedAgentDir = options.agentDir;
const promptPaths = options.promptPaths;
const includeDefaults = options.includeDefaults;
const templates: PromptTemplate[] = [];

View File

@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join, resolve, sep } from "node:path";
import chalk from "chalk";
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
import { CONFIG_DIR_NAME } from "../config.js";
import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js";
import type { ResourceDiagnostic } from "./diagnostics.js";
@@ -73,11 +73,12 @@ function loadContextFileFromDir(dir: string): { path: string; content: string }
return null;
}
export function loadProjectContextFiles(
options: { cwd?: string; agentDir?: string } = {},
): Array<{ path: string; content: string }> {
const resolvedCwd = options.cwd ?? process.cwd();
const resolvedAgentDir = options.agentDir ?? getAgentDir();
export function loadProjectContextFiles(options: {
cwd: string;
agentDir: string;
}): Array<{ path: string; content: string }> {
const resolvedCwd = options.cwd;
const resolvedAgentDir = options.agentDir;
const contextFiles: Array<{ path: string; content: string }> = [];
const seenPaths = new Set<string>();
@@ -113,8 +114,8 @@ export function loadProjectContextFiles(
}
export interface DefaultResourceLoaderOptions {
cwd?: string;
agentDir?: string;
cwd: string;
agentDir: string;
settingsManager?: SettingsManager;
eventBus?: EventBus;
additionalExtensionPaths?: string[];
@@ -204,8 +205,8 @@ export class DefaultResourceLoader implements ResourceLoader {
private lastThemePaths: string[];
constructor(options: DefaultResourceLoaderOptions) {
this.cwd = options.cwd ?? process.cwd();
this.agentDir = options.agentDir ?? getAgentDir();
this.cwd = options.cwd;
this.agentDir = options.agentDir;
this.settingsManager = options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir);
this.eventBus = options.eventBus ?? createEventBus();
this.packageManager = new DefaultPackageManager({

View File

@@ -144,7 +144,7 @@ export class FileSettingsStorage implements SettingsStorage {
private globalSettingsPath: string;
private projectSettingsPath: string;
constructor(cwd: string = process.cwd(), agentDir: string = getAgentDir()) {
constructor(cwd: string, agentDir: string) {
this.globalSettingsPath = join(agentDir, "settings.json");
this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
}
@@ -256,7 +256,7 @@ export class SettingsManager {
}
/** Create a SettingsManager that loads from files */
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
static create(cwd: string, agentDir: string = getAgentDir()): SettingsManager {
const storage = new FileSettingsStorage(cwd, agentDir);
return SettingsManager.fromStorage(storage);
}

View File

@@ -374,14 +374,14 @@ function escapeXml(str: string): string {
}
export interface LoadSkillsOptions {
/** Working directory for project-local skills. Default: process.cwd() */
cwd?: string;
/** Agent config directory for global skills. Default: ~/.pi/agent */
agentDir?: string;
/** Working directory for project-local skills. */
cwd: string;
/** Agent config directory for global skills. */
agentDir: string;
/** Explicit skill paths (files or directories) */
skillPaths?: string[];
/** Include default skills directories. Default: true */
includeDefaults?: boolean;
skillPaths: string[];
/** Include default skills directories. */
includeDefaults: boolean;
}
function normalizePath(input: string): string {
@@ -401,8 +401,8 @@ function resolveSkillPath(p: string, cwd: string): string {
* Load skills from all configured locations.
* Returns skills and any validation diagnostics.
*/
export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
const { cwd = process.cwd(), agentDir, skillPaths = [], includeDefaults = true } = options;
export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
const { cwd, agentDir, skillPaths, includeDefaults } = options;
// Resolve agentDir - if not provided, use default from config
const resolvedAgentDir = agentDir ?? getAgentDir();

View File

@@ -16,8 +16,8 @@ export interface BuildSystemPromptOptions {
promptGuidelines?: string[];
/** Text to append to system prompt. */
appendSystemPrompt?: string;
/** Working directory. Default: process.cwd() */
cwd?: string;
/** Working directory. */
cwd: string;
/** Pre-loaded context files. */
contextFiles?: Array<{ path: string; content: string }>;
/** Pre-loaded skills. */
@@ -25,7 +25,7 @@ export interface BuildSystemPromptOptions {
}
/** Build the system prompt with tools, guidelines, and context */
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
export function buildSystemPrompt(options: BuildSystemPromptOptions): string {
const {
customPrompt,
selectedTools,
@@ -36,7 +36,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
contextFiles: providedContextFiles,
skills: providedSkills,
} = options;
const resolvedCwd = cwd ?? process.cwd();
const resolvedCwd = cwd;
const promptCwd = resolvedCwd.replace(/\\/g, "/");
const now = new Date();

View File

@@ -444,7 +444,3 @@ export function createBashToolDefinition(
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
return wrapToolDefinition(createBashToolDefinition(cwd, options));
}
/** Default bash tool using process.cwd() for backwards compatibility. */
export const bashToolDefinition = createBashToolDefinition(process.cwd());
export const bashTool = createBashTool(process.cwd());

View File

@@ -485,7 +485,3 @@ export function createEditToolDefinition(
export function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {
return wrapToolDefinition(createEditToolDefinition(cwd, options));
}
/** Default edit tool using process.cwd() for backwards compatibility. */
export const editToolDefinition = createEditToolDefinition(process.cwd());
export const editTool = createEditTool(process.cwd());

View File

@@ -368,7 +368,3 @@ export function createFindToolDefinition(
export function createFindTool(cwd: string, options?: FindToolOptions): AgentTool<typeof findSchema> {
return wrapToolDefinition(createFindToolDefinition(cwd, options));
}
/** Default find tool using process.cwd() for backwards compatibility. */
export const findToolDefinition = createFindToolDefinition(process.cwd());
export const findTool = createFindTool(process.cwd());

View File

@@ -382,7 +382,3 @@ export function createGrepToolDefinition(
export function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {
return wrapToolDefinition(createGrepToolDefinition(cwd, options));
}
/** Default grep tool using process.cwd() for backwards compatibility. */
export const grepToolDefinition = createGrepToolDefinition(process.cwd());
export const grepTool = createGrepTool(process.cwd());

View File

@@ -227,7 +227,3 @@ export function createLsToolDefinition(
export function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {
return wrapToolDefinition(createLsToolDefinition(cwd, options));
}
/** Default ls tool using process.cwd() for backwards compatibility. */
export const lsToolDefinition = createLsToolDefinition(process.cwd());
export const lsTool = createLsTool(process.cwd());

View File

@@ -271,7 +271,3 @@ export function createReadToolDefinition(
export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
return wrapToolDefinition(createReadToolDefinition(cwd, options));
}
/** Default read tool using process.cwd() for backwards compatibility. */
export const readToolDefinition = createReadToolDefinition(process.cwd());
export const readTool = createReadTool(process.cwd());

View File

@@ -279,7 +279,3 @@ export function createWriteToolDefinition(
export function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {
return wrapToolDefinition(createWriteToolDefinition(cwd, options));
}
/** Default write tool using process.cwd() for backwards compatibility. */
export const writeToolDefinition = createWriteToolDefinition(process.cwd());
export const writeTool = createWriteTool(process.cwd());