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

- treat tools as a global allowlist across built-in, extension, and SDK tools
- remove process-cwd singleton tool usage from SDK and CLI paths
- add regression coverage for extension tool filtering

closes #3452
closes #2835
This commit is contained in:
Mario Zechner
2026-04-20 21:53:07 +02:00
parent ed89480f20
commit 27c1544839
16 changed files with 295 additions and 199 deletions

View File

@@ -13,6 +13,7 @@
### Fixed ### Fixed
- Fixed `ctx.ui.setWorkingIndicator()` custom frames to render verbatim instead of forcing the theme accent color, so extensions now own working-indicator coloring when they customize it ([#3467](https://github.com/badlogic/pi-mono/issues/3467))
- Fixed `pi update` reinstalling npm packages that are already at the latest published version by checking the installed package version before running `npm install <pkg>@latest` ([#3000](https://github.com/badlogic/pi-mono/issues/3000)) - Fixed `pi update` reinstalling npm packages that are already at the latest published version by checking the installed package version before running `npm install <pkg>@latest` ([#3000](https://github.com/badlogic/pi-mono/issues/3000))
- Fixed built-in tool wrapping to use the same extension-runner context path as extension tools, so built-in tools receive execution context and `read` can warn when the current model does not support images ([#3429](https://github.com/badlogic/pi-mono/issues/3429)) - Fixed built-in tool wrapping to use the same extension-runner context path as extension tools, so built-in tools receive execution context and `read` can warn when the current model does not support images ([#3429](https://github.com/badlogic/pi-mono/issues/3429))
- Fixed threaded `/resume` session relationships and current-session detection to canonicalize symlinked session paths during selector comparisons, so shared session directories no longer break parent-child matching or active-session delete protection ([#3364](https://github.com/badlogic/pi-mono/issues/3364)) - Fixed threaded `/resume` session relationships and current-session detection to canonicalize symlinked session paths during selector comparisons, so shared session directories no longer break parent-child matching or active-session delete protection ([#3364](https://github.com/badlogic/pi-mono/issues/3364))

View File

@@ -2049,8 +2049,16 @@ ctx.ui.setWorkingMessage("Thinking deeply...");
ctx.ui.setWorkingMessage(); // Restore default ctx.ui.setWorkingMessage(); // Restore default
// Working indicator (shown during streaming) // Working indicator (shown during streaming)
ctx.ui.setWorkingIndicator({ frames: ["●"] }); // Static dot ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // Static dot
ctx.ui.setWorkingIndicator({ frames: ["·", "•", "●", "•"], intervalMs: 120 }); ctx.ui.setWorkingIndicator({
frames: [
ctx.ui.theme.fg("dim", "·"),
ctx.ui.theme.fg("muted", "•"),
ctx.ui.theme.fg("accent", "●"),
ctx.ui.theme.fg("muted", "•"),
],
intervalMs: 120,
});
ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator
ctx.ui.setWorkingIndicator(); // Restore default spinner ctx.ui.setWorkingIndicator(); // Restore default spinner
@@ -2098,6 +2106,8 @@ ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text"); // Access current theme ctx.ui.theme.fg("accent", "styled text"); // Access current theme
``` ```
Custom working-indicator frames are rendered verbatim. If you want colors, add them to the frame strings yourself, for example with `ctx.ui.theme.fg(...)`.
### Custom Components ### Custom Components
For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called: For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:

View File

@@ -1,56 +1,44 @@
/** /**
* Tools Configuration * Tools Configuration
* *
* Use built-in tool sets or individual tools. * Use tool names to choose which built-in tools are enabled.
* *
* IMPORTANT: When using a custom `cwd`, you must use the tool factory functions * Tool names are matched against all available tools. If you use a custom `cwd`,
* (createCodingTools, createReadOnlyTools, createReadTool, etc.) to ensure * createAgentSession() applies that cwd when it builds the actual built-in tools.
* tools resolve paths relative to your cwd, not process.cwd().
* *
* For custom tools, see 06-extensions.ts - custom tools are now registered * For custom tools, see 06-extensions.ts - custom tools are registered via the
* via the extensions system using pi.registerTool(). * extensions system using pi.registerTool().
*/ */
import { import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
bashTool,
createAgentSession,
createBashTool,
createCodingTools,
createGrepTool,
createReadTool,
grepTool,
readOnlyTools,
readTool,
SessionManager,
} from "@mariozechner/pi-coding-agent";
// Read-only mode (no edit/write) - uses process.cwd() // Read-only mode (no edit/write)
await createAgentSession({ await createAgentSession({
tools: readOnlyTools, tools: ["read", "grep", "find", "ls"],
sessionManager: SessionManager.inMemory(), sessionManager: SessionManager.inMemory(),
}); });
console.log("Read-only session created"); console.log("Read-only session created");
// Custom tool selection - uses process.cwd() // Custom tool selection
await createAgentSession({ await createAgentSession({
tools: [readTool, bashTool, grepTool], tools: ["read", "bash", "grep"],
sessionManager: SessionManager.inMemory(), sessionManager: SessionManager.inMemory(),
}); });
console.log("Custom tools session created"); console.log("Custom tools session created");
// With custom cwd - MUST use factory functions! // With custom cwd
const customCwd = "/path/to/project"; const customCwd = "/path/to/project";
await createAgentSession({ await createAgentSession({
cwd: customCwd, cwd: customCwd,
tools: createCodingTools(customCwd), // Tools resolve paths relative to customCwd tools: ["read", "bash", "edit", "write"],
sessionManager: SessionManager.inMemory(), sessionManager: SessionManager.inMemory(customCwd),
}); });
console.log("Custom cwd session created"); console.log("Custom cwd session created");
// Or pick specific tools for custom cwd // Or pick specific tools for custom cwd
await createAgentSession({ await createAgentSession({
cwd: customCwd, cwd: customCwd,
tools: [createReadTool(customCwd), createBashTool(customCwd), createGrepTool(customCwd)], tools: ["read", "bash", "grep"],
sessionManager: SessionManager.inMemory(), sessionManager: SessionManager.inMemory(customCwd),
}); });
console.log("Specific tools with custom cwd session created"); console.log("Specific tools with custom cwd session created");

View File

@@ -2,19 +2,13 @@
* Full Control * Full Control
* *
* Replace everything - no discovery, explicit configuration. * Replace everything - no discovery, explicit configuration.
*
* IMPORTANT: When providing `tools` with a custom `cwd`, use the tool factory
* functions (createReadTool, createBashTool, etc.) to ensure tools resolve
* paths relative to your cwd.
*/ */
import { getModel } from "@mariozechner/pi-ai"; import { getModel } from "@mariozechner/pi-ai";
import { import {
AuthStorage, AuthStorage,
createAgentSession, createAgentSession,
createBashTool,
createExtensionRuntime, createExtensionRuntime,
createReadTool,
ModelRegistry, ModelRegistry,
type ResourceLoader, type ResourceLoader,
SessionManager, SessionManager,
@@ -41,7 +35,6 @@ const settingsManager = SettingsManager.inMemory({
retry: { enabled: true, maxRetries: 2 }, retry: { enabled: true, maxRetries: 2 },
}); });
// When using a custom cwd with explicit tools, use the factory functions
const cwd = process.cwd(); const cwd = process.cwd();
const resourceLoader: ResourceLoader = { const resourceLoader: ResourceLoader = {
@@ -65,9 +58,8 @@ const { session } = await createAgentSession({
authStorage, authStorage,
modelRegistry, modelRegistry,
resourceLoader, resourceLoader,
// Use factory functions with the same cwd to ensure path resolution works correctly tools: ["read", "bash"],
tools: [createReadTool(cwd), createBashTool(cwd)], sessionManager: SessionManager.inMemory(cwd),
sessionManager: SessionManager.inMemory(),
settingsManager, settingsManager,
}); });

View File

@@ -6,7 +6,6 @@ import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
import chalk from "chalk"; import chalk from "chalk";
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js"; import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js";
import type { ExtensionFlag } from "../core/extensions/types.js"; import type { ExtensionFlag } from "../core/extensions/types.js";
import { allTools, type ToolName } from "../core/tools/index.js";
export type Mode = "text" | "json" | "rpc"; export type Mode = "text" | "json" | "rpc";
@@ -27,7 +26,7 @@ export interface Args {
fork?: string; fork?: string;
sessionDir?: string; sessionDir?: string;
models?: string[]; models?: string[];
tools?: ToolName[]; tools?: string[];
noTools?: boolean; noTools?: boolean;
extensions?: string[]; extensions?: string[];
noExtensions?: boolean; noExtensions?: boolean;
@@ -104,19 +103,10 @@ export function parseArgs(args: string[]): Args {
} else if (arg === "--no-tools") { } else if (arg === "--no-tools") {
result.noTools = true; result.noTools = true;
} else if (arg === "--tools" && i + 1 < args.length) { } else if (arg === "--tools" && i + 1 < args.length) {
const toolNames = args[++i].split(",").map((s) => s.trim()); result.tools = args[++i]
const validTools: ToolName[] = []; .split(",")
for (const name of toolNames) { .map((s) => s.trim())
if (name in allTools) { .filter((name) => name.length > 0);
validTools.push(name as ToolName);
} else {
result.diagnostics.push({
type: "warning",
message: `Unknown tool "${name}". Valid tools: ${Object.keys(allTools).join(", ")}`,
});
}
}
result.tools = validTools;
} else if (arg === "--thinking" && i + 1 < args.length) { } else if (arg === "--thinking" && i + 1 < args.length) {
const level = args[++i]; const level = args[++i];
if (isValidThinkingLevel(level)) { if (isValidThinkingLevel(level)) {
@@ -231,9 +221,9 @@ ${chalk.bold("Options:")}
--no-session Don't save session (ephemeral) --no-session Don't save session (ephemeral)
--models <patterns> Comma-separated model patterns for Ctrl+P cycling --models <patterns> Comma-separated model patterns for Ctrl+P cycling
Supports globs (anthropic/*, *sonnet*) and fuzzy matching Supports globs (anthropic/*, *sonnet*) and fuzzy matching
--no-tools Disable all built-in tools --no-tools Disable all tools by default (built-in and extension)
--tools <tools> Comma-separated list of tools to enable (default: read,bash,edit,write) --tools <tools> Comma-separated allowlist of tool names to enable
Available: read, bash, edit, write, grep, find, ls Applies to built-in and extension tools
--thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh
--extension, -e <path> Load an extension file (can be used multiple times) --extension, -e <path> Load an extension file (can be used multiple times)
--no-extensions, -ne Disable extension discovery (explicit -e paths still work) --no-extensions, -ne Disable extension discovery (explicit -e paths still work)
@@ -332,7 +322,7 @@ ${chalk.bold("Environment Variables:")}
PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/) PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
PI_AI_ANTIGRAVITY_VERSION - Override Antigravity User-Agent version (e.g., 1.23.0) PI_AI_ANTIGRAVITY_VERSION - Override Antigravity User-Agent version (e.g., 1.23.0)
${chalk.bold("Available Tools (default: read, bash, edit, write):")} ${chalk.bold("Built-in Tool Names:")}
read - Read file contents read - Read file contents
bash - Execute bash commands bash - Execute bash commands
edit - Edit files with find/replace edit - Edit files with find/replace

View File

@@ -9,7 +9,6 @@ import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type Resource
import { type CreateAgentSessionResult, createAgentSession } from "./sdk.js"; import { type CreateAgentSessionResult, createAgentSession } from "./sdk.js";
import type { SessionManager } from "./session-manager.js"; import type { SessionManager } from "./session-manager.js";
import { SettingsManager } from "./settings-manager.js"; import { SettingsManager } from "./settings-manager.js";
import type { Tool } from "./tools/index.js";
/** /**
* Non-fatal issues collected while creating services or sessions. * Non-fatal issues collected while creating services or sessions.
@@ -53,7 +52,7 @@ export interface CreateAgentSessionFromServicesOptions {
model?: Model<any>; model?: Model<any>;
thinkingLevel?: ThinkingLevel; thinkingLevel?: ThinkingLevel;
scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>; scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
tools?: Tool[]; tools?: string[];
customTools?: ToolDefinition[]; customTools?: ToolDefinition[];
} }

View File

@@ -150,6 +150,8 @@ export interface AgentSessionConfig {
modelRegistry: ModelRegistry; modelRegistry: ModelRegistry;
/** Initial active built-in tool names. Default: [read, bash, edit, write] */ /** Initial active built-in tool names. Default: [read, bash, edit, write] */
initialActiveToolNames?: string[]; initialActiveToolNames?: string[];
/** Optional allowlist of tool names. When provided, only these tool names are exposed. */
allowedToolNames?: string[];
/** /**
* Override base tools (useful for custom runtimes). * Override base tools (useful for custom runtimes).
* *
@@ -278,6 +280,7 @@ export class AgentSession {
private _cwd: string; private _cwd: string;
private _extensionRunnerRef?: { current?: ExtensionRunner }; private _extensionRunnerRef?: { current?: ExtensionRunner };
private _initialActiveToolNames?: string[]; private _initialActiveToolNames?: string[];
private _allowedToolNames?: Set<string>;
private _baseToolsOverride?: Record<string, AgentTool>; private _baseToolsOverride?: Record<string, AgentTool>;
private _sessionStartEvent: SessionStartEvent; private _sessionStartEvent: SessionStartEvent;
private _extensionUIContext?: ExtensionUIContext; private _extensionUIContext?: ExtensionUIContext;
@@ -309,6 +312,7 @@ export class AgentSession {
this._modelRegistry = config.modelRegistry; this._modelRegistry = config.modelRegistry;
this._extensionRunnerRef = config.extensionRunnerRef; this._extensionRunnerRef = config.extensionRunnerRef;
this._initialActiveToolNames = config.initialActiveToolNames; this._initialActiveToolNames = config.initialActiveToolNames;
this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;
this._baseToolsOverride = config.baseToolsOverride; this._baseToolsOverride = config.baseToolsOverride;
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" }; this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
@@ -2221,6 +2225,8 @@ export class AgentSession {
private _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void { private _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void {
const previousRegistryNames = new Set(this._toolRegistry.keys()); const previousRegistryNames = new Set(this._toolRegistry.keys());
const previousActiveToolNames = this.getActiveToolNames(); const previousActiveToolNames = this.getActiveToolNames();
const allowedToolNames = this._allowedToolNames;
const isAllowedTool = (name: string): boolean => !allowedToolNames || allowedToolNames.has(name);
const registeredTools = this._extensionRunner.getAllRegisteredTools(); const registeredTools = this._extensionRunner.getAllRegisteredTools();
const allCustomTools = [ const allCustomTools = [
@@ -2229,9 +2235,11 @@ export class AgentSession {
definition, definition,
sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }), sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
})), })),
]; ].filter((tool) => isAllowedTool(tool.definition.name));
const definitionRegistry = new Map<string, ToolDefinitionEntry>( const definitionRegistry = new Map<string, ToolDefinitionEntry>(
Array.from(this._baseToolDefinitions.entries()).map(([name, definition]) => [ Array.from(this._baseToolDefinitions.entries())
.filter(([name]) => isAllowedTool(name))
.map(([name, definition]) => [
name, name,
{ {
definition, definition,
@@ -2265,7 +2273,9 @@ export class AgentSession {
const runner = this._extensionRunner; const runner = this._extensionRunner;
const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner); const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);
const wrappedBuiltInTools = wrapRegisteredTools( const wrappedBuiltInTools = wrapRegisteredTools(
Array.from(this._baseToolDefinitions.values()).map((definition) => ({ Array.from(this._baseToolDefinitions.values())
.filter((definition) => isAllowedTool(definition.name))
.map((definition) => ({
definition, definition,
sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, { source: "builtin" }), sourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, { source: "builtin" }),
})), })),
@@ -2278,11 +2288,17 @@ export class AgentSession {
} }
this._toolRegistry = toolRegistry; this._toolRegistry = toolRegistry;
const nextActiveToolNames = options?.activeToolNames const nextActiveToolNames = (
? [...options.activeToolNames] options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]
: [...previousActiveToolNames]; ).filter((name) => isAllowedTool(name));
if (options?.includeAllExtensionTools) { if (allowedToolNames) {
for (const toolName of this._toolRegistry.keys()) {
if (allowedToolNames.has(toolName)) {
nextActiveToolNames.push(toolName);
}
}
} else if (options?.includeAllExtensionTools) {
for (const tool of wrappedExtensionTools) { for (const tool of wrappedExtensionTools) {
nextActiveToolNames.push(tool.name); nextActiveToolNames.push(tool.name);
} }

View File

@@ -16,9 +16,6 @@ import { SettingsManager } from "./settings-manager.js";
import { isInstallTelemetryEnabled } from "./telemetry.js"; import { isInstallTelemetryEnabled } from "./telemetry.js";
import { time } from "./timings.js"; import { time } from "./timings.js";
import { import {
allTools,
bashTool,
codingTools,
createBashTool, createBashTool,
createCodingTools, createCodingTools,
createEditTool, createEditTool,
@@ -28,16 +25,8 @@ import {
createReadOnlyTools, createReadOnlyTools,
createReadTool, createReadTool,
createWriteTool, createWriteTool,
editTool,
findTool,
grepTool,
lsTool,
readOnlyTools,
readTool,
type Tool,
type ToolName, type ToolName,
withFileMutationQueue, withFileMutationQueue,
writeTool,
} from "./tools/index.js"; } from "./tools/index.js";
export interface CreateAgentSessionOptions { export interface CreateAgentSessionOptions {
@@ -58,8 +47,14 @@ export interface CreateAgentSessionOptions {
/** Models available for cycling (Ctrl+P in interactive mode) */ /** Models available for cycling (Ctrl+P in interactive mode) */
scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>; scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */ /**
tools?: Tool[]; * Optional allowlist of tool names.
*
* When omitted, pi enables the default built-in tools (read, bash, edit, write)
* and leaves extension/custom tools enabled.
* When provided, only the listed tool names are enabled.
*/
tools?: string[];
/** Custom tools to register (in addition to built-in tools). */ /** Custom tools to register (in addition to built-in tools). */
customTools?: ToolDefinition[]; customTools?: ToolDefinition[];
@@ -102,17 +97,6 @@ export type { Skill } from "./skills.js";
export type { Tool } from "./tools/index.js"; export type { Tool } from "./tools/index.js";
export { export {
// Pre-built tools (use process.cwd())
readTool,
bashTool,
editTool,
writeTool,
grepTool,
findTool,
lsTool,
codingTools,
readOnlyTools,
allTools as allBuiltInTools,
withFileMutationQueue, withFileMutationQueue,
// Tool factories (for custom cwd) // Tool factories (for custom cwd)
createCodingTools, createCodingTools,
@@ -185,7 +169,7 @@ function getOpenRouterAttributionHeaders(
* ``` * ```
*/ */
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> { export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
const cwd = options.cwd ?? process.cwd(); const cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();
const agentDir = options.agentDir ?? getDefaultAgentDir(); const agentDir = options.agentDir ?? getDefaultAgentDir();
let resourceLoader = options.resourceLoader; let resourceLoader = options.resourceLoader;
@@ -261,9 +245,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
} }
const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"]; const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"];
const initialActiveToolNames: ToolName[] = options.tools const initialActiveToolNames: string[] = options.tools ? [...options.tools] : defaultActiveToolNames;
? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allTools)
: defaultActiveToolNames;
let agent: Agent; let agent: Agent;
@@ -384,6 +366,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
customTools: options.customTools, customTools: options.customTools,
modelRegistry, modelRegistry,
initialActiveToolNames, initialActiveToolNames,
allowedToolNames: options.tools,
extensionRunnerRef, extensionRunnerRef,
sessionStartEvent: options.sessionStartEvent, sessionStartEvent: options.sessionStartEvent,
}); });

View File

@@ -5,8 +5,6 @@ export {
type BashToolDetails, type BashToolDetails,
type BashToolInput, type BashToolInput,
type BashToolOptions, type BashToolOptions,
bashTool,
bashToolDefinition,
createBashTool, createBashTool,
createBashToolDefinition, createBashToolDefinition,
createLocalBashOperations, createLocalBashOperations,
@@ -18,8 +16,6 @@ export {
type EditToolDetails, type EditToolDetails,
type EditToolInput, type EditToolInput,
type EditToolOptions, type EditToolOptions,
editTool,
editToolDefinition,
} from "./edit.js"; } from "./edit.js";
export { withFileMutationQueue } from "./file-mutation-queue.js"; export { withFileMutationQueue } from "./file-mutation-queue.js";
export { export {
@@ -29,8 +25,6 @@ export {
type FindToolDetails, type FindToolDetails,
type FindToolInput, type FindToolInput,
type FindToolOptions, type FindToolOptions,
findTool,
findToolDefinition,
} from "./find.js"; } from "./find.js";
export { export {
createGrepTool, createGrepTool,
@@ -39,8 +33,6 @@ export {
type GrepToolDetails, type GrepToolDetails,
type GrepToolInput, type GrepToolInput,
type GrepToolOptions, type GrepToolOptions,
grepTool,
grepToolDefinition,
} from "./grep.js"; } from "./grep.js";
export { export {
createLsTool, createLsTool,
@@ -49,8 +41,6 @@ export {
type LsToolDetails, type LsToolDetails,
type LsToolInput, type LsToolInput,
type LsToolOptions, type LsToolOptions,
lsTool,
lsToolDefinition,
} from "./ls.js"; } from "./ls.js";
export { export {
createReadTool, createReadTool,
@@ -59,8 +49,6 @@ export {
type ReadToolDetails, type ReadToolDetails,
type ReadToolInput, type ReadToolInput,
type ReadToolOptions, type ReadToolOptions,
readTool,
readToolDefinition,
} from "./read.js"; } from "./read.js";
export { export {
DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES,
@@ -78,80 +66,90 @@ export {
type WriteOperations, type WriteOperations,
type WriteToolInput, type WriteToolInput,
type WriteToolOptions, type WriteToolOptions,
writeTool,
writeToolDefinition,
} from "./write.js"; } from "./write.js";
import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { AgentTool } from "@mariozechner/pi-agent-core";
import type { ToolDefinition } from "../extensions/types.js"; import type { ToolDefinition } from "../extensions/types.js";
import { import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.js";
type BashToolOptions, import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.js";
bashTool, import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.js";
bashToolDefinition, import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.js";
createBashTool, import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.js";
createBashToolDefinition, import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.js";
} from "./bash.js"; import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.js";
import { createEditTool, createEditToolDefinition, editTool, editToolDefinition } from "./edit.js";
import { createFindTool, createFindToolDefinition, findTool, findToolDefinition } from "./find.js";
import { createGrepTool, createGrepToolDefinition, grepTool, grepToolDefinition } from "./grep.js";
import { createLsTool, createLsToolDefinition, lsTool, lsToolDefinition } from "./ls.js";
import {
createReadTool,
createReadToolDefinition,
type ReadToolOptions,
readTool,
readToolDefinition,
} from "./read.js";
import { createWriteTool, createWriteToolDefinition, writeTool, writeToolDefinition } from "./write.js";
export type Tool = AgentTool<any>; export type Tool = AgentTool<any>;
export type ToolDef = ToolDefinition<any, any>; export type ToolDef = ToolDefinition<any, any>;
export type ToolName = "read" | "bash" | "edit" | "write" | "grep" | "find" | "ls";
export const codingTools: Tool[] = [readTool, bashTool, editTool, writeTool]; export const allToolNames: Set<ToolName> = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
export const readOnlyTools: Tool[] = [readTool, grepTool, findTool, lsTool];
export const allTools = {
read: readTool,
bash: bashTool,
edit: editTool,
write: writeTool,
grep: grepTool,
find: findTool,
ls: lsTool,
};
export const allToolDefinitions = {
read: readToolDefinition,
bash: bashToolDefinition,
edit: editToolDefinition,
write: writeToolDefinition,
grep: grepToolDefinition,
find: findToolDefinition,
ls: lsToolDefinition,
};
export type ToolName = keyof typeof allTools;
export interface ToolsOptions { export interface ToolsOptions {
read?: ReadToolOptions; read?: ReadToolOptions;
bash?: BashToolOptions; bash?: BashToolOptions;
write?: WriteToolOptions;
edit?: EditToolOptions;
grep?: GrepToolOptions;
find?: FindToolOptions;
ls?: LsToolOptions;
}
export function createToolDefinition(toolName: ToolName, cwd: string, options?: ToolsOptions): ToolDef {
switch (toolName) {
case "read":
return createReadToolDefinition(cwd, options?.read);
case "bash":
return createBashToolDefinition(cwd, options?.bash);
case "edit":
return createEditToolDefinition(cwd, options?.edit);
case "write":
return createWriteToolDefinition(cwd, options?.write);
case "grep":
return createGrepToolDefinition(cwd, options?.grep);
case "find":
return createFindToolDefinition(cwd, options?.find);
case "ls":
return createLsToolDefinition(cwd, options?.ls);
default:
throw new Error(`Unknown tool name: ${toolName}`);
}
}
export function createTool(toolName: ToolName, cwd: string, options?: ToolsOptions): Tool {
switch (toolName) {
case "read":
return createReadTool(cwd, options?.read);
case "bash":
return createBashTool(cwd, options?.bash);
case "edit":
return createEditTool(cwd, options?.edit);
case "write":
return createWriteTool(cwd, options?.write);
case "grep":
return createGrepTool(cwd, options?.grep);
case "find":
return createFindTool(cwd, options?.find);
case "ls":
return createLsTool(cwd, options?.ls);
default:
throw new Error(`Unknown tool name: ${toolName}`);
}
} }
export function createCodingToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] { export function createCodingToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] {
return [ return [
createReadToolDefinition(cwd, options?.read), createReadToolDefinition(cwd, options?.read),
createBashToolDefinition(cwd, options?.bash), createBashToolDefinition(cwd, options?.bash),
createEditToolDefinition(cwd), createEditToolDefinition(cwd, options?.edit),
createWriteToolDefinition(cwd), createWriteToolDefinition(cwd, options?.write),
]; ];
} }
export function createReadOnlyToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] { export function createReadOnlyToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] {
return [ return [
createReadToolDefinition(cwd, options?.read), createReadToolDefinition(cwd, options?.read),
createGrepToolDefinition(cwd), createGrepToolDefinition(cwd, options?.grep),
createFindToolDefinition(cwd), createFindToolDefinition(cwd, options?.find),
createLsToolDefinition(cwd), createLsToolDefinition(cwd, options?.ls),
]; ];
} }
@@ -159,11 +157,11 @@ export function createAllToolDefinitions(cwd: string, options?: ToolsOptions): R
return { return {
read: createReadToolDefinition(cwd, options?.read), read: createReadToolDefinition(cwd, options?.read),
bash: createBashToolDefinition(cwd, options?.bash), bash: createBashToolDefinition(cwd, options?.bash),
edit: createEditToolDefinition(cwd), edit: createEditToolDefinition(cwd, options?.edit),
write: createWriteToolDefinition(cwd), write: createWriteToolDefinition(cwd, options?.write),
grep: createGrepToolDefinition(cwd), grep: createGrepToolDefinition(cwd, options?.grep),
find: createFindToolDefinition(cwd), find: createFindToolDefinition(cwd, options?.find),
ls: createLsToolDefinition(cwd), ls: createLsToolDefinition(cwd, options?.ls),
}; };
} }
@@ -171,23 +169,28 @@ export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] {
return [ return [
createReadTool(cwd, options?.read), createReadTool(cwd, options?.read),
createBashTool(cwd, options?.bash), createBashTool(cwd, options?.bash),
createEditTool(cwd), createEditTool(cwd, options?.edit),
createWriteTool(cwd), createWriteTool(cwd, options?.write),
]; ];
} }
export function createReadOnlyTools(cwd: string, options?: ToolsOptions): Tool[] { export function createReadOnlyTools(cwd: string, options?: ToolsOptions): Tool[] {
return [createReadTool(cwd, options?.read), createGrepTool(cwd), createFindTool(cwd), createLsTool(cwd)]; return [
createReadTool(cwd, options?.read),
createGrepTool(cwd, options?.grep),
createFindTool(cwd, options?.find),
createLsTool(cwd, options?.ls),
];
} }
export function createAllTools(cwd: string, options?: ToolsOptions): Record<ToolName, Tool> { export function createAllTools(cwd: string, options?: ToolsOptions): Record<ToolName, Tool> {
return { return {
read: createReadTool(cwd, options?.read), read: createReadTool(cwd, options?.read),
bash: createBashTool(cwd, options?.bash), bash: createBashTool(cwd, options?.bash),
edit: createEditTool(cwd), edit: createEditTool(cwd, options?.edit),
write: createWriteTool(cwd), write: createWriteTool(cwd, options?.write),
grep: createGrepTool(cwd), grep: createGrepTool(cwd, options?.grep),
find: createFindTool(cwd), find: createFindTool(cwd, options?.find),
ls: createLsTool(cwd), ls: createLsTool(cwd, options?.ls),
}; };
} }

View File

@@ -39,7 +39,6 @@ import {
import { SessionManager } from "./core/session-manager.js"; import { SessionManager } from "./core/session-manager.js";
import { SettingsManager } from "./core/settings-manager.js"; import { SettingsManager } from "./core/settings-manager.js";
import { printTimings, resetTimings, time } from "./core/timings.js"; import { printTimings, resetTimings, time } from "./core/timings.js";
import { allTools } from "./core/tools/index.js";
import { runMigrations, showDeprecationWarnings } from "./migrations.js"; import { runMigrations, showDeprecationWarnings } from "./migrations.js";
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js"; import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js"; import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";
@@ -368,14 +367,10 @@ function buildSessionOptions(
// Tools // Tools
if (parsed.noTools) { if (parsed.noTools) {
// --no-tools: start with no built-in tools // --no-tools: start with no built-in tools
// --tools can still add specific ones back // --tools can still add specific ones back, including extension tools.
if (parsed.tools && parsed.tools.length > 0) { options.tools = parsed.tools && parsed.tools.length > 0 ? [...parsed.tools] : [];
options.tools = parsed.tools.map((name) => allTools[name]);
} else {
options.tools = [];
}
} else if (parsed.tools) { } else if (parsed.tools) {
options.tools = parsed.tools.map((name) => allTools[name]); options.tools = [...parsed.tools];
} }
return { options, cliThinkingFromModel, diagnostics }; return { options, cliThinkingFromModel, diagnostics };

View File

@@ -22,7 +22,6 @@ import {
} from "../src/core/agent-session-runtime.js"; } from "../src/core/agent-session-runtime.js";
import { AuthStorage } from "../src/core/auth-storage.js"; import { AuthStorage } from "../src/core/auth-storage.js";
import { SessionManager } from "../src/core/session-manager.js"; import { SessionManager } from "../src/core/session-manager.js";
import { codingTools } from "../src/core/tools/index.js";
import { API_KEY } from "./utilities.js"; import { API_KEY } from "./utilities.js";
describe.skipIf(!API_KEY)("AgentSession forking", () => { describe.skipIf(!API_KEY)("AgentSession forking", () => {
@@ -40,7 +39,6 @@ describe.skipIf(!API_KEY)("AgentSession forking", () => {
if (runtimeHost) { if (runtimeHost) {
await runtimeHost.dispose(); await runtimeHost.dispose();
} }
process.chdir(tmpdir());
if (tempDir && existsSync(tempDir)) { if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true }); rmSync(tempDir, { recursive: true });
} }
@@ -73,7 +71,7 @@ describe.skipIf(!API_KEY)("AgentSession forking", () => {
sessionManager, sessionManager,
sessionStartEvent, sessionStartEvent,
model, model,
tools: codingTools, tools: ["read", "bash", "edit", "write"],
})), })),
services, services,
diagnostics: services.diagnostics, diagnostics: services.diagnostics,

View File

@@ -76,7 +76,6 @@ describe("AgentSessionRuntime session lifecycle events", () => {
cleanups.push(async () => { cleanups.push(async () => {
await runtimeHost.dispose(); await runtimeHost.dispose();
faux.unregister(); faux.unregister();
process.chdir(tmpdir());
if (existsSync(tempDir)) { if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true }); rmSync(tempDir, { recursive: true, force: true });
} }

View File

@@ -18,7 +18,7 @@ import { AgentSession } from "../src/core/agent-session.js";
import { ModelRegistry } from "../src/core/model-registry.js"; import { ModelRegistry } from "../src/core/model-registry.js";
import { SessionManager } from "../src/core/session-manager.js"; import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js";
import { codingTools } from "../src/core/tools/index.js"; import { createCodingTools } from "../src/core/tools/index.js";
import { import {
API_KEY, API_KEY,
createTestResourceLoader, createTestResourceLoader,
@@ -70,7 +70,7 @@ describe.skipIf(!HAS_ANTIGRAVITY_AUTH)("Compaction with thinking models (Antigra
initialState: { initialState: {
model, model,
systemPrompt: "You are a helpful assistant. Be concise.", systemPrompt: "You are a helpful assistant. Be concise.",
tools: codingTools, tools: createCodingTools(process.cwd()),
thinkingLevel, thinkingLevel,
}, },
}); });
@@ -168,7 +168,7 @@ describe.skipIf(!HAS_ANTHROPIC_AUTH)("Compaction with thinking models (Anthropic
initialState: { initialState: {
model, model,
systemPrompt: "You are a helpful assistant. Be concise.", systemPrompt: "You are a helpful assistant. Be concise.",
tools: codingTools, tools: createCodingTools(process.cwd()),
thinkingLevel, thinkingLevel,
}, },
}); });

View File

@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, rmSync } from "node:fs"; import { existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai"; import { getModel } from "@mariozechner/pi-ai";
@@ -63,4 +63,33 @@ describe("createAgentSession session manager defaults", () => {
session.dispose(); session.dispose();
}); });
it("derives cwd from an explicit sessionManager when cwd is omitted", async () => {
const model = getModel("anthropic", "claude-sonnet-4-5");
expect(model).toBeTruthy();
const sessionCwd = join(tempDir, "session-project");
mkdirSync(sessionCwd, { recursive: true });
const sessionManager = SessionManager.inMemory(sessionCwd);
const { session } = await createAgentSession({
agentDir,
model: model!,
sessionManager,
});
expect(session.sessionManager).toBe(sessionManager);
expect(session.systemPrompt).toContain(`Current working directory: ${sessionCwd}`);
const bashTool = session.agent.state.tools.find((tool) => tool.name === "bash");
expect(bashTool).toBeTruthy();
const result = await bashTool!.execute("test", { command: "pwd" });
const output = result.content
.filter((item): item is { type: "text"; text: string } => item.type === "text")
.map((item) => item.text)
.join("");
expect(realpathSync(output.trim())).toBe(realpathSync(sessionCwd));
session.dispose();
});
}); });

View File

@@ -28,7 +28,6 @@ describe("AgentSessionRuntime characterization", () => {
while (cleanups.length > 0) { while (cleanups.length > 0) {
await cleanups.pop()?.(); await cleanups.pop()?.();
} }
process.chdir(tmpdir());
}); });
async function createRuntimeForTest( async function createRuntimeForTest(
@@ -391,7 +390,7 @@ describe("AgentSessionRuntime characterization", () => {
await expect(runtime.fork("missing-entry")).rejects.toThrow("Invalid entry ID for forking"); await expect(runtime.fork("missing-entry")).rejects.toThrow("Invalid entry ID for forking");
}); });
it("updates process.cwd() on cross-cwd session replacement", async () => { it("updates the runtime session cwd on cross-cwd session replacement", async () => {
const firstDir = join(tmpdir(), `pi-runtime-cwd-a-${Date.now()}-${Math.random().toString(36).slice(2)}`); const firstDir = join(tmpdir(), `pi-runtime-cwd-a-${Date.now()}-${Math.random().toString(36).slice(2)}`);
const secondDir = join(tmpdir(), `pi-runtime-cwd-b-${Date.now()}-${Math.random().toString(36).slice(2)}`); const secondDir = join(tmpdir(), `pi-runtime-cwd-b-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(firstDir, { recursive: true }); mkdirSync(firstDir, { recursive: true });
@@ -459,8 +458,8 @@ describe("AgentSessionRuntime characterization", () => {
await runtime.switchSession(otherSessionFile); await runtime.switchSession(otherSessionFile);
expect(realpathSync(process.cwd())).toBe(realpathSync(secondDir));
expect(realpathSync(runtime.session.sessionManager.getCwd())).toBe(realpathSync(secondDir)); expect(realpathSync(runtime.session.sessionManager.getCwd())).toBe(realpathSync(secondDir));
expect(realpathSync(runtime.cwd)).toBe(realpathSync(secondDir));
}); });
it("restores model and thinking state from the destination session", async () => { it("restores model and thinking state from the destination session", async () => {

View File

@@ -0,0 +1,94 @@
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../../../src/core/resource-loader.js";
import { createAgentSession } from "../../../src/core/sdk.js";
import { SessionManager } from "../../../src/core/session-manager.js";
import { SettingsManager } from "../../../src/core/settings-manager.js";
describe("regression #2835: tool allowlists filter extension tools", () => {
let tempDir: string;
let agentDir: string;
beforeEach(() => {
tempDir = join(tmpdir(), `pi-tools-filter-${Date.now()}-${Math.random().toString(36).slice(2)}`);
agentDir = join(tempDir, "agent");
mkdirSync(agentDir, { recursive: true });
});
afterEach(() => {
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
});
async function createSession(allowedToolNames?: string[]) {
const settingsManager = SettingsManager.create(tempDir, agentDir);
const sessionManager = SessionManager.inMemory(tempDir);
const resourceLoader = new DefaultResourceLoader({
cwd: tempDir,
agentDir,
settingsManager,
extensionFactories: [
(pi) => {
pi.on("session_start", () => {
pi.registerTool({
name: "dynamic_tool",
label: "Dynamic Tool",
description: "Tool registered from session_start",
promptSnippet: "Run dynamic test behavior",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "ok" }],
details: {},
}),
});
});
},
],
});
await resourceLoader.reload();
const { session } = await createAgentSession({
cwd: tempDir,
agentDir,
model: getModel("anthropic", "claude-sonnet-4-5")!,
settingsManager,
sessionManager,
resourceLoader,
tools: allowedToolNames,
});
await session.bindExtensions({});
return session;
}
it("allows only explicitly listed built-in and extension tools", async () => {
const session = await createSession(["read", "dynamic_tool"]);
expect(
session
.getAllTools()
.map((tool) => tool.name)
.sort(),
).toEqual(["dynamic_tool", "read"]);
expect(session.getActiveToolNames().sort()).toEqual(["dynamic_tool", "read"]);
expect(session.systemPrompt).toContain("- read: Read file contents");
expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
expect(session.systemPrompt).not.toContain("- bash:");
expect(session.systemPrompt).not.toContain("- edit:");
session.dispose();
});
it("disables all tools when the allowlist is empty", async () => {
const session = await createSession([]);
expect(session.getAllTools()).toEqual([]);
expect(session.getActiveToolNames()).toEqual([]);
expect(session.systemPrompt).toContain("Available tools:\n(none)");
expect(session.systemPrompt).not.toContain("dynamic_tool");
session.dispose();
});
});