Skip to content

Commit 169ea63

Browse files
Steve SandersonCopilot
authored andcommitted
Hardcode toolFilterMode=denyPrecedence on SDK; drop public toggle
The SDK no longer exposes 'toolFilterMode'. Every session.create / session.resume request now sends toolFilterMode: 'denyPrecedence' unconditionally, so SDK callers always get composable include+exclude semantics (a tool is enabled when it matches availableTools — or availableTools is unset — AND it does not match excludedTools). Allowlist-precedence remains available on the runtime side as a CLI-only concession to legacy behavior; SDK consumers don't need it and the toggle was just extra surface area. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d166a45 commit 169ea63

3 files changed

Lines changed: 17 additions & 57 deletions

File tree

nodejs/src/client.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -880,19 +880,23 @@ export class CopilotClient {
880880
/**
881881
* Normalizes session-level tool filter options. Converts {@link ToolSet}
882882
* instances to plain string arrays, rejects misuse (bare `"*"`) and the
883-
* missing-availableTools case in `mode = "empty"`, and applies the
884-
* mode-aware default for `toolFilterMode`.
883+
* missing-availableTools case in `mode = "empty"`.
884+
*
885+
* The SDK always sends `toolFilterMode: "denyPrecedence"` so callers can
886+
* compose include + exclude lists naturally (e.g. "everything matching X
887+
* except Y") regardless of mode. Allowlist-precedence is intentionally not
888+
* exposed — it's available on the runtime side as a CLI-only concession to
889+
* legacy behavior, but SDK consumers always get the composable semantics.
885890
*
886891
* @internal
887892
*/
888893
private resolveToolFilterOptions(config: {
889894
availableTools?: string[] | ToolSet;
890895
excludedTools?: string[] | ToolSet;
891-
toolFilterMode?: "allowPrecedence" | "denyPrecedence";
892896
}): {
893897
availableTools: string[] | undefined;
894898
excludedTools: string[] | undefined;
895-
toolFilterMode: "allowPrecedence" | "denyPrecedence" | undefined;
899+
toolFilterMode: "denyPrecedence";
896900
} {
897901
const availableTools = toolFilterListToArray(config.availableTools);
898902
const excludedTools = toolFilterListToArray(config.excludedTools);
@@ -910,13 +914,7 @@ export class CopilotClient {
910914
}
911915
}
912916

913-
// Empty mode flips the default to deny-precedence so apps can compose
914-
// include + exclude lists naturally (e.g. "everything matching X
915-
// except Y"). Callers can still override this explicitly.
916-
const toolFilterMode =
917-
config.toolFilterMode ?? (this.options.mode === "empty" ? "denyPrecedence" : undefined);
918-
919-
return { availableTools, excludedTools, toolFilterMode };
917+
return { availableTools, excludedTools, toolFilterMode: "denyPrecedence" };
920918
}
921919

922920
async createSession(config: SessionConfig): Promise<CopilotSession> {

nodejs/src/types.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,36 +1558,21 @@ export interface SessionConfigBase {
15581558
* name form (exact match across any source). Build this list with
15591559
* {@link ToolSet} for type safety and readable intent.
15601560
*
1561-
* Interacts with {@link excludedTools} per
1562-
* {@link SessionConfigBase.toolFilterMode}.
1561+
* Composes with {@link excludedTools}: a tool is enabled when it matches
1562+
* `availableTools` (or `availableTools` is unset) AND it does not match
1563+
* `excludedTools`. This lets you express "everything matching X except Y".
15631564
*/
15641565
availableTools?: string[] | ToolSet;
15651566

15661567
/**
15671568
* List of tool names to disable. Supports the same pattern syntax as
15681569
* {@link availableTools}.
15691570
*
1570-
* Interacts with {@link availableTools} per
1571-
* {@link SessionConfigBase.toolFilterMode}: by default (`"allowPrecedence"`),
1572-
* `excludedTools` is ignored whenever `availableTools` is set; under
1573-
* `"denyPrecedence"` it always takes effect.
1571+
* Always takes precedence over {@link availableTools}: a tool listed here
1572+
* is disabled even if it also matches `availableTools`.
15741573
*/
15751574
excludedTools?: string[] | ToolSet;
15761575

1577-
/**
1578-
* Controls how {@link availableTools} and {@link excludedTools} combine
1579-
* when both are set.
1580-
*
1581-
* - `"allowPrecedence"` (default): If `availableTools` is set, `excludedTools`
1582-
* is ignored. Preserves the historical CLI behavior.
1583-
* - `"denyPrecedence"`: `excludedTools` always wins. Enables "everything
1584-
* matching X, except Y" patterns.
1585-
*
1586-
* When the client is in `Mode = "empty"`, the SDK defaults this to
1587-
* `"denyPrecedence"` so apps can subtract from `BuiltInTools.Isolated` etc.
1588-
*/
1589-
toolFilterMode?: "allowPrecedence" | "denyPrecedence";
1590-
15911576
/**
15921577
* Custom provider configuration (BYOK - Bring Your Own Key).
15931578
* When specified, uses the provided API endpoint instead of the Copilot API.

nodejs/test/toolSet.test.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,17 @@ describe("Tool filter wiring", () => {
174174
).rejects.toThrowError(/bare wildcard/);
175175
});
176176

177-
it("forwards toolFilterMode unchanged when set explicitly", async () => {
178-
const { client, spy } = await setupClient();
179-
await client.createSession({
180-
onPermissionRequest: approveAll,
181-
availableTools: ["builtin:bash"],
182-
excludedTools: ["builtin:bash"],
183-
toolFilterMode: "denyPrecedence",
184-
});
185-
const payload = spy.mock.calls.find(([m]) => m === "session.create")![1] as any;
186-
expect(payload.toolFilterMode).toBe("denyPrecedence");
187-
});
188-
189-
it("does not default toolFilterMode in copilot-cli mode", async () => {
177+
it("always sends toolFilterMode: denyPrecedence in copilot-cli mode", async () => {
190178
const { client, spy } = await setupClient("copilot-cli");
191179
await client.createSession({
192180
onPermissionRequest: approveAll,
193181
availableTools: ["builtin:bash"],
194182
});
195183
const payload = spy.mock.calls.find(([m]) => m === "session.create")![1] as any;
196-
expect(payload.toolFilterMode).toBeUndefined();
184+
expect(payload.toolFilterMode).toBe("denyPrecedence");
197185
});
198186

199-
it("defaults toolFilterMode to denyPrecedence in empty mode", async () => {
187+
it("always sends toolFilterMode: denyPrecedence in empty mode", async () => {
200188
const { client, spy } = await setupClient("empty");
201189
await client.createSession({
202190
onPermissionRequest: approveAll,
@@ -206,17 +194,6 @@ describe("Tool filter wiring", () => {
206194
expect(payload.toolFilterMode).toBe("denyPrecedence");
207195
});
208196

209-
it("empty-mode default can be overridden to allowPrecedence", async () => {
210-
const { client, spy } = await setupClient("empty");
211-
await client.createSession({
212-
onPermissionRequest: approveAll,
213-
availableTools: ["builtin:bash"],
214-
toolFilterMode: "allowPrecedence",
215-
});
216-
const payload = spy.mock.calls.find(([m]) => m === "session.create")![1] as any;
217-
expect(payload.toolFilterMode).toBe("allowPrecedence");
218-
});
219-
220197
it("applies the same filter normalization on session.resume", async () => {
221198
const { client, spy } = await setupClient("empty");
222199
const session = await client.createSession({

0 commit comments

Comments
 (0)