From d4bf36dfd6a726fd369401d75a7ff903de0b7604 Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Tue, 23 Jun 2026 08:19:13 -0400 Subject: [PATCH] fix(cli): respect remote shadowing policy in list --- .changeset/friendly-windows-heal.md | 6 ++++ packages/core/src/cli.ts | 6 ++++ packages/core/src/cli/inspection.ts | 3 ++ packages/core/test/cli-remote.test.ts | 43 +++++++++++++++++++++++++-- packages/core/test/config.test.ts | 17 +++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 .changeset/friendly-windows-heal.md diff --git a/.changeset/friendly-windows-heal.md b/.changeset/friendly-windows-heal.md new file mode 100644 index 00000000..6daf52ad --- /dev/null +++ b/.changeset/friendly-windows-heal.md @@ -0,0 +1,6 @@ +--- +"@caplets/core": patch +"caplets": patch +--- + +Respect remote Caplet shadowing policy when merging local overlays into remote CLI list output. diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts index 5147f81a..907038e0 100644 --- a/packages/core/src/cli.ts +++ b/packages/core/src/cli.ts @@ -4068,6 +4068,12 @@ function mergeRemoteAndLocalRows( if (row.disabled) { continue; } + if (remote.shadowing !== "allow") { + options.writeErr( + `Local Caplet '${row.server}' is suppressed because the remote Caplet forbids shadowing that Caplet ID.\n`, + ); + continue; + } options.writeErr( `Warning: ${formatOverlaySource(row.source)} Caplet ${row.server} shadows remote Caplet\n`, ); diff --git a/packages/core/src/cli/inspection.ts b/packages/core/src/cli/inspection.ts index 55c53954..7642fd7b 100644 --- a/packages/core/src/cli/inspection.ts +++ b/packages/core/src/cli/inspection.ts @@ -5,6 +5,7 @@ import { resolveConfigPath, resolveProjectConfigPath, type CapletConfig, + type CapletShadowingPolicy, type CapletsConfig, type ConfigSource, type ConfigWithSources, @@ -21,6 +22,7 @@ type CapletListRow = { source: ConfigSource["kind"] | "remote" | "unknown"; path: string | null; shadows: ConfigSource[]; + shadowing?: CapletShadowingPolicy | undefined; }; type ConfigPaths = { @@ -50,6 +52,7 @@ export function listCaplets( source: sources[server.server]?.kind ?? "unknown", path: sources[server.server]?.path ?? null, shadows: shadows[server.server] ?? [], + shadowing: server.shadowing, })); return rows.sort((left, right) => left.server.localeCompare(right.server)); } diff --git a/packages/core/test/cli-remote.test.ts b/packages/core/test/cli-remote.test.ts index 96a60faa..1c0fb6a7 100644 --- a/packages/core/test/cli-remote.test.ts +++ b/packages/core/test/cli-remote.test.ts @@ -493,7 +493,10 @@ describe("remote CLI routing", () => { const err: string[] = []; writeCliCapletConfig(context.configPath, "shared", "Disabled Shared", { disabled: true }); const fetch = vi.fn(async () => - Response.json({ ok: true, result: [remoteListRow("shared", "Remote Shared")] }), + Response.json({ + ok: true, + result: [{ ...remoteListRow("shared", "Remote Shared"), shadowing: "allow" }], + }), ); await runCli(["list", "--json"], { @@ -509,13 +512,44 @@ describe("remote CLI routing", () => { expect(err.join("")).not.toContain("shadows remote Caplet"); }); + it("keeps remote list rows when the remote Caplet forbids local shadowing", async () => { + const context = testContext("caplets-cli-remote-list-forbid-shadow-"); + const out: string[] = []; + const err: string[] = []; + writeCliCapletConfig(context.configPath, "shared", "Local Shared"); + const fetch = vi.fn(async () => + Response.json({ + ok: true, + result: [{ ...remoteListRow("shared", "Remote Shared"), shadowing: "forbid" }], + }), + ); + + await runCli(["list", "--json"], { + env: remoteEnv(context), + fetch, + writeOut: (value) => out.push(value), + writeErr: (value) => err.push(value), + }); + + expect(JSON.parse(out.join(""))).toEqual([ + expect.objectContaining({ server: "shared", name: "Remote Shared", source: "remote" }), + ]); + expect(err.join("")).toContain( + "Local Caplet 'shared' is suppressed because the remote Caplet forbids shadowing that Caplet ID.", + ); + expect(err.join("")).not.toContain("global Caplet shared shadows remote Caplet"); + }); + it("lets project list rows shadow remote rows and warns on stderr", async () => { const context = testContext("caplets-cli-remote-list-project-shadow-"); const out: string[] = []; const err: string[] = []; writeProjectMcpCaplet(context.projectCapletsRoot, "shared", "Project Shared"); const fetch = vi.fn(async () => - Response.json({ ok: true, result: [remoteListRow("shared", "Remote Shared")] }), + Response.json({ + ok: true, + result: [{ ...remoteListRow("shared", "Remote Shared"), shadowing: "allow" }], + }), ); await runCli(["list", "--json"], { @@ -537,7 +571,10 @@ describe("remote CLI routing", () => { const err: string[] = []; writeCliCapletConfig(context.configPath, "shared", "Global Shared"); const fetch = vi.fn(async () => - Response.json({ ok: true, result: [remoteListRow("shared", "Remote Shared")] }), + Response.json({ + ok: true, + result: [{ ...remoteListRow("shared", "Remote Shared"), shadowing: "allow" }], + }), ); await runCli(["list", "--json"], { diff --git a/packages/core/test/config.test.ts b/packages/core/test/config.test.ts index 8cf29abe..a6d056b5 100644 --- a/packages/core/test/config.test.ts +++ b/packages/core/test/config.test.ts @@ -3140,6 +3140,23 @@ describe("config", () => { ]); }); + it("includes Caplet shadowing policy in inspection list rows", () => { + const config = parseConfig({ + mcpServers: { + browser: { + name: "Browser", + description: "Local browser tools.", + command: "browser-mcp", + shadowing: "allow", + }, + }, + }); + + expect(listCaplets({ config, sources: {}, shadows: {} }, { includeDisabled: false })).toEqual([ + expect.objectContaining({ server: "browser", shadowing: "allow" }), + ]); + }); + it("rejects invalid Caplet set sources and duplicate IDs", () => { expect(() => parseConfig({