Skip to content

Require explicit recursive workspace lookup (#646) - #672

Draft
leynos wants to merge 2 commits into
mainfrom
issue-646-require-explicit-opt-in-for-recursive-workspace-executable-resolution
Draft

Require explicit recursive workspace lookup (#646)#672
leynos wants to merge 2 commits into
mainfrom
issue-646-require-explicit-opt-in-for-recursive-workspace-executable-resolution

Conversation

@leynos

@leynos leynos commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

This branch makes recursive workspace executable discovery an explicit
cwd_mode='workspace-recursive' opt-in. Default which and
command_available now search only PATH, preventing an empty or unset PATH
from resolving a checkout-controlled helper.

Closes #646.

Review walkthrough

Validation

  • make check-fmt: passed
  • make lint: passed
  • make doc-coverage: passed (99.14%)
  • make test: passed (2,813 nextest tests; all applicable doctests passed)

Summary by Sourcery

Make executable discovery PATH-only by default and require an explicit workspace-recursive opt-in for checkout-controlled command resolution.

New Features:

  • Add an explicit workspace-recursive executable discovery mode for trusted manifests.
  • Provide diagnostics and localization support guiding users to the recursive opt-in when appropriate.

Bug Fixes:

  • Prevent empty or unset PATH values from implicitly resolving checkout-controlled executables.

Enhancements:

  • Clarify executable search semantics across auto, always, never, and workspace-recursive modes while preserving bounded recursive lookup for deliberate callers.
  • Update resolver caching and lookup behavior to account for the selected search domain.

Documentation:

  • Document the new executable discovery trust boundary, mode semantics, migration path, and architectural decision.

Tests:

  • Expand unit, contract, feature, integration, and snapshot coverage for explicit recursive lookup and PATH-only defaults.

Chores:

  • Update changelog and localized which diagnostics for the breaking behavior change.

References

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR changes executable lookup to a PATH-only default and introduces an explicit cwd_mode='workspace-recursive' opt-in for bounded recursive workspace discovery, with corresponding resolver logic, cache and diagnostic updates, comprehensive regression coverage, and migration/security documentation.

Sequence diagram for explicit recursive workspace executable lookup

sequenceDiagram
    participant Caller
    participant WhichResolver
    participant Lookup
    participant Workspace

    Caller->>WhichResolver: resolve(command, options)
    WhichResolver->>Lookup: PATH lookup
    alt PATH match found
        Lookup-->>WhichResolver: matches
    else PATH miss
        alt cwd_mode == WorkspaceRecursive
            Lookup->>Workspace: search_workspace(env.cwd, command, options.all, workspace_skips)
            Workspace-->>Lookup: discovered paths
            Lookup-->>WhichResolver: matches or not_found
        else other cwd_mode
            Lookup-->>WhichResolver: not_found
        end
    end
    WhichResolver-->>Caller: result
Loading

Flow diagram for PATH-only default and recursive opt-in

flowchart TD
    A["which or command_available"] --> B{"cwd_mode"}
    B -->|auto| C["Search PATH only"]
    B -->|always| D["Search workspace root, then PATH"]
    B -->|never| E["Search non-empty PATH entries"]
    B -->|workspace-recursive| F["Search PATH first"]
    F --> G{"PATH miss?"}
    G -->|yes| H["search_workspace"]
    G -->|no| I["Return PATH match"]
    C --> J["Missing command remains absent"]
    D --> J
    E --> J
    H --> K["Return bounded workspace match or not_found"]
Loading

File-Level Changes

Change Details Files
Make recursive workspace executable discovery an explicit resolver mode while preserving flat search modes.
  • Add workspace-recursive parsing and option semantics.
  • Restrict auto and command_available defaults to explicit PATH directories, including no lookup for empty or unset PATH.
  • Run the existing bounded workspace walker only after a miss in the explicit recursive mode.
  • Ensure current-directory handling, miss diagnostics, cache keys, and workspace kill-switch behavior distinguish the new mode.
src/stdlib/which/options.rs
src/stdlib/which/env.rs
src/stdlib/which/lookup/mod.rs
src/stdlib/which/cache.rs
src/stdlib/which/mod.rs
src/localization/keys.rs
Add regression coverage for the trust-boundary and resolver contract.
  • Test that nested checkout executables resolve only with workspace-recursive.
  • Update cache, unit, feature, integration, predicate, and workspace-switch tests for the changed defaults and opt-in behavior.
  • Cover invalid mode handling and updated not-found diagnostics.
src/stdlib/which/lookup/tests.rs
tests/features/stdlib.feature
tests/std_filter_tests/which_filter_tests.rs
tests/stdlib_which_tests.rs
tests/stdlib_workspace_switch_tests.rs
tests/snapshots/which_diagnostic_snapshot_tests__which_args_invalid_cwd_mode.snap
tests/snapshots/which_diagnostic_snapshot_tests__which_not_found.snap
Document and announce the breaking search-domain change and migration path.
  • Record the security decision and alternatives in a new ADR.
  • Update design and user documentation with mode semantics, trust-boundary guidance, and kill-switch behavior.
  • Add migration guidance, changelog entries, and documentation index coverage.
docs/adr-018-require-explicit-recursive-workspace-which-search.md
docs/netsuke-design.md
docs/users-guide.md
docs/v0-1-0-migration-guide.md
docs/contents.md
CHANGELOG.md
Update localized resolver diagnostics for the new mode and workspace-search hint.
  • Add a localized workspace-recursive hint and accept the new mode in invalid-argument messages.
  • Update all shipped locale message catalogs to reflect the new search semantics.
locales/ar/messages.ftl
locales/cs/messages.ftl
locales/cy/messages.ftl
locales/da/messages.ftl
locales/de/messages.ftl
locales/el/messages.ftl
locales/en-GB/messages.ftl
locales/en-US/messages.ftl
locales/es-419/messages.ftl
locales/es-ES/messages.ftl
locales/fa/messages.ftl
locales/fi/messages.ftl
locales/fr/messages.ftl
locales/gd/messages.ftl
locales/he/messages.ftl
locales/hi/messages.ftl
locales/hu/messages.ftl
locales/id/messages.ftl
locales/it/messages.ftl
locales/ja/messages.ftl
locales/ko/messages.ftl
locales/nb/messages.ftl
locales/nl/messages.ftl
locales/pl/messages.ftl
locales/pt-BR/messages.ftl
locales/pt-PT/messages.ftl
locales/ro/messages.ftl
locales/ru/messages.ftl
locales/sv/messages.ftl
locales/th/messages.ftl
locales/tr/messages.ftl
locales/uk/messages.ftl
locales/vi/messages.ftl
locales/zh-Hans/messages.ftl
locales/zh-Hant/messages.ftl

Assessment against linked issues

Issue Objective Addressed Explanation
#646 Change default which and cwd_mode='auto' resolution to search only explicit PATH directories, so empty or unset PATH cannot recursively resolve executables from the workspace.
#646 Require a clearly named explicit opt-in for recursive workspace executable discovery, while defining cwd_mode='always' as a flat current-directory/workspace-root search rather than implicitly recursive.
#646 Preserve recursive lookup behavior and distinguish search modes in caching, tests, and user/design documentation, while retaining existing PATH, direct-path, platform, skip-list, symlink, executability, canonicalization, all, and cache behavior.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos force-pushed the issue-646-require-explicit-opt-in-for-recursive-workspace-executable-resolution branch from 9852e7b to c72a789 Compare September 4, 2026 11:20
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Please investigate the cause of the following issue using codegraph exploration and research, identify a fix and provide an AI coding agent prompt for the fix:

        FAIL [   0.056s] (1029/2514) netsuke-build stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path
  stdout ───

    running 1 test
    test stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path ... FAILED

    failures:

    failures:
        stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path

    test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1091 filtered out; finished in 0.05s
    
  stderr ───

    thread 'stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path' (5744) panicked at C:\Users\runneradmin\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\mockable-3.0.0\src\env.rs:425:1:
    MockEnv::os_string(?): No matching expectation found
    stack backtrace:
       0: std::panicking::panic_handler
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\std\src\panicking.rs:677
       1: core::panicking::panic_fmt
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\panicking.rs:80
       2: core::panicking::panic_display
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\panicking.rs:259
       3: core::option::expect_failed
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\option.rs:2257
       4: <core::option::Option<core::option::Option<std::ffi::os_str::OsString>>>::expect
       5: <mockable::env::MockEnv as mockable::env::Env>::os_string
       6: <netsuke::stdlib::which::env::EnvSnapshot>::capture_impl::<mockable::env::MockEnv>::{closure#0}
       7: <core::option::Option<std::ffi::os_str::OsString>>::or_else::<<netsuke::stdlib::which::env::EnvSnapshot>::capture_impl<mockable::env::MockEnv>::{closure#0}>
       8: <core::fmt::Arguments>::new::<229, 1>
       9: <netsuke::stdlib::which::env::EnvSnapshot>::capture_with_env::<mockable::env::MockEnv>
      10: <netsuke::stdlib::which::env::EnvSnapshot>::capture_with_env::<mockable::env::MockEnv>
      11: netsuke::runner::process::file_io::ambient_sync::sync_temp_ninja_file
      12: netsuke::stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode
      13: netsuke::manifest::glob::tests::diagnostics_support::counter_value_with_labels
      14: netsuke::stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path::{closure#0}
      15: <netsuke::stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path::{closure#0} as core::ops::function::FnOnce<()>>::call_once
      16: core::ops::function::FnOnce::call_once
                 at /rustc/c54751567b19c4ceb08b0412d83529c2568cba8b/library\core\src\ops\function.rs:250
    note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

  Cancelling due to test failure: 3 tests still running
        PASS [   0.013s] (1030/2514) netsuke-build stdlib::which::pathext_tests::duplicates_collapse_after_normalization
        PASS [   2.035s] (1031/2514) netsuke-build stdlib::network::tests::fetch_streams_responses_into_cache
        PASS [   1.367s] (1032/2514) netsuke-build stdlib::path::hash_utils::tests::properties::streamed_digest_matches_a_one_shot_digest_for_any_length
────────────
     Summary [  10.445s] 1032/2514 tests run: 1031 passed, 1 failed, 2 skipped
        FAIL [   0.056s] (1029/2514) netsuke-build stdlib::which::lookup::tests::workspace_only_executable_requires_recursive_mode::case_2_unset_path
warning: 1482/2514 tests were not run due to test failure (run with --no-fail-fast to run all tests, or run with --max-fail)
error: test run failed
make: *** [Makefile:148: test-nextest] Error 100

https://github.com/leynos/netsuke/actions/runs/33867515631/job/101005632437?pr=672

Seek a systemic fix rather than tactical. Ask yourself, can this happen again or happen elsewhere? If so, think about a long term fix of the underlying issue.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== changed files =='
git diff --name-only "$(git merge-base HEAD origin/main)" HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD

printf '%s\n' '== relevant file discovery =='
fd -t f -e rs . src tests | sort | rg '(which|ambient_sync|file_io|mock|env)'

printf '%s\n' '== outlines =='
for f in $(fd -t f -e rs . src tests | rg '(^|/)(env|mod|tests|ambient_sync)\.rs$|which'); do
  ast-grep outline "$f" --items all --type function,impl,struct,enum,trait 2>/dev/null || true
done

printf '%s\n' '== environment snapshot and mock usage =='
rg -n -C 5 -P '\b(EnvSnapshot|capture_with_env|capture_impl|MockEnv|os_string|PATH|Path)\b' src tests

printf '%s\n' '== ambient synchronisation call graph =='
rg -n -C 5 -P '\b(sync_temp_ninja_file|ambient_sync|capture_with_env|EnvSnapshot::capture)\s*\(' src tests

printf '%s\n' '== target test and test serialisation/environment mutation helpers =='
rg -n -C 8 -P '\bworkspace_only_executable_requires_recursive_mode\b|#\[(case|test|serial|tokio::test)' src tests

printf '%s\n' '== PR patch for affected code =='
git diff "$(git merge-base HEAD origin/main)" HEAD -- src tests 2>/dev/null || git diff HEAD~1 HEAD -- src tests

Length of output: 151802


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== EnvSnapshot implementation =='
env_file=$(fd -t f '^env\.rs$' src/stdlib/which | head -n1)
nl -ba "$env_file" | sed -n '1,360p'

echo '== failing test region =='
test_file=$(fd -t f '^tests\.rs$' src/stdlib/which/lookup | head -n1)
nl -ba "$test_file" | sed -n '300,440p'

echo '== all capture_with_env callers and MockEnv configuration =='
rg -n -C 10 -P '\b(capture_with_env|capture_impl|MockEnv::new|expect_os_string|expect_raw)\b' src tests

echo '== Env trait and MockEnv methods in lockfile/source metadata =='
rg -n -C 6 -P 'trait Env\b|fn os_string\b|fn raw\b|mockable' Cargo.toml Cargo.lock src tests

echo '== Windows-only which environment tests =='
rg -n -C 8 -P '#\[cfg\(windows\)\]|PATHEXT|capture_with_pathext|capture_with_env' src/stdlib/which

echo '== exact changed lines in EnvSnapshot and lookup test =='
git diff --unified=80 "$(git merge-base HEAD origin/main)" HEAD -- src/stdlib/which/env.rs src/stdlib/which/lookup/tests.rs

Length of output: 1367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

env_file=$(fd -t f '^env\.rs$' src/stdlib/which | head -n1)
test_file=$(fd -t f '^tests\.rs$' src/stdlib/which/lookup | head -n1)

echo "== $env_file =="
sed -n '1,360p' "$env_file"

echo "== $test_file lines 300-440 =="
sed -n '300,440p' "$test_file"

echo '== all mock environment fixtures and capture calls =='
rg -n -C 10 -P '\b(capture_with_env|capture_impl|MockEnv::new|expect_os_string|expect_raw)\b' src tests

echo '== Windows-specific capture behaviour =='
rg -n -C 10 -P '#\[cfg\(windows\)\]|PATHEXT|capture_with_pathext' src/stdlib/which

echo '== relevant dependency source, if cached =='
fd -t f 'env.rs' "${HOME}/.cargo/registry/src" 2>/dev/null | rg '/mockable-[^/]+/src/env\.rs$' | head -n 3 | while read -r f; do
  echo "== $f ==";
  sed -n '1,160p' "$f";
  sed -n '380,450p' "$f";
done

echo '== exact PR patch for the affected test and environment module =='
git diff --unified=80 "$(git merge-base HEAD origin/main)" HEAD -- src/stdlib/which/env.rs src/stdlib/which/lookup/tests.rs

Length of output: 103438


@leynos

Fix the incomplete MockEnv contract in the which environment-snapshot tests.

case_2_unset_path fails before it calls lookup. The failure is deterministic on Windows.

src/stdlib/which/lookup/tests.rs::snapshot_without_path configures these reads:

  • os_string("PATH")
  • raw("NETSUKE_WHICH_WORKSPACE")

On Windows, src/stdlib/which/env.rs::EnvSnapshot::capture_impl also calls:

env.os_string("PATHEXT")

MockEnv rejects that unconfigured call. The stack frame for ambient_sync::sync_temp_ninja_file is not the cause. The panic location and the Windows capture path identify the missing PATHEXT expectation.

Apply a systemic fix. Do not add a one-off expectation only in snapshot_without_path. The same defect can recur whenever a test injects MockEnv into EnvSnapshot::capture_with_env and omits a platform-required input.

Create a shared test-only environment builder for EnvSnapshot capture. Make it define the complete contract:

Platform Required injected reads
Non-Windows PATH, NETSUKE_WHICH_WORKSPACE
Windows PATH, PATHEXT, NETSUKE_WHICH_WORKSPACE

For the unset-PATH test, return None for both PATH and PATHEXT. This preserves the production contract: an absent PATHEXT uses DEFAULT_PATHEXT. It also avoids reading the host environment.

Add a Windows regression assertion for the unset-PATH snapshot. Verify that it completes with raw_path == None and uses the default PATHEXT list. Keep the existing behaviour assertion that cwd_mode='auto' does not recursively discover the nested executable.

Use this coding-agent prompt:

Fix the Windows-only `MockEnv::os_string` panic in the `which` resolver tests.

Context:
- The failing test is `src/stdlib/which/lookup/tests.rs::workspace_only_executable_requires_recursive_mode::case_2_unset_path`.
- `snapshot_without_path` injects `MockEnv` into `EnvSnapshot::capture_with_env`.
- The helper currently expects only `PATH` and `NETSUKE_WHICH_WORKSPACE`.
- On Windows, `src/stdlib/which/env.rs::EnvSnapshot::capture_impl` reads `PATH`, `PATHEXT`, and `NETSUKE_WHICH_WORKSPACE`.
- The unconfigured `PATHEXT` read causes `MockEnv::os_string(?): No matching expectation found`.

Implement a systemic test-fixture fix.

Requirements:
1. Create or extend a shared test-only helper for injected `EnvSnapshot` captures.
2. Make the helper configure every environment read that `EnvSnapshot::capture_with_env` can make on the active platform:
   - non-Windows: `os_string("PATH")` and `raw(WORKSPACE_FALLBACK_ENV)`;
   - Windows: `os_string("PATH")`, `os_string("PATHEXT")`, and `raw(WORKSPACE_FALLBACK_ENV)`.
3. Replace the local hand-written `MockEnv` configuration in `snapshot_without_path` with this helper.
4. For the unset-PATH case, return `None` for `PATH`.
5. On Windows, return `None` for `PATHEXT`. Preserve the `DEFAULT_PATHEXT` fallback behaviour. Do not read the host environment.
6. Add or extend Windows-specific tests to prove an injected unset `PATH` and unset `PATHEXT` snapshot:
   - captures successfully;
   - has `raw_path == None`;
   - uses `DEFAULT_PATHEXT`;
   - reads only the documented environment keys once.
7. Keep the existing security contract unchanged:
   - `cwd_mode='auto'`, empty `PATH`, and unset `PATH` must not recursively search the workspace;
   - only `cwd_mode='workspace-recursive'` may search the workspace recursively.
8. Do not suppress the mock failure with wildcard expectations. Strict key and call-count expectations must remain. They detect future additions to the capture contract.
9. Run the focused test on Windows and the `which` test suite. Run the complete test suite if practical.

Explain in the commit message or PR description that this is a test-fixture contract failure, not a resolver failure.

You are interacting with an AI system.

codescene-access[bot]

This comment was marked as outdated.

Keep the default `which` and `command_available` search domain limited to
PATH, and require `cwd_mode="workspace-recursive"` before scanning
checkout-controlled executables.

Preserve the existing recursive walk behind that explicit mode, with its
cache, skip-list, canonicalization, executable, and platform behaviour.
Document the trust boundary and update the complete test and diagnostic
coverage.
Add an accessible flow diagram showing the flat and recursive search paths
selected by each `cwd_mode` value.
@leynos
leynos force-pushed the issue-646-require-explicit-opt-in-for-recursive-workspace-executable-resolution branch from 8eee9eb to d2a2b70 Compare September 8, 2026 13:06
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No quality gates enabled for this code.

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.

Require explicit opt-in for recursive workspace executable resolution

1 participant