mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
fix(runner): preserve Claude user settings trust in HOME (#17826)
This commit is contained in:
1 parent
baec3abd67
commit
e61ee4b005
3 files changed
+61
-3
No files matched your search
@@ -818,7 +818,10 @@ rules and hooks apply. Native Codex tools can read and modify the execution home
|
||||
(read-only for planning); credentials in that home are not isolated from tools.
|
||||
Native Codex separately denies Runner-owned protocol state; ACPX Claude retains
|
||||
its existing provider trust boundary. Execution is unattended: native Codex uses `never`, and
|
||||
ACPX rejects requests for approval. The initial ACPX mode requires the Runner
|
||||
ACPX rejects requests for approval. When Claude starts in its home directory,
|
||||
its native user settings retain user trust even if the SDK also reports that
|
||||
same file as project settings. Other project settings keep the native trust filter.
|
||||
The initial ACPX mode requires the Runner
|
||||
setting `acpxPermissionMode: "approve-all"` (its default); other Runner modes are
|
||||
unqualified here and fail before launch. Local permission rules still apply.
|
||||
Preconfigured ACPX read-only/planning tasks also fail before provider launch
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const runnerPackage = JSON.parse(
|
||||
@@ -217,3 +220,39 @@ test("the Claude patch removes ambient project and local configuration", () => {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Claude preserves user permission modes when its working directory is home", async (t) => {
|
||||
const packagePath = createRequire(import.meta.url).resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
||||
const settingsModule = pathToFileURL(resolve(dirname(packagePath), "dist/settings.js")).href;
|
||||
const root = await mkdtemp(join(tmpdir(), "claude-settings-origin-"));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const home = join(root, "home");
|
||||
const project = join(root, "project");
|
||||
await mkdir(join(home, ".claude"), { recursive: true });
|
||||
await mkdir(join(project, ".claude"), { recursive: true });
|
||||
const userFile = join(home, ".claude/settings.json");
|
||||
const env = { ...process.env, HOME: home };
|
||||
delete env.CLAUDE_CONFIG_DIR;
|
||||
delete env.PAPERCLIP_ACPX_ISOLATED_CONTEXT;
|
||||
const inspect = (cwd) => JSON.parse(execFileSync(process.execPath, ["--input-type=module", "-e", `
|
||||
import { SettingsManager } from ${JSON.stringify(settingsModule)};
|
||||
const manager = new SettingsManager(${JSON.stringify(cwd)});
|
||||
try {
|
||||
await manager.initialize();
|
||||
console.log(JSON.stringify(manager.getSettings()));
|
||||
} finally { manager.dispose(); }
|
||||
`], { env, encoding: "utf8", timeout: 10_000 }));
|
||||
for (const defaultMode of ["bypassPermissions", "acceptEdits", "auto", "plan", "default"]) {
|
||||
const content = JSON.stringify({ permissions: { defaultMode, deny: ["Read(./private)"] } });
|
||||
await writeFile(userFile, content);
|
||||
assert.deepEqual(inspect(home).permissions, { defaultMode, deny: ["Read(./private)"] });
|
||||
assert.equal(await readFile(userFile, "utf8"), content, "discovery does not rewrite the profile");
|
||||
}
|
||||
await writeFile(userFile, JSON.stringify({ permissions: { defaultMode: "bypassPermissions" } }));
|
||||
for (const defaultMode of ["bypassPermissions", "acceptEdits", "auto"]) {
|
||||
await writeFile(join(project, ".claude/settings.json"), JSON.stringify({ permissions: { defaultMode } }));
|
||||
assert.equal(inspect(project).permissions?.defaultMode, undefined, "real project escalation stays filtered");
|
||||
}
|
||||
await writeFile(join(home, ".claude/settings.local.json"), JSON.stringify({ permissions: { defaultMode: "plan" } }));
|
||||
assert.equal(inspect(home).permissions.defaultMode, "plan", "local restrictions still win");
|
||||
});
|
||||
@@ -64,3 +64,19 @@ index 0214a509eb5e7b777a69e4e34637941c275d27b1..403be02df66e25eee29e7d5bef146270
|
||||
// Forward MCP elicitation requests onto ACP elicitation. Only attached
|
||||
// when the client advertised support, so non-supporting clients keep the
|
||||
// SDK's default (auto-decline) behavior. (AskUserQuestion is handled in
|
||||
diff --git a/dist/settings.js b/dist/settings.js
|
||||
--- a/dist/settings.js
|
||||
+++ b/dist/settings.js
|
||||
@@ -88,6 +88,12 @@
|
||||
async loadAllSettings() {
|
||||
try {
|
||||
const resolved = await resolveSettings({ cwd: this.cwd });
|
||||
+ const userPath = resolved.sources.find((entry) => entry.source === "user")?.path;
|
||||
+ for (const entry of resolved.sources) {
|
||||
+ if (userPath && entry.source === "project" && entry.path === userPath) {
|
||||
+ entry.source = "user";
|
||||
+ }
|
||||
+ }
|
||||
this.effective = filterEscalatingDefaultMode(resolved);
|
||||
}
|
||||
catch (error) {
|
||||
Reference in new issue
Block a user