Skip to content

fix(model): resolve CLI exec paths for Windows .cmd shims - #250

Open
WODE25500 wants to merge 2 commits into
microsoft:mainfrom
WODE25500:fix/model-windows-exec
Open

fix(model): resolve CLI exec paths for Windows .cmd shims#250
WODE25500 wants to merge 2 commits into
microsoft:mainfrom
WODE25500:fix/model-windows-exec

Conversation

@WODE25500

Copy link
Copy Markdown
Contributor

Cross-platform exec fixes for the model backends.

  • Resolve codex/claude/cursor/copilot exec paths via shutil.which at config load, so bare npm .cmd shims spawn on Windows (bare 'codex' -> codex.CMD; CreateProcess does not search PATHEXT, so bare names raise WinError 2). Mirrors the proven pattern in the sleep layer.
  • os.symlink uses target_is_directory + copytree/copy2 fallback (fixes OfficeQA on Windows).
  • Scrub the copilot exec stdout/stderr trace (was unredacted, unlike cursor/codex/claude).
  • Add tests for _resolve_cli_path.

Verified: _resolve_cli_path('codex') -> codex.CMD on Windows; targeted tests pass.

- Resolve codex/claude/cursor/copilot exec paths via shutil.which at config
  load so bare npm .cmd shims spawn on Windows (bare 'codex' -> codex.CMD;
  CreateProcess does not search PATHEXT, so bare names raise WinError 2).
- os.symlink uses target_is_directory + copytree/copy2 fallback (OfficeQA
  on Windows).
- Add tests for _resolve_cli_path.
@Yif-Yang

Copy link
Copy Markdown
Contributor

The CLI path-resolution change is useful, but the symlink fallback needs to fail closed before merge.

In prepare_workspace(), every OSError from os.symlink() is treated as “symlinks unavailable”, and copytree(..., dirs_exist_ok=True) then merges into whatever already occupies dst. I reproduced two failures using valid helper arguments:

  • If extra_files creates the destination first, the call succeeds and silently mixes stale and source files.
  • If two link_dirs entries use the same destination, the first creates a symlink to source A; the second gets EEXIST, then copytree follows the existing symlink and copies source B into source A, modifying data outside the workspace.

The current OfficeQA caller generates unique docs/root_N paths, so its normal Windows privilege fallback works, but the helper must not turn any collision into source-tree mutation. At minimum, re-raise when os.path.lexists(dst) and use shutil.copytree(src_abs, dst) without dirs_exist_ok=True; preferably fall back only for the expected Windows “symlink privilege not held” error. Add tests for forced symlink-permission fallback, an existing destination, and duplicate destinations, asserting that neither source is modified.

There is also an incomplete redaction path introduced here: Copilot stdout/stderr is passed through _redact_cursor_error(), but JSON such as {"token":"plain-secret"} remains unchanged because quoted mapping keys do not match its assignment regex. Please sanitize Copilot JSONL structurally using the existing mapping-key-aware sanitizer (or a shared equivalent), and add a run_copilot_exec regression proving secret fields are absent from returned traces and raised error details.

Address maintainer review on microsoft#250:
- prepare_workspace symlink fallback fails closed: re-raise when the
  destination already exists (lexists), never copytree into an existing dst,
  and only fall back for the Windows symlink-privilege-not-held error.
  Prevents a colliding extra_files or duplicate link_dirs entry from mutating
  source data outside the work dir.
- Sanitize Copilot stdout/stderr structurally (mapping-key aware) so JSON
  objects carrying a token/secret field are redacted even when the value is
  a quoted mapping (which the string-level redactor missed).
- Add tests: symlink privilege fallback, existing/duplicate dst, non-privilege
  re-raise, and Copilot JSONL secret-field redaction.
@WODE25500

WODE25500 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Yifan Yang (@Yif-Yang) — thank you for the thorough review and guidance! I've addressed the feedback:

  • The symlink fallback now fails closed: an existing destination raises FileExistsError; copyree/copytree no longer uses dirs_exist_ok; and we only fall back for the Windows "symlink privilege not held" error (WinError 1314 / EPERM / ENOTSUP), re-raising other errors — so a colliding or duplicate destination can no longer mutate the source outside the work dir.
  • Copilot JSON output is now sanitized structurally (mapping-key aware, _redact_copilot_trace/_redact_copilot_json), so {"token": "..."} is redacted too, while content/prompt are kept for debugging.
  • Added tests/test_workspace_symlink.py (privilege fallback, existing/duplicate destination, non-privilege re-raise, JSONL secret-field redaction) — all pass.

Thanks again for the detailed review!

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.

2 participants