Skip to content

Install gate, Phase 2: gate the full would-install set (tree pass)#112

Open
juangaitanv wants to merge 3 commits into
install-gate-phase-1from
install-gate-phase-2
Open

Install gate, Phase 2: gate the full would-install set (tree pass)#112
juangaitanv wants to merge 3 commits into
install-gate-phase-1from
install-gate-phase-2

Conversation

@juangaitanv

@juangaitanv juangaitanv commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR is Phase 2 of the install-gate feature for the Corgea CLI.

Phase 2 expands the gate from named targets to the full would-install set for npm and pip. That means transitive dependencies, lockfile installs, and bare npm installs are checked instead of only the packages typed on the command line.

This keeps Phase 1's public fail-open behavior while making the gate honest about when it has full tree coverage and when it had to fall back to named-only checks.

Stacked on #111. Base branch: install-gate-phase-1. Review this PR's diff in isolation; it contains the Phase 2 slice.

What Phase 2 Includes

  • pip tree resolution through pip install --dry-run --report -, with --only-binary :all: enforced so resolution does not build sdists or run package code.
  • requirements-file safety: -r files are pre-scanned for format-control directives such as --no-binary :all: that could re-enable sdist builds inside pip's parser.
  • npm tree resolution through an isolated npm install --package-lock-only --ignore-scripts in a throwaway temp dir, so the project lockfile is not touched.
  • Bare npm install gating from the nearest ancestor package.json, matching npm's own project-root discovery.
  • npm ci gating from lockfiles, including npm-shrinkwrap precedence, lockfileVersion 1 support, workspace stanza filtering, and fail-closed refusal for unparsable lockfiles unless --force is used.
  • Root-redirect protection for npm flags such as --prefix, -C, and -g, where the install target cannot be safely resolved from the current project context.
  • Provenance labels for findings: (transitive), (from requirements), (already in package.json), and (locked).
  • Named-only fallback disclosure when a full tree pass cannot run. pip -r entries are still parsed and checked in that fallback.
  • Bounded verdict concurrency fixed at 8, with progress output for larger jobs and collapsed repeated outage errors.
  • Test de-flake: vuln_api_url resolution is now tested as a pure function instead of mutating process env in parallel tests.

Deliberately Out Of Scope

Later phases add:

  • uv, yarn, and pnpm
  • --json
  • token auth and fail-closed enforcement

Exit Criteria - Met

A vulnerable transitive dependency blocks the install. A vulnerable lockfile blocks npm ci. Fallback warnings fire when and only when the tree pass did not run.

Covered by hermetic end-to-end tests in tests/cli_tree.rs, tests/cli_bare_install.rs, tests/cli_npm_ci.rs, and tests/cli_provenance.rs. Also confirmed against real npm/pip resolution on the test-cli fixtures, including monorepo ancestor walking and -r requirements.txt trees. ./harness check passes.

Comment thread src/precheck/tree.rs
Comment thread src/precheck/tree.rs
Comment thread src/precheck/mod.rs
Comment thread src/precheck/tree.rs Outdated
Comment thread src/precheck/tree.rs Outdated
Comment thread src/precheck/tree.rs Outdated
@juangaitanv juangaitanv force-pushed the install-gate-phase-1 branch from 68dbba9 to 5a99db0 Compare June 12, 2026 14:51
juangaitanv added a commit that referenced this pull request Jun 12, 2026
…hrinkwrap, lockfile v1, workspace stanzas

Addresses Cursor review on #112.

- SECURITY: the pip resolver's `--only-binary :all:` guard is now appended
  AFTER the user's args. pip format-control is last-wins, so a user
  `--no-binary :all:` could previously re-enable sdist builds and execute
  package code during the report step. e2e test pins the arg order.
- a bare `npm install --prefix <other>` (and `npm ci --prefix/-C`) now
  fails closed unless --force: it installs a redirected project's tree the
  gate can't resolve from the CWD, so it would otherwise install wholly
  unchecked. (Named installs still verify their targets + degrade the tree
  pass to the named-only warning.)
- npm lockfile reads now prefer npm-shrinkwrap.json (npm's precedence) over
  package-lock.json, in both the tree resolver and `npm ci`.
- parse_npm_lockfile supports lockfileVersion 1 (`dependencies` tree,
  recursing nested deps) so a v1 project's `npm ci` isn't forced to bypass
  the gate with --force.
- parse_npm_lockfile restricts the verdict set to `node_modules/` entries,
  excluding workspace SOURCE stanzas (`packages/foo`) — local packages with
  no registry identity that would otherwise be sent to the public vuln-api
  and could falsely block a monorepo install.
@juangaitanv juangaitanv force-pushed the install-gate-phase-2 branch from 936d69a to dd4b575 Compare June 12, 2026 14:51
@juangaitanv juangaitanv force-pushed the install-gate-phase-1 branch from 5a99db0 to ddd215b Compare June 12, 2026 16:42
juangaitanv added a commit that referenced this pull request Jun 12, 2026
…hrinkwrap, lockfile v1, workspace stanzas

Addresses Cursor review on #112.

- SECURITY: the pip resolver's `--only-binary :all:` guard is now appended
  AFTER the user's args. pip format-control is last-wins, so a user
  `--no-binary :all:` could previously re-enable sdist builds and execute
  package code during the report step. e2e test pins the arg order.
- a bare `npm install --prefix <other>` (and `npm ci --prefix/-C`) now
  fails closed unless --force: it installs a redirected project's tree the
  gate can't resolve from the CWD, so it would otherwise install wholly
  unchecked. (Named installs still verify their targets + degrade the tree
  pass to the named-only warning.)
- npm lockfile reads now prefer npm-shrinkwrap.json (npm's precedence) over
  package-lock.json, in both the tree resolver and `npm ci`.
- parse_npm_lockfile supports lockfileVersion 1 (`dependencies` tree,
  recursing nested deps) so a v1 project's `npm ci` isn't forced to bypass
  the gate with --force.
- parse_npm_lockfile restricts the verdict set to `node_modules/` entries,
  excluding workspace SOURCE stanzas (`packages/foo`) — local packages with
  no registry identity that would otherwise be sent to the public vuln-api
  and could falsely block a monorepo install.
@juangaitanv juangaitanv force-pushed the install-gate-phase-2 branch from dd4b575 to c86aa2d Compare June 12, 2026 16:42
Harvested from the install-vuln-gate spike (dfac68e), trimmed to pip/npm
(no uv, no --json, public mode only).

- pip tree via `pip install --dry-run --report -` (--only-binary :all:
  so resolution never builds sdists); npm tree via an isolated
  `npm install --package-lock-only --ignore-scripts` in a throwaway dir
  that never touches the project lockfile
- bare `npm install` gated from the nearest-ancestor package.json (like
  npm finds it); `npm ci` and aliases gated from the lockfile directly,
  with an unparsable-lockfile refusal (--force escape)
- transitive findings labeled by provenance: (transitive),
  (from requirements), (already in package.json) — with a
  "fix with: corgea npm install <pkg>@<fix>" hint — and (locked)
- honest named-only fallback: a failed dry-run or an npm root-redirect
  flag (--prefix, -g) degrades loudly, and pip -r requirements.txt
  entries are still parsed and verified in the fallback
- bare installs that block blame the existing tree explicitly when no
  finding was added by the command
- bounded verdict pool (fixed at 8) with a progress line above 8 jobs;
  repeated outage errors collapse into one line above 3 findings

Also de-flakes the config test: vuln_api_url resolution is tested as a
pure function instead of mutating process env, which raced concurrent
getenv calls under the parallel test harness.
…hrinkwrap, lockfile v1, workspace stanzas

Addresses Cursor review on #112.

- SECURITY: the pip resolver's `--only-binary :all:` guard is now appended
  AFTER the user's args. pip format-control is last-wins, so a user
  `--no-binary :all:` could previously re-enable sdist builds and execute
  package code during the report step. e2e test pins the arg order.
- a bare `npm install --prefix <other>` (and `npm ci --prefix/-C`) now
  fails closed unless --force: it installs a redirected project's tree the
  gate can't resolve from the CWD, so it would otherwise install wholly
  unchecked. (Named installs still verify their targets + degrade the tree
  pass to the named-only warning.)
- npm lockfile reads now prefer npm-shrinkwrap.json (npm's precedence) over
  package-lock.json, in both the tree resolver and `npm ci`.
- parse_npm_lockfile supports lockfileVersion 1 (`dependencies` tree,
  recursing nested deps) so a v1 project's `npm ci` isn't forced to bypass
  the gate with --force.
- parse_npm_lockfile restricts the verdict set to `node_modules/` entries,
  excluding workspace SOURCE stanzas (`packages/foo`) — local packages with
  no registry identity that would otherwise be sent to the public vuln-api
  and could falsely block a monorepo install.
… .npmrc rationale

- SECURITY: pip applies format-control directives found INSIDE a -r file
  AFTER command-line parsing, so a '--no-binary :all:' line in a
  requirements file overrode the tree pass's trailing '--only-binary
  :all:' guard and built sdists — executing package code — during the
  dry-run. The tree pass now pre-scans -r files (transitively, following
  nested includes) and refuses to dry-run any file carrying a
  format-control directive, degrading to the named-only fallback whose
  parser skips option lines. The scan is best-effort on unreadable files
  (same uid — pip can't read them either and fails loudly itself).
  Unit + e2e tests prove the dry-run never executes against such a file
  while the file's entries still verdict.
- collect_v1_dependencies carries an explicit depth cap (serde_json's
  recursion limit fires first today; the cap is the backstop), with a
  depth-bomb test.
- Documented why the .npmrc copy is safe (CLI flags win; package-lock=false
  degrades to named-only by design).
- Adapted the phase-1 alternate-spelling test to the tree-aware fake pip.
@juangaitanv juangaitanv force-pushed the install-gate-phase-1 branch from ddd215b to 5fb3e5d Compare June 12, 2026 18:28
@juangaitanv juangaitanv force-pushed the install-gate-phase-2 branch from c86aa2d to 33da96b Compare June 12, 2026 18:28
Comment thread src/precheck/tree.rs
// user `--no-binary :all:` / `--only-binary :none:` placed in install_args
// must not re-enable sdist builds (which would run package code during the
// report step, violating this file's safety invariant).
let output = Command::new(resolved)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do we need a timeout here? same for resolve_npm_tree?

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