feat(DIS-152): mapped project paths (host:virtual) for agents #66

Open
dev wants to merge 7 commits from refinement/mapped-project-paths into main
2 changed files with 23 additions and 7 deletions
Showing only changes of commit cbd2df66fb - Show all commits

View file

@ -172,23 +172,39 @@ export function createClaudeConfig(
// Create directory structure // Create directory structure
fs.mkdirSync(commandsDir, { recursive: true }); fs.mkdirSync(commandsDir, { recursive: true });
// Base allow list — always includes workspace-local operations const mappings = pathMappings ?? [];
const allow: string[] = ["Read", "Write(./**)", "Edit(./**)", "Bash(git diff *)"];
// Add explicit read/write/edit permissions for each mapped host path // Build allow list — always workspace-local, plus explicit mapped host paths.
for (const mapping of pathMappings ?? []) { // When path mappings exist we use explicit Read(./**) instead of a blanket
// Read, because a blanket Read combined with deny(../**) would silently block
// the mapped absolute paths (deny always wins over allow in Claude Code).
const allow: string[] = [
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Bash(git diff *)",
];
for (const mapping of mappings) {
const normalized = mapping.host.replace(/\\/g, "/"); const normalized = mapping.host.replace(/\\/g, "/");
allow.push(`Read(${normalized}/**)`); allow.push(`Read(${normalized}/**)`);
allow.push(`Write(${normalized}/**)`); allow.push(`Write(${normalized}/**)`);
allow.push(`Edit(${normalized}/**)`); allow.push(`Edit(${normalized}/**)`);
} }
// Deny list — the ../** rules are only added when there are NO path mappings.
// With mappings, deny(../**) would override the explicit host-path allows.
// Without mappings, ../** acts as a catch-all traversal guard.
const denyTraversal = mappings.length === 0
? ["Read(../**)", "Write(../**)", "Edit(../**)"]
: [];
const settings = { const settings = {
permissions: { permissions: {
allow, allow,
ask: ["Bash(git push *)"], ask: ["Bash(git push *)"],
deny: [ deny: [
"Read(../**)", "Write(../**)", "Edit(../**)", ...denyTraversal,
"Read(./.env)", "WebFetch", "Read(./.env)", "WebFetch",
"Bash(rm -rf *)", "Bash(curl * | sh)", "Bash(rm -rf *)", "Bash(curl * | sh)",
"Bash(ssh *)", "Bash(scp *)", "Bash(ssh *)", "Bash(scp *)",

View file

@ -30,11 +30,11 @@ describe("identity-templates", () => {
expect(settings.permissions.deny).toContain("Read(./.env)"); expect(settings.permissions.deny).toContain("Read(./.env)");
}); });
it("settings.json permissions.allow contains Read", () => { it("settings.json permissions.allow contains Read(./**)", () => {
setupAgentWorkspace(tmpDir, "test-agent", "A test agent", "chan-123"); setupAgentWorkspace(tmpDir, "test-agent", "A test agent", "chan-123");
const settingsPath = path.join(tmpDir, ".claude", "settings.json"); const settingsPath = path.join(tmpDir, ".claude", "settings.json");
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
expect(settings.permissions.allow).toContain("Read"); expect(settings.permissions.allow).toContain("Read(./**)");
}); });
it("CLAUDE.md contains Prompt-Injection section", () => { it("CLAUDE.md contains Prompt-Injection section", () => {