Skip to content

fix: replace hand-rolled cmdline escaping with JSON-encoded --agent-config#38

Open
yeelam-gordon wants to merge 23 commits into
mainfrom
dev/yeelam/cmdline-builder-tests
Open

fix: replace hand-rolled cmdline escaping with JSON-encoded --agent-config#38
yeelam-gordon wants to merge 23 commits into
mainfrom
dev/yeelam/cmdline-builder-tests

Conversation

@yeelam-gordon

Copy link
Copy Markdown
Collaborator

Problem

All WT-to-WTA argument passing used hand-rolled escaping (MSVCRT convention), which is not compatible with CommandLineToArgvW. This creates an injection surface for values containing quotes or backslashes.

Solution

  1. Correct quoting - new QuoteArgForCommandLine.h implements the real CommandLineToArgvW algorithm
  2. JSON-encoded config - agent config fields serialized as single JSON object via --agent-config
  3. Backward compatible - WTA accepts both legacy individual args and new --agent-config

Changes

  • src/cascadia/inc/QuoteArgForCommandLine.h - New: correct quoting + BuildAgentConfigArg()
  • src/cascadia/TerminalApp/TerminalPage.cpp - 3 launch paths use BuildAgentConfigArg
  • src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.cpp - probe-models uses QuoteArgForCommandLine
  • tools/wta/src/main.rs - AgentConfig struct + --agent-config clap arg
  • tools/wta/src/cmdline.rs - 8 new JSON round-trip tests (34 total, all pass)

Testing

  • 34 Rust unit tests pass (adversarial JSON payloads, embedded quotes, backslashes, Unicode, injection attempts)
  • All tests use real CommandLineToArgvW as ground truth

yeelam-gordon and others added 2 commits May 21, 2026 22:16
- Extract build_wt_commandline() from shell_manager into cmdline.rs
- Fix quoting bug: embedded double-quotes in args were not escaped,
  allowing argument injection via CommandLineToArgvW parsing
- Add 26 unit tests that round-trip through the real OS parser
  (CommandLineToArgvW) covering: quotes, backslashes, whitespace,
  empty args, shell metacharacters, NUL rejection, and realistic
  agent commands
- Add Win32_UI_Shell feature to windows-sys for test-time
  CommandLineToArgvW access

Future direction: replace hand-rolled commandline escaping with
JSON-encoded structured args to eliminate the escaping problem
by construction (serde_json is already a dependency).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace hand-rolled commandline escaping with a single JSON-encoded arg.
C++ host serializes agent config into JSON, passes via --agent-config.
Rust WTA deserializes with serde_json (backward compatible).

Security: eliminates 6 hand-rolled escaping sites, reduces attack
surface to one correctly-quoted argument boundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 21, 2026 15:14
Comment thread tools/wta/src/cmdline.rs Fixed
Comment thread tools/wta/src/cmdline.rs Fixed
Comment thread tools/wta/src/cmdline.rs Fixed
Comment thread tools/wta/src/cmdline.rs Fixed
Comment thread tools/wta/src/cmdline.rs Fixed
Comment thread tools/wta/src/cmdline.rs Fixed
Comment thread tools/wta/src/cmdline.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens WT↔WTA argument passing by replacing ad-hoc Windows CRT-style escaping with CommandLineToArgvW-compatible quoting, and by bundling agent-related launch configuration into a single JSON --agent-config argument to reduce injection surface.

Changes:

  • Added a shared C++ quoting helper (QuoteArgForCommandLine) and a C++ helper to emit a JSON --agent-config fragment for WTA launches.
  • Updated Terminal launch paths (TerminalPage + Settings Editor probe) to use correct quoting and JSON-based config passing.
  • Added a Rust cmdline module with build_wt_commandline and OS-roundtrip tests; updated WTA CLI parsing to accept and overlay --agent-config.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tools/wta/src/shell/shell_manager.rs Uses the new Rust commandline builder when creating WT panes.
tools/wta/src/main.rs Adds --agent-config parsing and overlays JSON fields onto legacy CLI args (including delegate subcommand support).
tools/wta/src/cmdline.rs Introduces Windows commandline construction + CommandLineToArgvW round-trip tests (including JSON payload cases).
tools/wta/Cargo.toml Enables windows-sys Shell APIs needed for CommandLineToArgvW tests.
src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.cpp Uses CommandLineToArgvW-compatible quoting when passing --agent to probe-models.
src/cascadia/TerminalApp/TerminalPage.cpp Switches multiple WTA launch paths to BuildAgentConfigArg + correct argument quoting.
src/cascadia/inc/QuoteArgForCommandLine.h Adds a reusable argument quoting helper and JSON --agent-config fragment builder.

Comment thread src/cascadia/inc/QuoteArgForCommandLine.h Outdated
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp Outdated
@github-actions

This comment has been minimized.

BuildAgentConfigArg uses manual RFC 8259 JSON construction, not JsonCpp.
Updated comments to reflect the actual implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Comment thread src/cascadia/inc/QuoteArgForCommandLine.h
Comment thread src/cascadia/inc/QuoteArgForCommandLine.h Outdated
Comment thread tools/wta/src/main.rs Outdated
Comment thread tools/wta/src/main.rs
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
- Add <cwchar> and <stdexcept> includes for self-contained header
- Reject embedded NUL in QuoteArgForCommandLine (prevents silent truncation)
- Add QuoteProgramPath() helper for argv[0] (simpler rules, rejects quotes)
- Use QuoteProgramPath for wta executable path in delegate launch
- Fix Rust docstring: 'JSON encoding' not 'JSON library'
- Extract parse_agent_config() helper to eliminate overlay duplication

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@yeelam-gordon yeelam-gordon requested a review from Copilot May 21, 2026 15:46
Addresses all 5 Copilot review comments from round 2.
@yeelam-gordon yeelam-gordon force-pushed the dev/yeelam/cmdline-builder-tests branch from a15387b to ad520e5 Compare May 21, 2026 15:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Comment thread tools/wta/src/shell/shell_manager.rs Outdated
Comment thread tools/wta/src/main.rs Outdated
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp Outdated
Comment on lines +970 to +972
// Use correct CommandLineToArgvW quoting for the agent argument.
const std::wstring args = L"probe-models --agent " +
::Microsoft::Terminal::CommandLine::QuoteArgForCommandLine(std::wstring_view{ agentCmdline });

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 92e663d — QuoteArgForCommandLine returns std::optional. Probe path skips launch if quoting fails.

Comment thread src/cascadia/inc/QuoteArgForCommandLine.h Outdated
@github-actions

This comment has been minimized.

- Convert QuoteArgForCommandLine and QuoteProgramPath to return
  std::optional<std::wstring> instead of throwing on invalid input
- Update all callers (TerminalPage, AIAgentsViewModel) to handle
  the optional gracefully with early-return/skip patterns
- Fix shell_manager.rs comment about error propagation behavior
- Remove <stdexcept> include (no longer needed)

This prevents app-terminating exceptions from settings/user text
containing embedded NUL, satisfying the non-throwing API requirement
for UI code paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yeelam-gordon yeelam-gordon requested a review from Copilot May 21, 2026 16:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
Comment thread src/cascadia/inc/QuoteArgForCommandLine.h Outdated
- Make delegate prompt quoting failure a hard early-return (not silent)
- Fix BuildAgentConfigArg doc: NUL is escaped by JSON, not rejected

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Comment thread src/cascadia/TerminalApp/TerminalPage.cpp Outdated
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp Outdated
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
Comment thread src/cascadia/TerminalApp/TerminalPage.cpp
- Use QuoteProgramPath(wtaPath) instead of raw path in agent pane
  creation (_AutoCreateHiddenAgentPane, _OpenOrReuseAgentPane)
- Add diagnostic logging on all quoting failures for discoverability
- Fixes potential argv[0] mis-parsing when wta is in a path with spaces

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread src/cascadia/TerminalApp/TerminalPage.cpp Outdated
Aligns with the convention used in other TerminalApp sources (e.g.
AgentRegistry.h) and in AIAgentsViewModel.cpp.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread src/cascadia/inc/QuoteArgForCommandLine.h Outdated
Comment thread src/cascadia/inc/QuoteArgForCommandLine.h Outdated
Comment thread tools/wta/src/main.rs Outdated
@github-actions

This comment has been minimized.

- Remove noexcept from QuoteArgForCommandLine and QuoteProgramPath;
  wrap bodies in try/catch to return nullopt on allocation failure
  instead of risking std::terminate.
- Eliminate AgentConfigOverlay struct; parse_agent_config now returns
  AgentConfig directly since both structs had identical fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@github-actions

This comment has been minimized.

- Add 'ffi', 'gpt', 'pwned' to expect.txt (standard Rust FFI, model
  names, and security test strings).
- Add spaces after escape sequences in test strings (\tworld → \t world,
  \nline → \n line, \rline → \r line) to prevent the spell checker from
  seeing 'tworld', 'nline', 'rline' as words. Tests still pass — the
  quoting logic is character-agnostic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread tools/wta/src/main.rs Outdated
Comment thread tools/wta/src/main.rs Outdated
@github-actions

This comment has been minimized.

Use pattern destructuring instead of field-by-field access on the
overlay variable. While Rust supports partial moves, destructuring
makes the intent clearer and avoids potential confusion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown

@check-spelling-bot Report

⚠️ Dictionary not found

Problems were encountered retrieving check dictionaries (cspell:cpp/src/lang-keywords.txt cspell:cpp/src/template-strings.txt cspell:dart/src/dart.txt cspell:python/src/additional_words.txt cspell:haskell/dict/haskell.txt cspell:clojure/src/clojure.txt cspell:monkeyc/src/monkeyc_keywords.txt cspell:rust/dict/rust.txt cspell:public-licenses/src/additional-licenses.txt cspell:cpp/src/compiler-msvc.txt cspell:powershell/dict/powershell.txt cspell:cpp/src/people.txt cspell:r/src/r.txt cspell:redis/dict/redis.txt cspell:cpp/src/stdlib-cpp.txt cspell:software-terms/dict/softwareTerms.txt cspell:npm/dict/npm.txt cspell:public-licenses/src/generated/public-licenses.txt cspell:cpp/src/lang-jargon.txt cspell:elixir/dict/elixir.txt cspell:ruby/dict/ruby.txt cspell:svelte/dict/svelte.txt cspell:typescript/dict/typescript.txt cspell:sql/src/tsql.txt cspell:ada/dict/ada.txt cspell:k8s/dict/k8s.txt cspell:cpp/src/stdlib-cerrno.txt cspell:node/dict/node.txt cspell:swift/src/swift.txt cspell:css/dict/css.txt cspell:python/src/python/python.txt cspell:sql/src/sql.txt cspell:software-terms/dict/webServices.txt cspell:cpp/src/stdlib-cmath.txt cspell:gaming-terms/dict/gaming-terms.txt cspell:cpp/src/ecosystem.txt cspell:latex/dict/latex.txt cspell:lua/dict/lua.txt cspell:python/src/python/python-lib.txt cspell:java/src/java-terms.txt cspell:docker/src/docker-words.txt cspell:cpp/src/stdlib-c.txt cspell:java/src/java.txt cspell:django/dict/django.txt cspell:dotnet/dict/dotnet.txt cspell:cpp/src/compiler-gcc.txt cspell:scala/dict/scala.txt cspell:cpp/src/compiler-clang-attributes.txt cspell:golang/dict/go.txt cspell:python/src/common/extra.txt cspell:html/dict/html.txt cspell:php/dict/php.txt cspell:fullstack/dict/fullstack.txt cspell:shell/dict/shell-all-words.txt).

⚠️ For more information, see check-dictionary-not-found.

🔴 Please review

See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.

Unrecognized words (142)
adbea
agentic
arget
asid
askuser
azmcp
bestpractices
caac
CACHEDIR
capturep
cbe
cdfabe
checkmarks
chpwd
Cim
claude
CLAUDECODE
clippy
clis
cmdkey
crossterm
CWDs
DACLs
Dedented
demotable
desync
DFX
diffed
dotent
doy
drx
dtx
eef
eku
entrancy
ession
extened
Figma
focusp
foob
fooba
footgun
formedness
Ghostty
githubnext
greenfield
greppable
haikus
Hinnant
inputbox
installable
IOCP
ipfs
kaitao
keyspace
killp
llm
lrx
LSBs
lsp
lsw
ltx
MBM
mcp
meproj
MMdd
mojibake
mpsc
MRT
mtimes
myproj
nafter
ncwd
neww
NOAGGREGATION
noname
nopath
normaliser
normalises
noshortcuts
notacommand
nrx
nsummary
ntwo
ntx
Nushell
obra
Oids
oobe
ools
openai
parallelizable
peekable
Prereq
PRIs
proactively
prx
psobject
ptx
pytest
qdk
qqqqq
Rasterize
ratatui
recognises
regen
reparses
replacen
reprioritized
respawning
respawns
retryable
rmcp
rrx
rtx
rustc
RUSTFLAGS
rustup
serde
sideload
SIGKILLs
signtool
splitn
splitw
submittable
synthesises
THH
tokio
toolpath
trn
undercounted
undercounting
Uninstalls
unrecognised
usize
vcxprojs
vendored
Wez
wlen
yeelam
ZDOTDIR
zzzzz
These words are not needed and should be removed Ccc cplusplus ctl Debian dotnet drv endptr evt Fullwidth gitlab hdr idl IME inbox intelligentterminal Ioctl KVM lbl lld lsb NONINFRINGEMENT notif oss outdir pri prioritization PSobject rcv segfault Signtool sourced SWP Tbl testname transitioning unk unparseable Virt VMs webpage websites xsi

Some files were automatically ignored 🙈

These sample patterns would exclude them:

^\.dotnet\/\.dotnet\/TelemetryStorageService/
^\Q.dotnet/.dotnet/.workloadAdvertisingManifestSentinel10.0.200\E$
^\Q.dotnet/.dotnet/10.0.201.aspNetCertificateSentinel\E$
^\Q.dotnet/.dotnet/10.0.201.dotnetFirstUseSentinel\E$
^\Q.dotnet/.dotnet/10.0.201.toolpath.sentinel\E$
^\Qinstaller/bootstrap/target/.rustc_info.json\E$
^copilot-version\.err$
^copilot-version\.out$

You should consider excluding directory paths (e.g. (?:^|/)vendor/), filenames (e.g. (?:^|/)yarn\.lock$), or file extensions (e.g. \.gz$)

You should consider adding them to:

.github/actions/spelling/excludes.txt

File matching is via Perl regular expressions.

To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.

To accept these unrecognized words as correct, update file exclusions, and remove the previously acknowledged and now absent words, you could run the following commands

... in a clone of the git@github.com:microsoft/intelligent-terminal.git repository
on the dev/yeelam/cmdline-builder-tests branch (ℹ️ how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c/apply.pl' |
perl - 'https://github.com/microsoft/intelligent-terminal/actions/runs/26259594519/attempts/1' &&
git commit -m 'Update check-spelling metadata'
Available 📚 dictionaries could cover words (expected and unrecognized) not in the 📘 dictionary

This includes both expected items (2057) from .github/actions/spelling/expect/alphabet.txt .github/actions/spelling/expect/expect.txt .github/actions/spelling/expect/web.txt and unrecognized words (142)

Dictionary Entries Covers Uniquely
cspell:csharp/csharp.txt 32 2 2
cspell:aws/aws.txt 232 2 2
cspell:fonts/fonts.txt 536 1 1

Consider adding to the extra_dictionaries array (in the .github/actions/spelling/config.json file):

    "cspell:csharp/csharp.txt",
    "cspell:aws/aws.txt",
    "cspell:fonts/fonts.txt",

To stop checking additional dictionaries, put (in the .github/actions/spelling/config.json file):

"check_extra_dictionaries": []
Forbidden patterns 🙅 (10)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

Should be nonexistent
\b[Nn]o[nt][- ]existent\b
Should be preexisting
[Pp]re[- ]existing
Should be ; otherwise or . Otherwise

https://study.com/learn/lesson/otherwise-in-a-sentence.html

, [Oo]therwise\b
Should probably be Otherwise,
(?<=\. )Otherwise\s
Complete sentences in parentheticals should not have a space before the period.
\s\.\)(?!.*\}\})
Should be set up (setup is a noun / set up is a verb)
\b[Ss]etup(?= (?:an?|the|to)\b)
Should be reentrancy
[Rr]e[- ]entrancy
Should be reentrant
[Rr]e[- ]entrant
Should be whether or not ...
(?i)\b(?:whe|ra)ther(?:\s\w+)+ or not\.
Should be WinGet
\bWinget\b

Pattern suggestions ✂️ (2)

You could add these patterns to .github/actions/spelling/patterns/d47c725f3fb35fa85bf78220f272b6b047c172af.txt:

# Automatically suggested patterns

# hit-count: 1 file-count: 1
# python
\b(?i)py(?!gment|gmy|lon|ramid|ro|th)(?=[a-z]{2,})

# hit-count: 1 file-count: 1
# node packages
(["'])@[^/'" ]+/[^/'" ]+\g{-1}

Alternatively, if a pattern suggestion doesn't make sense for this project, add a # to the beginning of the line in the candidates file with the pattern to stop suggesting it.

Errors, Warnings, and Notices ❌ (8)

See the 📂 files view, the 📜action log, 👼 SARIF report, or 📝 job summary for details.

❌ Errors, Warnings, and Notices Count
⚠️ binary-file 6
ℹ️ candidate-pattern 2
⚠️ check-dictionary-not-found 52
❌ check-file-path 29
❌ forbidden-pattern 17
⚠️ noisy-file 7
⚠️ single-line-file 1
⚠️ token-is-substring 3

See ❌ Event descriptions for more information.

✏️ Contributor please read this

By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.

If the listed items are:

  • ... misspelled, then please correct them instead of using the command.
  • ... names, please add them to .github/actions/spelling/allow/names.txt.
  • ... APIs, you can add them to a file in .github/actions/spelling/allow/.
  • ... just things you're using, please add them to an appropriate file in .github/actions/spelling/expect/.
  • ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in .github/actions/spelling/patterns/.

See the README.md in each directory for more information.

🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

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.

3 participants