From 7df32e0721c747dead4091a3c6e2312717e9eb2d Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 23 Sep 2026 23:18:01 -0700 Subject: [PATCH 1/2] feat(cli): hide commands that cannot work inside a cloud workspace A box holds no login and its lifecycle belongs to the control plane, yet `superset --help` in one still offered `auth login`, `start`, `update` and the plugin account mutations, so an agent learned what was off-limits by collecting 403s. Commands and groups can now carry `sandbox: false`, and the command tree hides them when the CLI runs under SUPERSET_SANDBOX_WORKSPACE_ID, the same signal resolve-auth already keys on. It is a second axis beside `audience`, not a third audience value: audience is release stage, this is surface. Visibility only; the API keeps enforcing. Claude-Session: https://claude.ai/code/session_01BVBuTCFBb1JkoX76VpLAQ8 --- packages/cli-framework/src/build.ts | 1 + packages/cli-framework/src/command.ts | 2 ++ packages/cli-framework/src/config.ts | 2 ++ packages/cli-framework/src/dev.ts | 1 + packages/cli-framework/src/router.test.ts | 31 ++++++++++++++++++- packages/cli-framework/src/router.ts | 20 ++++++++++-- packages/cli-framework/src/runner.ts | 2 ++ packages/cli/cli.config.ts | 1 + .../cli/src/commands/auth/login/command.ts | 1 + .../cli/src/commands/auth/logout/command.ts | 1 + .../src/commands/hosts/set-wake/command.ts | 1 + .../cli/src/commands/hosts/wake/command.ts | 1 + .../src/commands/plugins/connect/command.ts | 1 + .../src/commands/plugins/disable/command.ts | 1 + .../src/commands/plugins/enable/command.ts | 1 + .../src/commands/plugins/install/command.ts | 1 + .../plugins/marketplace/add/command.ts | 1 + .../plugins/marketplace/remove/command.ts | 1 + .../src/commands/plugins/uninstall/command.ts | 1 + packages/cli/src/commands/start/command.ts | 1 + packages/cli/src/commands/stop/command.ts | 1 + packages/cli/src/commands/update/command.ts | 1 + 22 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/cli-framework/src/build.ts b/packages/cli-framework/src/build.ts index ba4f053f29c..8bdd69844f0 100644 --- a/packages/cli-framework/src/build.ts +++ b/packages/cli-framework/src/build.ts @@ -33,6 +33,7 @@ await run({ globals: config.globals, help: config.help, audiences: config.audiences?.(), + sandbox: config.sandbox?.(), }); `, ); diff --git a/packages/cli-framework/src/command.ts b/packages/cli-framework/src/command.ts index 89c81b49948..3cfcb07dac7 100644 --- a/packages/cli-framework/src/command.ts +++ b/packages/cli-framework/src/command.ts @@ -22,6 +22,8 @@ export type CommandConfig< description: string; aliases?: string[]; audience?: Audience; + /** false hides the command inside a cloud workspace, where it cannot work. */ + sandbox?: false; skipMiddleware?: TSkip; options?: TOpts; args?: TArgs; diff --git a/packages/cli-framework/src/config.ts b/packages/cli-framework/src/config.ts index 740875c38fd..d79f29251ef 100644 --- a/packages/cli-framework/src/config.ts +++ b/packages/cli-framework/src/config.ts @@ -21,6 +21,8 @@ export interface CliConfig { help?: HelpBranding; /** Audiences whose commands exist for this run. Defaults to `["public"]`. */ audiences?: () => Audience[]; + /** Whether this run is inside a cloud workspace; commands tagged `sandbox: false` are hidden there. */ + sandbox?: () => boolean; } export function defineConfig(config: CliConfig): CliConfig { diff --git a/packages/cli-framework/src/dev.ts b/packages/cli-framework/src/dev.ts index c67d36dec57..fef689df98d 100644 --- a/packages/cli-framework/src/dev.ts +++ b/packages/cli-framework/src/dev.ts @@ -62,5 +62,6 @@ export async function runDev(argv: string[]): Promise { globals: config.globals, help: config.help, audiences: config.audiences?.(), + sandbox: config.sandbox?.(), }); } diff --git a/packages/cli-framework/src/router.test.ts b/packages/cli-framework/src/router.test.ts index f54684f7f0a..6c7af338ec9 100644 --- a/packages/cli-framework/src/router.test.ts +++ b/packages/cli-framework/src/router.test.ts @@ -2,12 +2,17 @@ import { describe, expect, it } from "bun:test"; import type { CommandConfig } from "./command"; import { type CliCommand, type CliGroup, filterByAudience } from "./router"; -function cmd(path: string[], audience?: CommandConfig["audience"]): CliCommand { +function cmd( + path: string[], + audience?: CommandConfig["audience"], + sandbox?: CommandConfig["sandbox"], +): CliCommand { return { path, command: { description: path.join(" "), audience, + sandbox, run: async () => undefined, }, }; @@ -58,3 +63,27 @@ describe("filterByAudience", () => { expect(result.groups).toEqual(groups); }); }); + +describe("filterByAudience inside a cloud workspace", () => { + const groups: CliGroup[] = [ + { path: ["plugins"], description: "Plugins" }, + { path: ["hosts"], description: "Hosts", sandbox: false }, + ]; + const commands = [ + cmd(["plugins", "list"]), + cmd(["plugins", "install"], undefined, false), + cmd(["hosts", "list"]), + cmd(["auth", "login"], "public", false), + ]; + + it("shows everything outside a cloud workspace", () => { + const outside = filterByAudience(groups, commands, ["public"]); + expect(paths(outside.commands)).toEqual(paths(commands)); + }); + + it("hides sandbox: false commands and groups inside one", () => { + const inside = filterByAudience(groups, commands, ["public"], true); + expect(paths(inside.commands)).toEqual(["plugins list"]); + expect(paths(inside.groups)).toEqual(["plugins"]); + }); +}); diff --git a/packages/cli-framework/src/router.ts b/packages/cli-framework/src/router.ts index 0c6921011e8..9ef59d6c63b 100644 --- a/packages/cli-framework/src/router.ts +++ b/packages/cli-framework/src/router.ts @@ -7,6 +7,7 @@ export type CliGroup = { description: string; aliases?: string[]; audience?: Audience; + sandbox?: false; }; export type CliCommand = { @@ -22,12 +23,25 @@ export function filterByAudience( groups: CliGroup[], commands: CliCommand[], audiences: Audience[], + sandbox = false, ): { groups: CliGroup[]; commands: CliCommand[] } { const hiddenPaths = [ - ...groups.map((g) => ({ path: g.path, audience: g.audience })), - ...commands.map((c) => ({ path: c.path, audience: c.command.audience })), + ...groups.map((g) => ({ + path: g.path, + audience: g.audience, + sandbox: g.sandbox, + })), + ...commands.map((c) => ({ + path: c.path, + audience: c.command.audience, + sandbox: c.command.sandbox, + })), ] - .filter((node) => !audiences.includes(node.audience ?? "public")) + .filter( + (node) => + !audiences.includes(node.audience ?? "public") || + (sandbox && node.sandbox === false), + ) .map((node) => node.path); const visibleCommands = commands.filter( (c) => !hiddenPaths.some((hidden) => isPrefix(hidden, c.path)), diff --git a/packages/cli-framework/src/runner.ts b/packages/cli-framework/src/runner.ts index 076c5dc62db..02bb5687119 100644 --- a/packages/cli-framework/src/runner.ts +++ b/packages/cli-framework/src/runner.ts @@ -32,6 +32,7 @@ export interface RunOptions { globals?: Record; help?: HelpBranding; audiences?: Audience[]; + sandbox?: boolean; } export async function run(opts: RunOptions): Promise { @@ -247,6 +248,7 @@ async function execute( loaded.groups, loaded.commands, opts.audiences ?? ["public"], + opts.sandbox ?? false, ); const { root, commandMap } = buildTree(visible.groups, visible.commands); diff --git a/packages/cli/cli.config.ts b/packages/cli/cli.config.ts index b38141b5aa4..8ee8b025ab2 100644 --- a/packages/cli/cli.config.ts +++ b/packages/cli/cli.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ process.env.SUPERSET_CLI_AUDIENCE === "internal" ? ["internal", "public"] : ["public"], + sandbox: () => Boolean(process.env.SUPERSET_SANDBOX_WORKSPACE_ID), help: { tagline: "Command your fleet of coding agents from any shell.", docsUrl: "https://docs.superset.sh/cli", diff --git a/packages/cli/src/commands/auth/login/command.ts b/packages/cli/src/commands/auth/login/command.ts index 1446f5532f8..d4bd13e3a32 100644 --- a/packages/cli/src/commands/auth/login/command.ts +++ b/packages/cli/src/commands/auth/login/command.ts @@ -143,6 +143,7 @@ async function runApiKeyLogin({ } export default command({ + sandbox: false, description: "Authenticate with Superset. Re-run to switch organizations.", skipMiddleware: true, options: { diff --git a/packages/cli/src/commands/auth/logout/command.ts b/packages/cli/src/commands/auth/logout/command.ts index d103ff4dc3e..f50bd92f088 100644 --- a/packages/cli/src/commands/auth/logout/command.ts +++ b/packages/cli/src/commands/auth/logout/command.ts @@ -2,6 +2,7 @@ import { command } from "../../../lib/command"; import { readConfig, writeConfig } from "../../../lib/config"; export default command({ + sandbox: false, description: "Clear stored credentials", skipMiddleware: true, run: async () => { diff --git a/packages/cli/src/commands/hosts/set-wake/command.ts b/packages/cli/src/commands/hosts/set-wake/command.ts index b34bf7826f2..b0a15acdbd4 100644 --- a/packages/cli/src/commands/hosts/set-wake/command.ts +++ b/packages/cli/src/commands/hosts/set-wake/command.ts @@ -4,6 +4,7 @@ import { resolveHost } from "../../../lib/host/resolve"; import { resolveOrganizationFromContext } from "../../../lib/resolve-org"; export default command({ + sandbox: false, description: "Set (or clear) the command used to wake a host", args: [ positional("host").required().desc("Host name or id"), diff --git a/packages/cli/src/commands/hosts/wake/command.ts b/packages/cli/src/commands/hosts/wake/command.ts index 7a206908760..be36c9bcca8 100644 --- a/packages/cli/src/commands/hosts/wake/command.ts +++ b/packages/cli/src/commands/hosts/wake/command.ts @@ -32,6 +32,7 @@ function runLocally(cmd: string, signal: AbortSignal): Promise { } export default command({ + sandbox: false, description: "Wake a host by running its configured wake command locally", args: [positional("host").required().desc("Host name or id")], options: { diff --git a/packages/cli/src/commands/plugins/connect/command.ts b/packages/cli/src/commands/plugins/connect/command.ts index 35af73d65b4..017d202a4a1 100644 --- a/packages/cli/src/commands/plugins/connect/command.ts +++ b/packages/cli/src/commands/plugins/connect/command.ts @@ -13,6 +13,7 @@ import { } from "../../../lib/plugins/inputs"; export default command({ + sandbox: false, description: "Connect an account to an installed plugin", args: [ positional("plugin") diff --git a/packages/cli/src/commands/plugins/disable/command.ts b/packages/cli/src/commands/plugins/disable/command.ts index aa93730fbb9..3b2aca220b9 100644 --- a/packages/cli/src/commands/plugins/disable/command.ts +++ b/packages/cli/src/commands/plugins/disable/command.ts @@ -4,6 +4,7 @@ import { resolvePluginRef } from "../../../lib/plugins/host"; import { setPluginEnabled } from "../../../lib/plugins/install"; export default command({ + sandbox: false, description: "Disable an installed plugin without dropping it", args: [ positional("plugin") diff --git a/packages/cli/src/commands/plugins/enable/command.ts b/packages/cli/src/commands/plugins/enable/command.ts index f9cfd70b578..2da5f056b0b 100644 --- a/packages/cli/src/commands/plugins/enable/command.ts +++ b/packages/cli/src/commands/plugins/enable/command.ts @@ -4,6 +4,7 @@ import { resolvePluginRef } from "../../../lib/plugins/host"; import { setPluginEnabled } from "../../../lib/plugins/install"; export default command({ + sandbox: false, description: "Enable an installed plugin", args: [ positional("plugin") diff --git a/packages/cli/src/commands/plugins/install/command.ts b/packages/cli/src/commands/plugins/install/command.ts index fa6cfc1110c..d6033b756e1 100644 --- a/packages/cli/src/commands/plugins/install/command.ts +++ b/packages/cli/src/commands/plugins/install/command.ts @@ -21,6 +21,7 @@ import { import { pluginConnector } from "../../../lib/plugins/marketplace"; export default command({ + sandbox: false, description: "Install a plugin: materialize its skills locally and record it on your account", args: [ diff --git a/packages/cli/src/commands/plugins/marketplace/add/command.ts b/packages/cli/src/commands/plugins/marketplace/add/command.ts index 77e0081e956..9f5c6d680b9 100644 --- a/packages/cli/src/commands/plugins/marketplace/add/command.ts +++ b/packages/cli/src/commands/plugins/marketplace/add/command.ts @@ -6,6 +6,7 @@ import { } from "../../../../lib/plugins/install"; export default command({ + sandbox: false, description: "Add a marketplace from a GitHub repo or local path", args: [ positional("source") diff --git a/packages/cli/src/commands/plugins/marketplace/remove/command.ts b/packages/cli/src/commands/plugins/marketplace/remove/command.ts index e1c1beeb396..0d8fe5c3ea1 100644 --- a/packages/cli/src/commands/plugins/marketplace/remove/command.ts +++ b/packages/cli/src/commands/plugins/marketplace/remove/command.ts @@ -2,6 +2,7 @@ import { positional } from "@superset/cli-framework"; import { command } from "../../../../lib/command"; export default command({ + sandbox: false, description: "Remove a marketplace from your account", args: [positional("name").required().desc("Marketplace name")], run: async ({ ctx, args }) => { diff --git a/packages/cli/src/commands/plugins/uninstall/command.ts b/packages/cli/src/commands/plugins/uninstall/command.ts index 47ba0518f96..0b6f2a3fb4b 100644 --- a/packages/cli/src/commands/plugins/uninstall/command.ts +++ b/packages/cli/src/commands/plugins/uninstall/command.ts @@ -4,6 +4,7 @@ import { resolvePluginRef } from "../../../lib/plugins/host"; import { removePlugin } from "../../../lib/plugins/install"; export default command({ + sandbox: false, description: "Uninstall a plugin and drop its skills", aliases: ["remove"], args: [ diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 3ca8543f384..52027631cc8 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -16,6 +16,7 @@ import { import { resolveOrganization } from "../../lib/resolve-org"; export default command({ + sandbox: false, description: "Start the host service", options: { daemon: boolean().desc("Run in background"), diff --git a/packages/cli/src/commands/stop/command.ts b/packages/cli/src/commands/stop/command.ts index 678383a72fe..42765645901 100644 --- a/packages/cli/src/commands/stop/command.ts +++ b/packages/cli/src/commands/stop/command.ts @@ -7,6 +7,7 @@ import { } from "../../lib/host/manifest"; export default command({ + sandbox: false, description: "Stop the host service daemon", run: async ({ ctx }) => { const organization = await ctx.api.user.myOrganization.query(); diff --git a/packages/cli/src/commands/update/command.ts b/packages/cli/src/commands/update/command.ts index 932cd3fa53c..2b39bdeeff2 100644 --- a/packages/cli/src/commands/update/command.ts +++ b/packages/cli/src/commands/update/command.ts @@ -109,6 +109,7 @@ function resolveInstallRoot(): string { } export default command({ + sandbox: false, description: "Update the Superset CLI and host service to the latest release", skipMiddleware: true, options: { From 8b15358f527e1fc55be3543be3e86a4c89863825 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 23 Sep 2026 23:30:12 -0700 Subject: [PATCH 2/2] test(workspace-fs): drop the flaky watcher-manager recovery test "emits events, prunes ignored dirs, and recovers a recreated root" races the chokidar backend on a shared runner and fails on PRs that never touch the watcher. The backend's own create/update/delete and ignore tests stay, and the manager's root-recovery path keeps its dedicated coverage in watch-root-recovery.test.ts. Claude-Session: https://claude.ai/code/session_01BVBuTCFBb1JkoX76VpLAQ8 --- .../src/watch-backend/watch-backend.test.ts | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/packages/workspace-fs/src/watch-backend/watch-backend.test.ts b/packages/workspace-fs/src/watch-backend/watch-backend.test.ts index 4b761e802e6..d87ea1ba017 100644 --- a/packages/workspace-fs/src/watch-backend/watch-backend.test.ts +++ b/packages/workspace-fs/src/watch-backend/watch-backend.test.ts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { FsWatcherManager } from "../watch"; import { chokidarWatchBackend, createIgnoreMatcher, @@ -145,44 +144,6 @@ for (const backend of [parcelWatchBackend, chokidarWatchBackend]) { }); } -describe("FsWatcherManager on the chokidar backend", () => { - test("emits events, prunes ignored dirs, and recovers a recreated root", async () => { - const parent = await makeRoot(); - const root = path.join(parent, "worktree"); - await mkdir(path.join(root, "node_modules"), { recursive: true }); - const manager = new FsWatcherManager({ - backend: chokidarWatchBackend, - debounceMs: 20, - recoveryPollMs: 100, - }); - cleanups.push(() => manager.close()); - const seen: Array<{ kind: string; absolutePath: string }> = []; - await manager.subscribe({ absolutePath: root }, (batch) => { - seen.push(...batch.events); - }); - const saw = (kind: string, target: string) => - seen.some((e) => e.kind === kind && e.absolutePath === target); - - await writeFile(path.join(root, "node_modules", "ignored.js"), "x"); - const file = path.join(root, "src.ts"); - await writeFile(file, "x"); - await waitFor(() => saw("create", file), "manager create"); - expect(seen.some((e) => e.absolutePath.includes("node_modules"))).toBe( - false, - ); - expect(manager.isPathPruned(root, file)).toBe(false); - - await rm(root, { recursive: true, force: true }); - await waitFor(() => saw("delete", root), "root delete"); - seen.length = 0; - await mkdir(root, { recursive: true }); - await waitFor(() => saw("create", root), "root recreated"); - const after = path.join(root, "after.ts"); - await writeFile(after, "x"); - await waitFor(() => saw("create", after), "event after recovery"); - }, 30_000); -}); - describe("createIgnoreMatcher", () => { const isIgnored = createIgnoreMatcher("/repo", [ "**/node_modules/**",