Skip to content
19 changes: 19 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@

export const DEFAULT_DESTRUCTIVE_COMMAND_GUARD_ENABLED = false

/**
* Whether workspace-scoped command-list layers (`zoo-code.allowedCommands` /
* `zoo-code.deniedCommands` values set in workspace settings) participate in
* command auto-approval at all. Opt-in: while off, workspace-layer entries
* contribute nothing to enforcement or display. The manifest declares the key
* with `scope: "application"`, so only user settings can change it; this
* constant is the single default shared by every consumer reading the
* setting.
*/
export const DEFAULT_WORKSPACE_COMMAND_LISTS_ENABLED = false

Check warning on line 66 in packages/types/src/global-settings.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/global-settings.ts:66: Survived BooleanLiteral mutant (replacement: true). See the job summary for the complete list and resolution guidance.

/**
* Whether commands that are not explicitly auto-approved are automatically
* denied (with a structured reason sent to the model) instead of prompting the
Expand Down Expand Up @@ -180,6 +191,14 @@
followupAutoApproveTimeoutMs: z.number().optional(),
allowedCommands: z.array(z.string()).optional(),
deniedCommands: z.array(z.string()).optional(),
/**
* Gate for the workspace-scoped command-list layers. While false (the
* default), workspace-set `allowedCommands`/`deniedCommands` entries
* contribute nothing; when true they are composed under the global layer,
* which outranks them wherever it matches a command.
* @default false
*/
workspaceCommandListsEnabled: z.boolean().optional(),
commandExecutionTimeout: z.number().optional(),
commandTimeoutAllowlist: z.array(z.string()).optional(),
preventCompletionWithOpenTodos: z.boolean().optional(),
Expand Down
4 changes: 1 addition & 3 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ export type ExtensionState = Pick<
| "followupAutoApproveTimeoutMs"
| "allowedCommands"
| "deniedCommands"
| "workspaceCommandListsEnabled"
| "allowedMaxRequests"
| "allowedMaxCost"
| "ttsEnabled"
Expand Down Expand Up @@ -662,9 +663,7 @@ export interface WebviewMessage {
| "getDismissedUpsells"
| "openMarkdownPreview"
| "updateSettings"
| "allowedCommands"
| "getTaskWithAggregatedCosts"
| "deniedCommands"
| "openDebugApiHistory"
| "openDebugUiHistory"
| "downloadErrorDiagnostics"
Expand Down Expand Up @@ -722,7 +721,6 @@ export interface WebviewMessage {
stepIndex?: number
isLaunchAction?: boolean
forceShow?: boolean
commands?: string[]
audioType?: AudioType
serverName?: string
toolName?: string
Expand Down
79 changes: 78 additions & 1 deletion src/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vi.mock("vscode", () => ({
registerTextDocumentContentProvider: vi.fn(),
getConfiguration: vi.fn().mockReturnValue({
get: vi.fn().mockReturnValue([]),
inspect: vi.fn(),
}),
createFileSystemWatcher: vi.fn().mockReturnValue({
onDidCreate: vi.fn(),
Expand Down Expand Up @@ -228,15 +229,17 @@ vi.mock("../api/providers/fetchers/modelCache", () => ({

describe("extension.ts", () => {
let mockContext: vscode.ExtensionContext
let globalStateValues: Map<string, unknown>
let settingsUpdatedHandler: ((data: Record<string, never>) => void | Promise<void>) | undefined

beforeEach(() => {
vi.clearAllMocks()

globalStateValues = new Map<string, unknown>()
mockContext = {
extensionPath: "/test/path",
globalState: {
get: vi.fn().mockReturnValue(undefined),
get: vi.fn((key: string) => globalStateValues.get(key)),
update: vi.fn(),
},
subscriptions: [],
Expand Down Expand Up @@ -536,6 +539,80 @@ describe("extension.ts", () => {
expect(dotenv.config).toHaveBeenCalledTimes(1)
})

describe("allowedCommands activation seeding", () => {
function setConfigScope(
vscode: typeof import("vscode"),
scope: { globalValue?: string[]; workspaceValue?: string[] },
) {
const { globalValue, workspaceValue } = scope
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
// Key-scoped so proxy/other activation reads still see no value.
get: vi.fn((key: string) =>
key === "allowedCommands" ? [...(globalValue ?? []), ...(workspaceValue ?? [])] : undefined,
),
inspect: vi.fn().mockReturnValue({
key: "allowedCommands",
// Real inspect() always reports the declared package default;
// including it here lets the workspace-override test prove the
// default stays unseeded beside an override, not just absent.
defaultValue: ["git log", "git diff", "git show"],
globalValue,
workspaceValue,
}),
update: vi.fn(),
// The double scripts key-scoped get() and per-scope inspect()
// shapes the shared helper cannot express without obscuring them.
} as unknown as vscode.WorkspaceConfiguration)
}

beforeEach(() => {
vi.resetModules()
})

afterEach(async () => {
const vscode = await import("vscode")
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
get: vi.fn().mockReturnValue([]),
inspect: vi.fn(),
// Restores the neutral default after the scope fixtures above;
// update is intentionally absent because no test runs after this.
} as unknown as vscode.WorkspaceConfiguration)
})

test("does not seed global state from a workspace-scoped override", async () => {
const vscode = await import("vscode")
setConfigScope(vscode, { workspaceValue: ["docker"] })

const { activate } = await import("../extension")
await activate(mockContext)

expect(mockContext.globalState.update).not.toHaveBeenCalledWith("allowedCommands", expect.anything())
expect(globalStateValues.get("allowedCommands")).toBeUndefined()
})

test("seeds global state from the user-scoped value when it is empty", async () => {
const vscode = await import("vscode")
setConfigScope(vscode, { globalValue: ["git"] })

const { activate } = await import("../extension")
await activate(mockContext)

expect(mockContext.globalState.update).toHaveBeenCalledWith("allowedCommands", ["git"])
})

test("does not overwrite global state that is already populated", async () => {
const vscode = await import("vscode")
setConfigScope(vscode, { globalValue: ["git"] })
globalStateValues.set("allowedCommands", ["npm"])

const { activate } = await import("../extension")
await activate(mockContext)

expect(mockContext.globalState.update).not.toHaveBeenCalledWith("allowedCommands", expect.anything())
expect(globalStateValues.get("allowedCommands")).toEqual(["npm"])
})
})

describe("cloud organization settings handling", () => {
beforeEach(() => {
vi.resetModules()
Expand Down
Loading
Loading