Skip to content

Fix Composer vendor-dir config cascade (#439, #686) - #1408

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-composer-vendor-dir-cascade
Oct 12, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-composer-vendor-dir-cascade

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #439
Refs #686

Summary

scan -g and agent-mode scans missed Composer installs whose
vendor-dir was set anywhere except the project's own composer.json.
They reported "No packages found" and exited 0 while the installed copies
stayed unpatched.

Root cause

The Composer crawler implemented only part of Composer's
Config::get('vendor-dir') cascade:

  • Global (-g) hardcoded $COMPOSER_HOME/vendor. It ignored the
    global project's config.vendor-dir, the user-level
    $COMPOSER_HOME/config.json, and COMPOSER_VENDOR_DIR, which Composer
    resolves against the Composer home.
  • Local never read the user-level $COMPOSER_HOME/config.json, the
    file composer config -g vendor-dir lib writes.
  • No layer expanded ~/, $VAR or %VAR% the way Composer's
    Platform::expandPath does.

Fix

One resolver, resolve_vendor_dir, now mirrors Composer's order:
COMPOSER_VENDOR_DIR, then the project's composer.json, then the
user-level config.json, then vendor. It applies expandPath and is
used by the global branch, local discovery and the reinstall-hint label.

  • Values from the invoking user's own environment and Composer home are
    honoured as given, including absolute and ~/ values. They have the
    same trust level as the COMPOSER_VENDOR_DIR value that was already
    honoured as given.
  • The scanned project's composer.json keeps the existing relative-subpath
    write gate. A ~/ or $VAR value there is now refused instead of
    resolving to a literal <project>/~/... directory, and the hint prints
    <vendor-dir> instead of a wrong path.
  • Local discovery finds the Composer home without spawning composer.
    It uses COMPOSER_HOME exactly as given, otherwise the same
    platform-default probe the global branch falls back to. The user config
    is read only when neither the env var nor the project's composer.json
    already decides the vendor dir.

Why #686 is Refs, not Fixes

#686 is about an absolute or ~/ config.vendor-dir in the scanned
project's
composer.json. The crawler deliberately refuses that value,
and absolute_config_vendor_dir_is_refused /
config_vendor_dir_escaping_project_is_refused lock that in. The value
names the directory apply later writes into, and composer.json is
tamperable. The npm crawler cites the same rule. Fully fixing #686 needs
a maintainer decision between two options:

  1. Honour those values. This lifts a cross-ecosystem write-boundary
    rule, and resolve_project_root would then bound install-path at
    the external vendor dir's parent.
  2. Keep refusing them, but fail closed. vex would refuse to attest
    from the lock pin, and agent apply would exit non-zero, when the
    project's vendor-dir was refused. This needs plumbing through
    vex_sources / vex_consumed.

This PR covers the part of #686 that doesn't need that decision: ~/,
$HOME and $VAR expansion for values from the user's env and
Composer home. #686 stays open and is labelled agent:needs-human.

Review findings not fixed (/code-review high)

  • Absolute user-level vendor-dir widens the install-path boundary to its
    parent.
    This already happens for an absolute COMPOSER_VENDOR_DIR.
    It needs a tampered installed.json inside the user's own vendor tree.
    The trust level is unchanged.
  • Local tests now read the developer's real $COMPOSER_HOME/config.json.
    This is the same kind of ambient input as COMPOSER_VENDOR_DIR, which
    these tests already don't isolate. It only matters on a machine where
    someone ran composer config -g vendor-dir.
  • ~/ expansion with HOME unset doesn't fall back to passwd. This is
    rare. Expansion returns None, so the scan finds nothing; it never
    resolves a wrong path.

Tests

Before the fix, 5 of the 6 new crawler_composer_e2e tests failed. The
precedence control passed both before and after. All 6 pass with the fix.

Issue Regression test
#439 global composer.json global_vendor_dir_from_global_composer_json
#439 global config.json global_vendor_dir_from_user_config_json
#439 global env global_vendor_dir_env_resolves_against_composer_home
#439 local config.json local_vendor_dir_from_user_config_json
#439 precedence project_vendor_dir_outranks_user_config_json
#686 (partial) user_level_vendor_dir_expands_home_and_env_placeholders, unit test_expand_composer_path, test_resolve_vendor_dir_cascade, test_local_composer_home_honours_set_env, hint label ~/deps case

Run locally:

  • cargo fmt --all -- --check: OK
  • cargo clippy --workspace --all-features -- -D warnings: OK
  • cargo test -p socket-patch-core --all-features: 6322 passed, 4 failed. All 4 failures are read-only-permission tests (copy_tree, vlt_heal, pypi_poetry, pypi_requirements) that can't fail as root in this sandbox (uid 0), and none touch Composer.
  • cargo test -p socket-patch-cli --all-features --lib plus e2e_composer, e2e_composer_vendor_dir_hints, e2e_composer_version_identity, e2e_vendor_composer_crlf: OK
  • SOCKET_PATCH_COMPOSER_E2E_REQUIRED=1 … --test e2e_vendor_composer_build --test e2e_redirect_composer_build -- --ignored against a real Composer 2: OK
  • The full cargo test --workspace didn't fit in this sandbox's disk allowance, so CI covers the remaining suites.

No changes were needed in the npm, PyPI or gem wrappers.

🤖 Generated with Claude Code

https://claude.ai/code/session_015wAxXbPypqoqPcJYcfXYT3


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
scan -g and agent scans missed Composer installs whose vendor-dir was
set anywhere but the project's composer.json: the global project's
composer.json, the user-level $COMPOSER_HOME/config.json (what
`composer config -g vendor-dir` writes), or COMPOSER_VENDOR_DIR for
`composer global`. They reported "No packages found" and exit 0 while
the installed copies stayed unpatched.

One resolver now mirrors Composer's Config::get('vendor-dir'): env,
project composer.json, user config.json, then vendor, with
Platform::expandPath's ~/, $VAR and %VAR% expansion. The global branch,
local discovery and the reinstall-hint label all use it. Values from
the user's own environment and Composer home are honoured as given;
the scanned project's composer.json keeps the existing relative-subpath
gate, and a ~/ or $VAR value there is now refused instead of resolving
to a literal <project>/~/... directory.

Fixes #439. Refs #686.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Address review findings on the vendor-dir resolver:

- A set COMPOSER_HOME is used exactly as Composer uses it, so a
  missing directory no longer falls back to ~/.composer's config.json,
  and a relative value resolves against the project.
- The user-level config.json is read only when neither the env nor the
  project's composer.json decided the vendor dir.
- Global and local discovery share one platform-default probe.
- ~/ and $VAR expansion keep non-UTF-8 bytes.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 12, 2026 02:13
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fed9863. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 12, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review (burn-down agent)


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does: One resolver, resolve_vendor_dir, now follows Composer's own vendor-dir order: COMPOSER_VENDOR_DIR, then the project's composer.json, then the user-level $COMPOSER_HOME/config.json, then vendor. It also expands ~/, $VAR and %VAR% the way Platform::expandPath does. scan -g uses the resolver instead of the hardcoded $COMPOSER_HOME/vendor (fixes #439), and local scans now honour composer config -g vendor-dir. The scanned project's own composer.json keeps the existing rule that only a relative subpath is accepted.

Risk: low to medium. It only changes which directory discovery reads from, and the write-boundary rule on untrusted project composer.json is kept. Values from the user's own env and Composer home are trusted and honoured as given, including absolute paths, the same way COMPOSER_VENDOR_DIR already was.

Look here:

  • composer_crawler.rs:555-597:`` resolve_vendor_dir; the gate on the project's composer.json is at :581
  • composer_crawler.rs:494:```` expand_composer_path
  • composer_crawler.rs:601-611:````` local_composer_home and `global_vendor_dir_sync`
  • crawler_composer_e2e.rs:1345-1576:`````` 6 new e2e tests, plus an EnvVars guard that restores env vars on drop; the env-mutating tests are `#[serial]`

Verified: read the full diff (3 files). Locally: crawler_composer_e2e 39/39 passed, composer_crawler unit tests 38/38, composer_hints 6/6. With main's composer_crawler.rs restored, 5 of the 6 new e2e tests fail. The 6th guards against a precedence regression and passes either way. CHANGELOG.md untouched; no review threads. CI: ci-ok and clippy green on the head. Bugbot: no issues.

Changes I made: none.

Open questions (none of them block this PR):

  1. On Linux, composer_home_candidates (:420-441) probes ~/.composer before $XDG_CONFIG_HOME/composer / ~/.config/composer. When XDG is in use, Composer checks the XDG dir first. This order predates the PR, but local mode now depends on it too. It only matters when both directories exist; worth a small follow-up.
  2. {$home}-style config references ("vendor-dir": "{$home}/vendor") are not expanded. In trusted layers discovery then silently finds nothing; in the project's composer.json the value is accepted as a literal subpath, which stays inside the project. Follow-up candidate: treat {$ as unexpandable.
  3. The Hosted Composer vex attests not_affected for an unpatched package when config.vendor-dir is absolute or uses ~/ or $HOME/ #686 decision the description asks for (whether to honour an absolute or ~/ vendor-dir in the project's composer.json) is still open, which is why this PR only references Hosted Composer vex attests not_affected for an unpatched package when config.vendor-dir is absolute or uses ~/ or $HOME/ #686.
  4. A few existing local tests and vendor_dir_label_follows_config_vendor_dir now read the developer's real Composer home, so they can fail on a machine with composer config -g vendor-dir set. CI is unaffected.

Auto-merge is armed, so approving sends it straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit e438ecb Oct 12, 2026
86 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-composer-vendor-dir-cascade branch October 12, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants