Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions apps/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
type Effort,
} from '@deepcode/core';
import { execFile } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { readFile, stat } from 'node:fs/promises';
import { isAbsolute, resolve } from 'node:path';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);
Expand Down Expand Up @@ -406,10 +407,38 @@ export const ConfigCommand: SlashCommand = {

export const AddDirCommand: SlashCommand = {
name: '/add-dir',
description: 'Add an additional allowed directory (M3 enforced; M2 records intent).',
run(args) {
if (args.length === 0) return ['Usage: /add-dir <path>'];
return [`Recorded ${args[0]} as additional allowed directory (effective in M3).`];
description: 'Add a directory the agent may write to (persists; sandbox-enforced).',
async run(args, ctx) {
const current = ctx.settings.permissions?.additionalDirectories ?? [];
if (args.length === 0) {
return current.length > 0
? ['Additional writable directories:', ...current.map((d) => ` ${d}`)]
: ['No additional directories yet. Usage: /add-dir <path>'];
}
if (!ctx.userSettingsPath) return ['(/add-dir is unavailable here.)'];
const dir = isAbsolute(args[0]!) ? args[0]! : resolve(ctx.cwd, args[0]!);
try {
if (!(await stat(dir)).isDirectory()) return [`Not a directory: ${dir}`];
} catch {
return [`No such directory: ${dir}`];
}
let onDisk: Record<string, unknown> = {};
try {
onDisk = JSON.parse(await readFile(ctx.userSettingsPath, 'utf8')) as Record<string, unknown>;
} catch {
/* missing → start fresh */
}
const perms = (onDisk.permissions ?? {}) as Record<string, unknown>;
const existing = Array.isArray(perms.additionalDirectories)
? (perms.additionalDirectories as string[])
: [];
perms.additionalDirectories = [...new Set([...existing, dir])];
onDisk.permissions = perms;
await writeSettings(ctx.userSettingsPath, onDisk as DeepCodeSettings);
return [
`Added ${dir} as an additional writable directory.`,
'The sandboxed Bash tool can write there (restart for it to take effect in a new session).',
];
},
};

Expand Down
6 changes: 5 additions & 1 deletion apps/cli/src/headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
loadMemory,
loadOutputStyles,
loadSettings,
withAdditionalWritableDirs,
loadSkills,
makeSkillTool,
resolveCredentials,
Expand Down Expand Up @@ -289,7 +290,10 @@ export async function runHeadless(opts: HeadlessOpts): Promise<number> {
pluginDirs: pluginContrib.dirs,
autoCompact: { contextWindow: contextWindowFor(model), threshold: 0.8 },
autoMode: settings.autoMode,
sandboxConfig: settings.sandbox,
sandboxConfig: withAdditionalWritableDirs(
settings.sandbox,
settings.permissions?.additionalDirectories,
),
// In headless mode there's no human to ask: auto-deny anything that
// would normally need approval. Users wanting auto-yes should pass
// --mode dontAsk or --mode bypassPermissions (gated by trust).
Expand Down
28 changes: 28 additions & 0 deletions apps/cli/src/parity-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,31 @@ describe('/config set', () => {
expect(out.join('\n')).toMatch(/Usage: \/config set/);
});
});

describe('/add-dir', () => {
it('persists a validated directory to permissions.additionalDirectories', async () => {
const home = await tmpHome(); // a real, existing directory
const path = join(home, 'settings.json');
const out = await reg.match('/add-dir')!.cmd.run([home], ctx({ userSettingsPath: path }));
expect(out.join('\n')).toMatch(/Added .* writable directory/i);
const written = JSON.parse(await readFile(path, 'utf8')) as {
permissions?: { additionalDirectories?: string[] };
};
expect(written.permissions?.additionalDirectories).toContain(home);
});

it('rejects a non-existent directory', async () => {
const home = await tmpHome();
const out = await reg
.match('/add-dir')!
.cmd.run([join(home, 'nope')], ctx({ userSettingsPath: join(home, 'settings.json') }));
expect(out.join('\n')).toMatch(/no such directory/i);
});

it('lists current directories with no args', async () => {
const out = await reg
.match('/add-dir')!
.cmd.run([], ctx({ settings: { permissions: { additionalDirectories: ['/x/y'] } } }));
expect(out.join('\n')).toContain('/x/y');
});
});
6 changes: 5 additions & 1 deletion apps/cli/src/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
runAgent,
settingsPaths,
wirePlugins,
withAdditionalWritableDirs,
collectPluginContributions,
type Effort,
type McpClientHandle,
Expand Down Expand Up @@ -616,7 +617,10 @@ export async function startRepl(opts: ReplOpts): Promise<number> {
pluginDirs: pluginContrib.dirs,
autoCompact: { contextWindow: contextWindowFor(ctx.model), threshold: 0.8 },
autoMode: settings.autoMode,
sandboxConfig: settings.sandbox,
sandboxConfig: withAdditionalWritableDirs(
settings.sandbox,
settings.permissions?.additionalDirectories,
),
approval: async (toolName, _input, verdict) => {
output.write(`\n ⏸ Approve ${toolName}? Reason: ${verdict.reason}\n`);
const answer = (await rl.question(' [y]es / [n]o / [a]lways: ')).trim().toLowerCase();
Expand Down
Loading
Loading