f5273eee6f
- Decouple the per-tool approval gate from extension presence. ExtensionRunner
and the ExtensionToolWrapper that hosts the gate are now constructed
unconditionally in createAgentSession. Previously the runner was only built
when extensionsResult.extensions.length > 0, so the entire approval system
silently disappeared for sessions with no extensions loaded — any
tools.approvalMode: prompt|custom setting was a no-op without feedback.
Today this hole was masked by createAutoresearchExtension always being
pushed inline; the unconditional construction makes the safety invariant
explicit, and a new regression test in approval-mode.test.ts pins it.
- Extend CRITICAL_BASH_PATTERNS to cover remote-fetch-then-execute shapes
that the original `bash <(curl …)` regex missed:
- `source <(curl …)` / `. <(curl …)` (anchored at command boundary so
`find . -name foo` doesn't false-positive)
- `eval "$(curl …)"` / `eval $(curl …)` / `eval `curl …``
Also adds `chmod -R` symbolic-mode forms (`u+x`, `u+rwx,o+w …`) targeting
filesystem root, and `tee` / `tee -a` writes to /etc/{passwd,shadow,sudoers}
(the standard way to write root-owned files without redirect). Benign
forms (`source ./local.sh`, `chmod -R u+x ./build`, `tee /var/log/app.log`,
`eval "$VAR"`) are pinned negative in the test suite.
- Extend formatApprovalPrompt with payload previews for the destructive tools
that previously rendered as bare `Allow tool: <name>`: eval (language +
first cell's code), task (agent + first task's id + assignment), ast_edit
(first op's pattern / replacement / paths), browser (action + tab + url +
code), and write content (alongside path). For `task` in particular this
closes the gap that docs/approval-mode.md's "parent's approval covers the
subagent" claim was waving at — the prompt now actually shows what's being
delegated.
- Tighten isMcpToolName: drop the fallback `|| toolName.includes("__")` so
an extension tool legally named `my__feature` or `pkg__util__do` is no
longer falsely labelled `Origin: MCP server tool` in the approval prompt.
Strict `mcp__` prefix only.
- Revert the cargo-cult `{ autoApprove: true } as AgentToolContext` insertions
in agent-session-python-cleanup.test.ts and sdk-move-cwd.test.ts. The tests
create sessions without passing settings, so the wrapper falls through to
approvalMode "auto" automatically; the explicit flag was unnecessary and
the `as AgentToolContext` cast hid that autoApprove lives on
CustomToolContext, not AgentToolContext.
- Document in commands/launch.ts the dual --auto-approve declaration (oclif
Flags for --help, manual parseArgs for runtime) so a future rename catches
both call sites.
- Promote the subagent caveat in docs/approval-mode.md to a callout near the
top: anything `task` is asked to do runs unattended once the parent task
call is approved.
Verification:
- bun test packages/coding-agent/test/tools/approval.test.ts → 75 pass / 0 fail
(was 57; +18 cases covering new remote-exec patterns, chmod symbolic, tee
/etc, isMcp negative, and eval/task/ast_edit/browser/write payload previews)
- bun test packages/coding-agent/test/tools/approval-mode.test.ts → 7 pass /
0 fail (was 7; +1 case asserting extensionRunner is always constructed)
- bun tsc --noEmit -p packages/coding-agent → clean
- bun x biome check . → clean
- Windows EBUSY tempdir-cleanup noise in agent-session-python-cleanup and
sdk-move-cwd is pre-existing on this branch (already documented in the
PR body) and absent on Linux CI.
73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
import { afterEach, describe, expect, it } from "bun:test";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { getBundledModel } from "@oh-my-pi/pi-ai";
|
|
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk";
|
|
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
|
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
|
|
function textContent(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
return (
|
|
result.content
|
|
?.filter(
|
|
(block): block is { type: "text"; text: string } => block.type === "text" && typeof block.text === "string",
|
|
)
|
|
.map(block => block.text)
|
|
.join("\n") ?? ""
|
|
);
|
|
}
|
|
|
|
describe("createAgentSession cwd after /move", () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const tempDir of tempDirs.splice(0)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("runs tools from the moved session directory", async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `pi-sdk-move-cwd-${Snowflake.next()}-`));
|
|
tempDirs.push(tempDir);
|
|
const cwdA = path.join(tempDir, "cwd-a");
|
|
const cwdB = path.join(tempDir, "cwd-b");
|
|
fs.mkdirSync(cwdA, { recursive: true });
|
|
fs.mkdirSync(cwdB, { recursive: true });
|
|
|
|
const sessionManager = SessionManager.create(cwdA, path.join(tempDir, "sessions"));
|
|
const { session } = await createAgentSession({
|
|
cwd: cwdA,
|
|
agentDir: tempDir,
|
|
sessionManager,
|
|
settings: Settings.isolated({
|
|
"async.enabled": false,
|
|
"bash.autoBackground.enabled": false,
|
|
"bashInterceptor.enabled": false,
|
|
}),
|
|
model: getBundledModel("openai", "gpt-4o-mini"),
|
|
disableExtensionDiscovery: true,
|
|
skills: [],
|
|
contextFiles: [],
|
|
promptTemplates: [],
|
|
slashCommands: [],
|
|
enableMCP: false,
|
|
enableLsp: false,
|
|
toolNames: ["bash"],
|
|
});
|
|
|
|
try {
|
|
await sessionManager.moveTo(cwdB);
|
|
|
|
const bashTool = session.getToolByName("bash");
|
|
if (!bashTool) throw new Error("Expected bash tool");
|
|
const result = await bashTool.execute("pwd-after-move", { command: "pwd" });
|
|
|
|
expect(textContent(result)).toContain(cwdB);
|
|
} finally {
|
|
await session.dispose();
|
|
}
|
|
});
|
|
});
|