Skip to content

fix: prompt for update parameters before CLI update#945

Open
EhabY wants to merge 7 commits into
mainfrom
fix/api-workspace-update-defaults
Open

fix: prompt for update parameters before CLI update#945
EhabY wants to merge 7 commits into
mainfrom
fix/api-workspace-update-defaults

Conversation

@EhabY
Copy link
Copy Markdown
Collaborator

@EhabY EhabY commented May 7, 2026

Summary

  • Collects newly required workspace update parameters with VS Code InputBox/QuickPick prompts before running coder update.
  • Runs the CLI update without invoking a shell — args go straight to spawn(binary, args, {}), so values reach coder exactly as typed and there's no quote-escaping surface on Windows. The REST API fallback is preserved for older CLIs.
  • Carries the fresh workspace through the connection state machine, falls back to starting the existing workspace if the update fails or the user cancels, and hides the update status bar while the workspace is still building.

Fixes #939.

Notes for reviewers

Parameter prompting + validation lives in a new src/api/updateParameters.ts module so src/api/workspace.ts stays focused on CLI orchestration. User cancellation throws a dedicated WorkspaceUpdateCancelledError so the state machine can skip the failure warning toast and log it as info instead.

Hardening picked up during review:

  • runCliCommand no longer spawns through a shell, removing the Windows cmd.exe injection surface and the need for cross-platform escape rules.
  • proc.stdin.end() is called immediately after spawn so any future CLI prompt the extension didn't pre-collect EOFs and errors out instead of hanging.
  • Client-side regex testing is dropped (server validates with RE2; JS's backtracking engine would freeze the extension host on hostile patterns). Number-range and required-field checks still run client-side for inline feedback.
  • ${env:VAR} references in coder.globalFlags are now substituted (replacement for the lost shell expansion); documented in package.json.

Tests

  • New unit suite at test/unit/api/updateParameters.test.ts covering parameter collection, prompt validation, cancellation, multi-select serialization, and a ReDoS regression guard.
  • escapeShellArg covered in test/unit/util.test.ts for both platforms.
  • ${env:VAR} substitution covered in test/unit/cliConfig.test.ts.
  • State machine has dedicated tests for the cancellation vs failure branches in maybeUpdate.
Supporting context

The original issue was caused by coder update prompting for parameter values while the extension streams CLI output to a read-only VS Code OutputChannel.

This PR keeps the CLI update path but avoids interactive terminal prompts during remote setup by pre-collecting only parameters that need new values:

  • required active-template parameters without defaults;
  • excluding parameters that already have values on the current build;
  • using quick picks for booleans/options/multi-selects and input boxes for free-form values;
  • applying number-range validation client-side and deferring regex/monotonic constraints to the server.

The collected values are passed as --parameter name=value argv entries to coder update. If the CLI is older than --parameter-aware coder update, the existing REST API fallback handles the version bump.

@EhabY EhabY changed the title fix: update workspaces via API defaults fix: prompt for workspace update parameters May 7, 2026
@EhabY EhabY changed the title fix: prompt for workspace update parameters fix: run workspace updates in interactive task May 8, 2026
@EhabY EhabY self-assigned this May 8, 2026
@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch 19 times, most recently from 558f101 to 436f566 Compare May 11, 2026 20:35
@EhabY EhabY changed the title fix: run workspace updates in interactive task fix: prompt for update parameters before CLI update May 12, 2026
@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch from 436f566 to cdc26fe Compare May 12, 2026 09:36
@EhabY EhabY marked this pull request as ready for review May 12, 2026 09:36
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented May 12, 2026

/coder-agents-review

@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch from cdc26fe to b59cadd Compare May 12, 2026 09:42
Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass review (Netero only). One P0: the new test file has a broken import that prevents the entire updateWorkspace/startWorkspace test suite from loading. The 424 new test lines are dead code until this is fixed.

The panel has not yet reviewed this PR. Fixing this P0 first avoids spending parallel review time on code that may shift once tests actually run.

Netero's take: "The test file is a ghost town. 462 lines of coverage, zero executions. One import stands between this PR and meaningful test feedback."

1 P0, 0 lower.

🤖 This review was automatically generated with Coder Agents.

Comment thread test/unit/api/workspace.test.ts Outdated
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented May 12, 2026

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean feature design. The parameter collection flow, state machine fallback with upfront mode downgrade, and workspace propagation through getWorkspace() are well-structured. The test infrastructure (controlSpawn, quickInputMock, table-driven parameter tests) is solid for async CLI + UI interactions. DEREM-1 (broken import) fixed in b59cadd.

One P1 blocks merge: escapeCommandArg only escapes " inside double quotes, but spawn uses shell: true. Template parameter names and option values come from the server with no character restriction, so a malicious template admin can achieve code execution on the user's local machine. Six of fourteen reviewers independently flagged this. Fix: either single-quote the parameter arg (matching getHeaderArgs in settings/headers.ts), or refactor runCliCommand to use spawn(binary, argsArray) without shell: true.

The PR description explains what the PR does but not why the CLI's own prompts can't be used. The doc comment on updateWorkspace has the answer ("the resolver phase can't render an interactive terminal"), but a reviewer reading the description first would wonder why coder update can't prompt the user directly.

Pariston: "The fix doesn't create a problem worse than the original."

1 P1, 6 P3, 4 Nit.


src/api/workspace.ts:131

Nit [DEREM-14] "canceled" vs "cancelled" inconsistency. Codebase convention for user-facing messages is "cancelled" (17 instances across progress.ts, workspaceStateMachine.ts, cliManager.ts, remote.ts). This line uses American spelling "canceled" to mirror the server API status string, but it's a user-facing error message, not an API enum. (Leorio)

🤖

src/api/workspace.ts:275

P3 [DEREM-3] makeValidator has 6 branching paths and formatConstraint has 6 paths, none with direct test coverage. Server-side validation is the safety net, so incorrect client validation produces confusing inline messages but does not pass bad values to the CLI. The functions total ~35 lines of branching logic. (Netero)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread src/api/workspace.ts Outdated
Comment thread src/api/workspace.ts Outdated
Comment thread src/remote/workspaceStateMachine.ts
Comment thread src/api/workspace.ts Outdated
Comment thread src/api/workspace.ts Outdated
Comment thread src/api/workspace.ts Outdated
Comment thread src/api/workspace.ts Outdated
Comment thread test/unit/api/workspace.test.ts Outdated
Comment thread test/unit/api/workspace.test.ts Outdated
Comment thread src/api/workspace.ts
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented May 12, 2026

Addressing the two notes embedded in the review body (DEREM-3 and DEREM-14):

DEREM-3 (validator/constraint coverage): added a parameter prompt validation describe block in test/unit/api/updateParameters.test.ts that drives makeValidator and formatConstraint through collectUpdateParameters. Coverage spans the empty-required block, number range, non-numeric input, regex mismatch (both default and substituted message), {value} and {min}/{max} substitution, JSON-null bound handling, and placeholder rendering for each constraint shape. No new exports required.

DEREM-14 (canceled vs cancelled): fixed. Error message switched to "cancelled". The server enum string ("canceled") on the comparison side is unchanged.

All other inline threads replied to individually.

@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented May 12, 2026

/coder-agents-review

@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch from 892c77c to ecac995 Compare May 12, 2026 11:47
Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 findings thoroughly addressed. 12 of 13 open findings fixed in 892c77c with corresponding tests. The escapeShellArg extraction and deduplication with getHeaderArgs is a trust signal: the author generalized from a pointed correction. The WorkspaceUpdateCancelledError class cleanly separates user cancellation from failures. DEREM-7 (empty required field) contested; panel closed 6/6, accepting the author's defense that the stepped quick-input flow provides sufficient context.

Two new issues surfaced during fix verification. The DEREM-4 fix is correct on Unix (single quotes block all expansion) but incomplete on Windows: cmd.exe does not treat \" as an escape, and &, |, <, >, ^ are metacharacters even inside double quotes (DEREM-17, P1). Separately, the parameter collection filter is narrower than the CLI's prompting conditions; three scenarios exist where the CLI reaches an interactive prompt the extension didn't pre-collect, causing an unrecoverable hang because stdin is never closed (DEREM-18, P2).

Hisoka on the running-case status guard: "Dormant today because coder update blocks until the workspace is running. Breaks when CLI semantics change."

1 P1, 1 P2, 5 P3.

🤖 This review was automatically generated with Coder Agents.

Comment thread src/util.ts Outdated
Comment thread src/api/updateParameters.ts
Comment thread src/remote/workspaceStateMachine.ts
Comment thread test/unit/remote/workspaceStateMachine.test.ts Outdated
Comment thread test/unit/api/workspace.test.ts
Comment thread src/api/updateParameters.ts Outdated
Comment thread src/util.ts
@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch 2 times, most recently from 3652d57 to 900ebc9 Compare May 12, 2026 13:51
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented May 12, 2026

/coder-agents-review

Copy link
Copy Markdown

@coder-agents-review coder-agents-review Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R3 findings comprehensively addressed. The structural decision to drop shell: true from runCliCommand (DEREM-17) eliminates the entire class of shell injection bugs. proc.stdin.end() (DEREM-18) converts future unmatched CLI prompts into catchable errors. Dropping client-side regex (DEREM-22) removes the ReDoS surface. The ${env:VAR} substitution is a clean replacement for the lost shell expansion. All R3 fixes verified by Netero and the panel.

Two P2 findings from the shell: true removal. First, spawn without a shell changes the failure channel for a missing binary: Node emits an error event instead of a shell-reported non-zero exit. There is no error handler, so the promise never settles and the progress dialog hangs (DEREM-23, 3 reviewers). One-line fix: proc.on("error", reject). Second, getGlobalFlags includes getHeaderArgs output which applies escapeShellArg. Without a shell, those baked-in quotes become literal characters, breaking any multi-word --header-command value for update and start operations (DEREM-24).

Hisoka on the spawn error: "The DEREM-17 fix correctly removed shell: true but changed the failure channel. With a shell, a missing binary produces stderr and a non-zero close code. Without a shell, Node emits an error event on the ChildProcess."

2 P2, 2 P3.


src/api/workspace.ts:83

P3 [DEREM-25] Close handler drops the signal parameter, producing "exited with code null" when the process is killed.

proc.on("close", (code: number) => { ... }) annotates code as number and ignores the second parameter. Node's signature is (code: number | null, signal: NodeJS.Signals | null). When killed by signal (OOM, SIGTERM), code is null. An operator sees "exited with code null" with no indication of what killed the process.

Fix: accept (code: number | null, signal: NodeJS.Signals | null) and include signal in the message when code is null.

(Chopper P3, Hisoka P3)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread src/api/workspace.ts Outdated
Comment thread src/settings/cli.ts Outdated
Comment thread test/unit/api/workspace.test.ts
@EhabY
Copy link
Copy Markdown
Collaborator Author

EhabY commented May 12, 2026

Addressing DEREM-25 from the R4 review body (no inline thread for it):

Fixed in 83c36a3. The close handler now accepts (code: number | null, signal: NodeJS.Signals | null) and formats the error message based on which one is set:

  • Non-zero exit: "..." exited with code N (unchanged path)
  • Signal kill: "..." exited with signal SIGTERM (or signal unknown if both null)

Stderr is still appended when available. Added a regression test (reports the terminating signal when the process is killed) that closes the mocked process with (null, "SIGTERM") and asserts the message contains signal SIGTERM.

All other R4 inline threads replied to individually.

@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch from 83c36a3 to 3b17d24 Compare May 12, 2026 18:46
EhabY and others added 6 commits May 12, 2026 22:23
`coder start` and `coder update` can need to prompt for template parameters,
but the VS Code workbench's terminal renderer isn't initialized during
`resolveAuthority` — so a PTY terminal created in that phase silently drops
all output and can't be used to surface prompts. See microsoft/vscode#12000
and #144729: extension-Pseudoterminal writes before `open()` fires are
ignored, and `open()` doesn't fire until the workbench has mounted the
terminal panel at least once.

Run the CLI via plain `spawn` again and stream output through the existing
write callback (output channel). Before invoking `coder update`, fetch the
new template version's rich parameters and the workspace's current build
parameters; for any required parameter that has no default and no existing
value, prompt the user with `vscode.window.showInputBox` (with regex/range
validation) or `showQuickPick` (when the parameter declares options). Pass
the collected values as `--parameter name=value` flags so the CLI never
has to prompt.

Also propagate the post-update workspace through the state machine so the
agent check uses the fresh build's resources.
- Move parameter prompts and validation to src/api/updateParameters.ts.
- Escape --parameter values via escapeShellArg so server-controlled names
  and values cannot inject shell metacharacters under shell: true.
- Clear stale agent id after a stopped/failed to running update.
- Treat JSON null on validation_min/max as unset.
- Substitute {min}/{max}/{value} in validation_error.
- Drop the warning toast when the user cancels a parameter prompt.
- Rename untilHidden to collectInput; canceled to cancelled.
- Modernize tests: Promise.withResolvers, vi.waitFor, multi-select coverage.
- Drop `shell: true` from `runCliCommand`; pass args as an array via
  `getGlobalFlags` (raw). Eliminates Windows cmd.exe quoting concerns
  entirely (DEREM-17) and lets `--parameter` values pass through verbatim.
- Close child stdin after spawn so unexpected interactive CLI prompts
  see EOF and error out instead of hanging (DEREM-18).
- Guard the running case of the state machine against post-update builds
  that aren't running yet (DEREM-19).
- Drop client-side regex validation (server validates with RE2, ReDoS-safe)
  to prevent extension-host freezes on hostile patterns (DEREM-22).
- Expand `${env:VAR}` in `coder.globalFlags` so users can reference
  environment variables now that there's no outer shell expansion.
- Document the new globalFlags expansion semantics in package.json.
- Handle spawn 'error' events (ENOENT, EACCES) in runCliCommand so missing
  or unexecutable binaries reject the promise instead of hanging the
  progress dialog (DEREM-23).
- Thread the shell-escape choice through to getHeaderArgs so the value is
  only quoted in shell:true contexts. Multi-word coder.headerCommand values
  now reach the CLI verbatim in execFile/no-shell paths (DEREM-24).
- Report the terminating signal in the error message when the process is
  killed (DEREM-25).
- Expose stdinEnd from the spawn test harness and assert it in the
  happy-path test so the DEREM-18 EOF fix is locked in (DEREM-26).

Also added regression tests for spawn 'error' and signal-kill paths, and
updated cliExec/cliConfig tests for the corrected no-shell header value.
@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch from 3b17d24 to e647f3b Compare May 12, 2026 19:23
Splits the shell-handling change out of this PR so it can land (and be
reverted) on its own. Reinstates:

- `runCliCommand` spawns through `shell: true` again, with the workspace
  identifier wrapped in `escapeShellArg` (so server-controlled values
  still flow through the cmd.exe-safe `""`/`%%` escape introduced in R2).
- `coder update` parameter values go through `escapeShellArg` in
  updateParameters.ts before being passed as `--parameter name=value`.
- `getUserGlobalFlags` returns `coder.globalFlags` verbatim again; the
  `${env:VAR}` substitution was tied to the no-shell path and moves to
  the follow-up PR.
- package.json `coder.globalFlags` description drops the env-expansion
  note for now.

Kept (orthogonal hardening, no shell dependence):
- `proc.stdin.end()` to EOF unexpected CLI prompts (DEREM-18).
- State-machine running-case guard (DEREM-19).
- Server-side regex only, no client-side regex eval (DEREM-22).
- `getHeaderArgs(esc)` still threads the escape choice so execFile
  callers (speedtest/supportBundle) don't get quoted header values
  (DEREM-24).
- Signal-aware close handler reporting `signal SIGTERM` etc. (DEREM-25).
- `stdinEnd` assertion in the happy-path test (DEREM-26).

Dropped the `proc.on("error", reject)` handler since with `shell: true`
a missing binary surfaces as a non-zero close from the shell, not a
spawn error event; the corresponding ENOENT test is removed.

Restores the test-side `shellQuote` mirror of `escapeShellArg` in
test/utils/platform.ts (and its tests) for asserting shell-escaped
values cross-platform.
@EhabY EhabY force-pushed the fix/api-workspace-update-defaults branch from 3dc671f to dfa3c13 Compare May 12, 2026 20:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can't provide values/input for new workspace parameters when updating via extension pop-up?

1 participant