Skip to content

fix(runtime-api): tighten workspace, git and job path handling - #6752

Open
Hmbown wants to merge 2 commits into
mainfrom
fix/bh2-runtime-api-workspace-git-jobs
Open

Hmbown wants to merge 2 commits into
mainfrom
fix/bh2-runtime-api-workspace-git-jobs

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

No-Issue: verified bug-hunt findings

Lane runtime-api-workspace-git-jobs. Four leads re-verified on origin/main (6779519); two further leads were duplicates of these. Two commits: be1e56774 (the fixes) and 7f84148cf (review follow-ups: blocking-call gate, one .git rule, push wiring, job cwd wording).

1. Workspace file routes match .git only in lowercase

Problem: relative_request_path and the directory listing matched the component against .git exactly. On case-insensitive filesystems (the macOS default, and Windows), .GIT/config or .Git/hooks/... got past that check but opened the real repository metadata. That was true for file read/write, expect.files keys and the stage/discard path arguments. POST /v1/threads/{id}/file-revert (SnapshotRepo::validate_restore_file) had its own, separate ASCII-case-only check.
Now: one snapshot::is_git_metadata_name matches .git in any letter case. On Windows it also matches the trailing-dot/space, :stream and GIT~N short-name forms. Workspace request paths, the listing filter, file restore, snapshot-delta display paths, turn-artifact display paths and the LSP semantic paths all use it. The Windows forms are a pure helper that takes the platform as a bool, so they are tested on every host.
Test: git_metadata_directory_is_refused_in_any_letter_case, listing_hides_git_metadata_directory_in_any_letter_case, git_metadata_name_covers_case_and_windows_aliases (checks .git., .git , .git::$INDEX_ALLOCATION, GIT~1 and git~12 with windows = true, and that they are ordinary names otherwise). restore_file_if_unchanged_refuses_directories_git_metadata_and_ignored_files adds the Windows spellings when it runs on Windows CI.
Not migrated: tools/subagent/delivery.rs and core/authority.rs keep their own .git checks because those files belong to another lane.

2. GET /v1/diff?path= read the path as a pathspec

Problem: the diff read and the untracked-status probe ran without --literal-pathspecs. The writes in the same module already use it. In a workspace that is a subdirectory of a larger repository, :/other/file or :(top)... returned diffs for tracked files outside the workspace.
Now: both reads run with --literal-pathspecs, through the file_diff_args builder.
Test: file_diff_reads_the_path_literally_inside_a_subdirectory_workspace uses a real repo with a nested workspace. w.txt still diffs. :/…, :(top)… and * return nothing from the sibling directory.

3. POST /v1/git/push accepted option-like remote values

Problem: the character allow-list allowed a leading -, so --force, --mirror, --delete and -f were passed to git push as options. Paths such as ../other-repo were also accepted.
Now: push_args builds the arguments. The effective remote is the requested one, or origin when only set_upstream is given. It must appear in git remote and must not start with -. -- comes before it.
Test: push_remote_must_be_a_configured_remote_name, plus push_args_validate_the_effective_remote_and_end_options. The second test uses a real repo and a bare remote. It covers trim, the remote lookup and the origin default (400 while no origin exists), then runs the built push --set-upstream -- origin main through git and checks the upstream.

4. Job cwd was checked against the thread workspace but run against the server cwd

Problem: create_thread_job checked thread.workspace.join(cwd) but passed the raw string to ShellManager, which resolves a relative path against the daemon's own cwd. A relative cwd therefore either failed or ran in a different directory with the same name. An absolute cwd had no containment check.
Now: resolve_job_cwd resolves the path with tokio::fs, so it never blocks a runtime worker. It checks containment on the canonical path and passes the manager the joined, absolute spelling, so Windows does not receive a \\?\ verbatim cwd. Outside trust mode the cwd must stay inside the workspace. This is stricter than the shell tool's resolve_path: the route has no workspace_follow_symlinks, trusted external roots or ~ expansion, so a symlink leading out of the workspace gets 403. The doc comment and docs/RUNTIME_API.md state this.
Test: job_cwd_resolves_against_the_thread_workspace covers:

  • a relative path and an absolute path inside the workspace, both returned in the caller's spelling
  • a missing directory (400)
  • outside paths (403)
  • a symlink that escapes the workspace (403, unix)
  • trust mode (allowed)

Findings reviewed and not changed

  • 400 before 403 reveals whether a path exists: not changed. A caller who can reach this route can already run commands in the thread, so directory existence is not something the route hides.
  • Windows verbatim cwd: handled by passing the joined path (see 4). I could not run it on Windows here; Windows CI runs the unit tests.

Evidence

  • Targeted cargo test -p codewhale-tui --lib -- runtime_api::jobs::tests runtime_api::git::tests runtime_api::workspace::tests snapshot::repo::tests::{git_metadata_name,restore_file_if_unchanged_refuses,workspace_relative_path} snapshot::delta runtime_threads::turn_artifacts lsp::: 132 passed / 0 failed.
  • Same new tests with the follow-up fixes reverted: 1 passed / 3 failed (git_metadata_name_covers_case_and_windows_aliases, job_cwd_resolves_against_the_thread_workspace, push_args_validate_the_effective_remote_and_end_options). The first commit's revert run was 24 passed / 5 failed.
  • scripts/check-blocking-calls-budget.py: failed on be1e56774 (jobs.rs: path_canonicalize sites 2 > budget 0). It now passes (747 sites, within budget) with no budget raise. check-runtime-contract-budget.py and check-dead-code-budget.py pass. cargo fmt --all -- --check is clean.
  • cargo clippy -p codewhale-tui --lib --tests reports nothing in the touched files. It does stop on 21 existing too_many_arguments errors in other files.
  • I did not run the full TUI suite locally; hosted CI covers it.

🤖 Generated with Claude Code

https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks

- Workspace files: treat `.git` in any letter case (plus Windows
  trailing-dot/space, stream and GIT~N short-name spellings) as the
  repository metadata directory in request paths and listings, so
  case-insensitive filesystems resolve the same refusal.
- GET /v1/diff: read the path with --literal-pathspecs (diff and the
  untracked probe), so `:/`/`:(top)` magic cannot select files outside a
  subdirectory workspace.
- POST /v1/git/push: `remote` must be a configured remote and may not
  start with `-`.
- POST /v1/threads/{id}/jobs: resolve `cwd` against the thread workspace
  and hand the manager the absolute path (it resolved relative paths
  against the server's cwd); outside trust mode the cwd must stay inside
  the workspace, matching the shell tool's resolve_path rule.

Tests: 5 new unit tests; targeted runtime_api::{workspace,git,jobs}
tests 29 passed / 0 failed; with fixes reverted 24 passed / 5 failed
(all five new tests). cargo fmt --check clean; no clippy findings in
touched files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks
Copilot AI balanced review requested due to automatic review settings September 29, 2026 12:03

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

- Jobs: resolve `cwd` with tokio::fs so no path resolution runs on a
  runtime worker (the #6149 blocking-call ratchet failed on the two inline
  canonicalize calls). Containment is still checked on the canonical path,
  but the manager gets the joined spelling as before, so Windows gets no
  `\\?\` verbatim cwd. Doc comment and RUNTIME_API.md now say the route is
  stricter than the shell tool's resolve_path (no follow_symlinks, trusted
  roots or `~`), so a symlink leading out of the workspace is refused.
- `.git` rule: one `snapshot::is_git_metadata_name` for workspace routes,
  file restore (validate_restore_file had its own ASCII-case-only check),
  snapshot delta display paths, turn-artifact display paths and the LSP
  semantic paths. The Windows forms live in a pure helper that takes the
  platform as a bool, so trailing dot/space, `:stream` and `GIT~N` are
  tested on every host.
- Push: `push_args` builds the arguments, validates the effective remote
  (including the `origin` default for set_upstream) and puts `--` before
  the remote.

Tests: 3 new/extended tests. Targeted runtime_api::{jobs,git,workspace},
snapshot::{repo,delta}, turn_artifacts and lsp filters: 132 passed /
0 failed. With the three fixes reverted: git_metadata_name, job_cwd and
push_args tests fail (1 passed / 3 failed). cargo fmt --check clean;
check-blocking-calls-budget, check-runtime-contract-budget and
check-dead-code-budget pass; clippy reports nothing in touched files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks

This branch has not been deployed

No deployments
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