Browse Source

fix(cli): reject default-surface flags leaked onto the web subcommand

ds-review-bot: `dsh web -p task`, `dsh web --resume s`, and `dsh --config c.yml
web` reached the web action with those values in program.opts() but the action
ignored them and served — silently dropping mode-specific inputs. The web
action now reads the parent opts and fails loud (exit 1) on a leaked
--config/-p/--resume, matching the root mode's mixing guard. Covered in
args.spec.ts.

Also (ds-review-bot): tui-demo/README documented the removed `dsh
[path-to-cordis.yml]` positional form; corrected to bare `dsh` / `dsh --config
<path>`. Agent Note + Chinese pair note the web-leak guard.
Turtle 2 months ago
parent
commit
5a06b9e926

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write
-2026-07-24-dsh-commander-argument-adapter.md: 1da81a1bdfc64fb6b7565c4881a7be25fb619fd4
-2026-07-24-dsh-commander-argument-adapter.zh.md: 5835d859ea8abf321a3d57bda3218f76569f8c7a
+2026-07-24-dsh-commander-argument-adapter.md: c304cac5870af838794df85a18be63ca85ce06eb
+2026-07-24-dsh-commander-argument-adapter.zh.md: fb16f89c84c27d42f7aa638c5b019c52ce51068e

File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.md


File diff suppressed because it is too large
+ 0 - 0
.agents/notes/implemented/architecture/2026-07-24-dsh-commander-argument-adapter.zh.md


+ 11 - 1
apps/cli/src/args.ts

@@ -112,7 +112,17 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
     .option('--host <host>', 'override the config bind host (127.0.0.1 or 0.0.0.0)')
     .option('--port <port>', 'override the config listen port (0 requests an OS-assigned port)')
     .option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
-    .action((options: WebOptions) => { resolved = resolveWeb(options) })
+    .action((options: WebOptions) => {
+      // Commander parses the parent (default-surface) options on either side of
+      // the subcommand into `program.opts()`. `web` shares none of them, so a
+      // leaked `--config`/`-p`/`--resume` is a mistyped invocation that must
+      // fail loud rather than silently start the web server and drop it.
+      const parent = program.opts<{ config?: string; prompt?: string; resume?: string }>()
+      if (parent.config !== undefined || parent.prompt !== undefined || parent.resume !== undefined) {
+        program.error('error: web takes none of --config, -p/--prompt, or --resume')
+      }
+      resolved = resolveWeb(options)
+    })
 
   try {
     program.parse(argv, { from: 'user' })

+ 5 - 0
apps/cli/tests/args.spec.ts

@@ -47,6 +47,11 @@ describe('parseDshArgs', () => {
     expect(exitCode(['-p', 'x', '--resume', 's'])).toBe(1)
     expect(exitCode(['--bogus'])).toBe(1)
     expect(exitCode(['bogus-positional'])).toBe(1)
+    // A default-surface flag on either side of `web` leaks into program.opts()
+    // but the web subcommand shares none of them: reject rather than serve.
+    expect(exitCode(['web', '-p', 'task'])).toBe(1)
+    expect(exitCode(['web', '--resume', 's'])).toBe(1)
+    expect(exitCode(['--config', 'c.yml', 'web'])).toBe(1)
   })
 
   it('exits 0 for --help (disclosing web) and --version', () => {

+ 1 - 1
packages/examples/tui-demo/README.md

@@ -49,7 +49,7 @@ Fresh runs mint a `main-session-<uuid>` session id and pass it to both the TUI a
 
 ## Front door
 
-This package ships no bin. The [`dsh`](../../../apps/cli/README.md) CLI is the terminal front door: `dsh [path-to-cordis.yml]` boots a leaf config that mounts this bundle (defaulting to the shipped `examples/tui-agent/cordis.yml`), loads the optional cwd `.env`, drives the Cordis Loader, and waits for the full plugin tree. The repository installs Loader's optional native helper, so bare package specifiers resolve under plain Node.
+This package ships no bin. The [`dsh`](../../../apps/cli/README.md) CLI is the terminal front door: bare `dsh` boots the shipped `examples/tui-agent/cordis.yml` (which mounts this bundle), and `dsh --config <path-to-cordis.yml>` boots an alternate leaf config that mounts it. It loads the optional cwd `.env`, drives the Cordis Loader, and waits for the full plugin tree. The repository installs Loader's optional native helper, so bare package specifiers resolve under plain Node.
 
 ## Example leaf
 

Some files were not shown because too many files changed in this diff