Multitenancy hardening: Client Mode#1428
Conversation
Adds Node SDK surface for the multitenancy hardening work in
github/copilot-agent-runtime#7155 (runtime PR #8760).
- New `mode: "empty" | "copilot-cli"` on CopilotClientOptions; empty
mode requires baseDirectory or sessionFs and rejects sessions
without explicit availableTools.
- New ToolSet builder + BuiltInTools.Isolated constant for ergonomic,
source-qualified tool patterns (builtin:*, mcp:*, custom:*).
- availableTools / excludedTools now accept ToolSet or string[]; bare
"*" is rejected with a clear error pointing at the source-qualified
forms.
- New toolFilterMode option ("allowPrecedence" | "denyPrecedence");
empty mode defaults to denyPrecedence so apps can compose
include+exclude.
- Unit tests (18) and e2e tests (3) including recorded CapiProxy
snapshots.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
…recedence -> excluded Mirrors the rename landed in the runtime PR. Also regenerates rpc.ts to pick up the new toolFilterPrecedence field on SessionUpdateOptionsParams, and renames the corresponding E2E capture snapshot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cross-SDK Consistency ReviewThe PR is clearly marked as a draft with explicit follow-up tasks for the other SDKs — this review just maps out the gaps to inform those follow-ups. Changes in this PR (Node.js only)
Notes for follow-up SDKsPython (
Go (
.NET (
Java / Rust — same surface area as above, adjusted for language conventions. What already existsAll existing SDKs already have The Node.js implementation in this PR is clean and the design is straightforward to port. Good foundation! 🎉
|
Why
Today, instantiating
CopilotClientgives you the full Copilot CLIexperience — every built-in tool, every host-side capability, all of the
defaults that make sense for a local developer. For SDK consumers
building multi-tenant agents (servers running untrusted prompts on
behalf of many users), those defaults are dangerous: a tool like
bashor
web_fetchshouldn't be reachable unless the host explicitlyopts in.
Rather than make the runtime guess what an app wants, we let the SDK
declare the agent shape up front. Two pieces:
mode: "empty"says "start from nothing, I'll declare what'ssafe." It refuses to construct without a
baseDirectory(orsessionFs), and refuses to create sessions without an explicitavailableTools.ToolSet+ source-qualified patterns (builtin:*,mcp:*,custom:*, plus exact names) make those declarations ergonomicwithout surprising blast radius. Bare
"*"is now rejected with apointer to the qualified forms — we don't want anyone accidentally
typing one character and re-enabling shell access.
What's new on the SDK surface
availableToolsandexcludedToolsnow accept either aToolSetor aplain
string[]of patterns. The same patterns flow straight through tothe runtime (which is mode-agnostic — see #7155 / #8760 for the
contract).
Tests
nodejs/test/toolSet.test.ts(18 tests) covers the builder,empty-mode validation, ToolSet → wire normalization, bare-
*rejection, and
toolFilterModedefaulting.nodejs/test/e2e/mode_empty.e2e.test.ts(3 tests, withrecorded CapiProxy snapshots) verifies what the LLM actually sees:
Isolatedexcludes shell /edit/grep/web_fetch.builtin:*re-exposes the shell tool.denyPrecedence(the empty-mode default) letsexcludedToolssubtract from
availableTools.All pass locally; CI will run the rest.
Back-compat
"copilot-cli"(the existing behavior). Existingapps that don't set
modesee no change.availableTools/excludedToolspreviously acceptedstring[];they now also accept
ToolSet. Existing call sites compile unchanged.Follow-ups on this branch
nodejs/src/generated/rpc.tsto includetoolFilterMode(currently passed via the wire payload directly).