This pull request adds a feature that allows users to configure auto-approved commands via a `safeCommands` array in the configuration file. ## Related Issue #380 ## Changes - Added loading and validation of the `safeCommands` array in `src/utils/config.ts` - Implemented auto-approval logic for commands matching `safeCommands` prefixes in `src/approvals.ts` - Added test cases in `src/tests/approvals.test.ts` to verify `safeCommands` behavior - Updated documentation with examples and explanations of the configuration
This commit is contained in:
@@ -288,6 +288,9 @@ Codex looks for config files in **`~/.codex/`**.
|
|||||||
model: o4-mini # Default model
|
model: o4-mini # Default model
|
||||||
fullAutoErrorMode: ask-user # or ignore-and-continue
|
fullAutoErrorMode: ask-user # or ignore-and-continue
|
||||||
notify: true # Enable desktop notifications for responses
|
notify: true # Enable desktop notifications for responses
|
||||||
|
safeCommands:
|
||||||
|
- npm test # Automatically approve npm test
|
||||||
|
- yarn lint # Automatically approve yarn lint
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also define custom instructions:
|
You can also define custom instructions:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
identify_files_added,
|
identify_files_added,
|
||||||
identify_files_needed,
|
identify_files_needed,
|
||||||
} from "./utils/agent/apply-patch";
|
} from "./utils/agent/apply-patch";
|
||||||
|
import { loadConfig } from "./utils/config";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { parse } from "shell-quote";
|
import { parse } from "shell-quote";
|
||||||
|
|
||||||
@@ -296,6 +297,24 @@ export function isSafeCommand(
|
|||||||
): SafeCommandReason | null {
|
): SafeCommandReason | null {
|
||||||
const [cmd0, cmd1, cmd2, cmd3] = command;
|
const [cmd0, cmd1, cmd2, cmd3] = command;
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
if (config.safeCommands && Array.isArray(config.safeCommands)) {
|
||||||
|
for (const safe of config.safeCommands) {
|
||||||
|
// safe: "npm test" → ["npm", "test"]
|
||||||
|
const safeArr = typeof safe === "string" ? safe.trim().split(/\s+/) : [];
|
||||||
|
if (
|
||||||
|
safeArr.length > 0 &&
|
||||||
|
safeArr.length <= command.length &&
|
||||||
|
safeArr.every((v, i) => v === command[i])
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
reason: "User-defined safe command",
|
||||||
|
group: "User config",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (cmd0) {
|
switch (cmd0) {
|
||||||
case "cd":
|
case "cd":
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export type StoredConfig = {
|
|||||||
saveHistory?: boolean;
|
saveHistory?: boolean;
|
||||||
sensitivePatterns?: Array<string>;
|
sensitivePatterns?: Array<string>;
|
||||||
};
|
};
|
||||||
|
/** User-defined safe commands */
|
||||||
|
safeCommands?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Minimal config written on first run. An *empty* model string ensures that
|
// Minimal config written on first run. An *empty* model string ensures that
|
||||||
@@ -87,6 +89,8 @@ export type AppConfig = {
|
|||||||
saveHistory: boolean;
|
saveHistory: boolean;
|
||||||
sensitivePatterns: Array<string>;
|
sensitivePatterns: Array<string>;
|
||||||
};
|
};
|
||||||
|
/** User-defined safe commands */
|
||||||
|
safeCommands?: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -271,6 +275,7 @@ export const loadConfig = (
|
|||||||
: DEFAULT_AGENTIC_MODEL),
|
: DEFAULT_AGENTIC_MODEL),
|
||||||
instructions: combinedInstructions,
|
instructions: combinedInstructions,
|
||||||
notify: storedConfig.notify === true,
|
notify: storedConfig.notify === true,
|
||||||
|
safeCommands: storedConfig.safeCommands ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -348,6 +353,13 @@ export const loadConfig = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load user-defined safe commands
|
||||||
|
if (Array.isArray(storedConfig.safeCommands)) {
|
||||||
|
config.safeCommands = storedConfig.safeCommands.map(String);
|
||||||
|
} else {
|
||||||
|
config.safeCommands = [];
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -389,6 +401,10 @@ export const saveConfig = (
|
|||||||
sensitivePatterns: config.history.sensitivePatterns,
|
sensitivePatterns: config.history.sensitivePatterns,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Save: User-defined safe commands
|
||||||
|
if (config.safeCommands && config.safeCommands.length > 0) {
|
||||||
|
configToSave.safeCommands = config.safeCommands;
|
||||||
|
}
|
||||||
|
|
||||||
if (ext === ".yaml" || ext === ".yml") {
|
if (ext === ".yaml" || ext === ".yml") {
|
||||||
writeFileSync(targetPath, dumpYaml(configToSave), "utf-8");
|
writeFileSync(targetPath, dumpYaml(configToSave), "utf-8");
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import type { SafetyAssessment } from "../src/approvals";
|
import type { SafetyAssessment } from "../src/approvals";
|
||||||
|
|
||||||
import { canAutoApprove } from "../src/approvals";
|
import { canAutoApprove } from "../src/approvals";
|
||||||
import { describe, test, expect } from "vitest";
|
import { describe, test, expect, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("../src/utils/config", () => ({
|
||||||
|
loadConfig: () => ({
|
||||||
|
safeCommands: ["npm test", "sl"],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("canAutoApprove()", () => {
|
describe("canAutoApprove()", () => {
|
||||||
const env = {
|
const env = {
|
||||||
@@ -89,4 +95,27 @@ describe("canAutoApprove()", () => {
|
|||||||
|
|
||||||
expect(check(["cargo", "build"])).toEqual({ type: "ask-user" });
|
expect(check(["cargo", "build"])).toEqual({ type: "ask-user" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("commands in safeCommands config should be safe", async () => {
|
||||||
|
expect(check(["npm", "test"])).toEqual({
|
||||||
|
type: "auto-approve",
|
||||||
|
reason: "User-defined safe command",
|
||||||
|
group: "User config",
|
||||||
|
runInSandbox: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(check(["sl"])).toEqual({
|
||||||
|
type: "auto-approve",
|
||||||
|
reason: "User-defined safe command",
|
||||||
|
group: "User config",
|
||||||
|
runInSandbox: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(check(["npm", "test", "--watch"])).toEqual({
|
||||||
|
type: "auto-approve",
|
||||||
|
reason: "User-defined safe command",
|
||||||
|
group: "User config",
|
||||||
|
runInSandbox: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user