Files
pmg/packagemanager/pypi_executor.go
T
648adcbda4 feat: add uvx (uv tool run) package executor (#357)
* feat(uvx): add uvx (uv tool run) package executor

Adds support for `uvx`, implemented as a PyPI Executor alongside pipx.
uvx is an alias for `uv tool run`: it installs a tool into an ephemeral
environment and runs it, so it has no install/list subcommand and the
first positional argument (or --from) is the package to audit.

Parsing highlights:
- --from overrides the positional command as the package to audit
- --with packages are audited as additional environment dependencies
- name@version shorthand (ruff@0.3.0, ruff@latest) is normalized
- flag parsing stops at the tool name so the tool's own flags are not
  misread as uvx options; uvx's value/boolean flags are registered so
  none greedily consume the package positional
- VCS/URL/local-path specs are skipped for registry auditing

Wires up command registration, analytics, shell alias/shim, cloud audit
mapping, a dedicated `uvx` sandbox profile (UV_*/PIP_* env, uv cache and
tool dirs), config policy, docs, unit tests and an E2E workflow step.

Closes #326

https://claude.ai/code/session_011hyLxq7oWJX5Dp4tCEfG19

* chore(uvx): align docs and base profile with uvx support

Incorporates the low-risk, non-parser improvements from the community
PR #345 (author non-responsive) into our implementation:

- list uvx (and the previously-missing pipx) as PyPI managers in the
  pypi-restrictive base profile package_managers and its README, so the
  base profile applies directly when selected via --sandbox-profile
- document uvx in docs/github-action.md and docs/proxy-mode.md
- add version / IsExplicitVersion assertions to the uvx parser tests

Our pflag-based parser is kept as-is: unlike #345 it audits --with
packages and handles all uvx short flags (e.g. -w), both of which the
community PR misses.

* fix(uvx): skip interpreter requests; use require in tests

Addresses review feedback on PR #357:

- uvx interpreter requests (`uvx python`, `uvx python@3.12`, `uvx pypy`,
  ...) launch an isolated interpreter rather than installing a PyPI tool.
  Treating the positional as a package made the guard flow resolve/analyze
  pkg:pypi/python (and python==3.12), which could wrongly block or fail a
  valid invocation. Skip these for the positional; --with packages on the
  same command are still audited.
- Use require.NoError / require.Len for fatal assertions in the uvx tests,
  matching the repo's testing convention, so a failure stops the subtest
  before a nil dereference instead of panicking.

* docs(uvx): document fail-open and --with-requirements trade-offs

Record the two deliberate parsing decisions raised in review as in-code
trade-off comments (no behavior change):

- unknown flags are tolerated (fail open), consistent with the other
  executors; the residual gap only affects non-proxy guard mode since the
  default proxy flow intercepts every registry download.
- --with-requirements / --with-editable values are consumed but not
  expanded into audit targets; expanding them needs manifest-extractor and
  guard changes, tracked as follow-up. Proxy mode still covers them.

* docs(uvx): drop --with-requirements limitation note

Per maintainer review: guard mode is being deprecated and auditing the
contents of an existing requirements file is a scanner's responsibility,
not PMG's. Remove the "known limitation / follow-up" note; the flags stay
registered only so their values are not mistaken for the tool positional.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-02 18:49:31 +05:30

234 lines
7.4 KiB
Go

package packagemanager
import (
"io"
"slices"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/spf13/pflag"
)
type PypiPackageExecutorConfig struct {
CommandName string
InstallCommands []string
NonDownloadCommands []string
// ImplicitRun marks executors that always run a tool with no install
// subcommand (uvx, i.e. `uv tool run`). For these the first positional
// argument (or --from) is the package to audit. See parseUvxCommand.
ImplicitRun bool
}
func DefaultPipxPackageExecutorConfig() PypiPackageExecutorConfig {
return PypiPackageExecutorConfig{
CommandName: "pipx",
InstallCommands: []string{"install", "inject", "run", "upgrade", "upgrade-all", "reinstall", "reinstall-all"},
NonDownloadCommands: []string{
"list", "uninstall", "uninstall-all", "completions", "uninject", "ensurepath", "environment",
},
}
}
type pypiPackageExecutor struct {
Config PypiPackageExecutorConfig
}
func NewPypiPackageExecutor(config PypiPackageExecutorConfig) (*pypiPackageExecutor, error) {
return &pypiPackageExecutor{
Config: config,
}, nil
}
var _ PackageManager = &pypiPackageExecutor{}
func (p *pypiPackageExecutor) Name() string {
return p.Config.CommandName
}
func (p *pypiPackageExecutor) Ecosystem() packagev1.Ecosystem {
return packagev1.Ecosystem_ECOSYSTEM_PYPI
}
func (p *pypiPackageExecutor) ParseCommand(args []string) (*ParsedCommand, error) {
if len(args) > 0 && args[0] == p.Config.CommandName {
args = args[1:]
}
command := Command{Exe: p.Config.CommandName, Args: args}
// uvx (uv tool run) has no install subcommand; the tool is always run.
if p.Config.ImplicitRun {
return p.parseUvxCommand(command, args)
}
if len(args) < 1 {
return &ParsedCommand{Command: command}, nil
}
// pipx run <pkg> downloads and executes a package without globally installing it.
// We extract the package name so it can be audited before execution.
if args[0] == "run" {
return p.parseRunCommand(command, args[1:])
}
// pipx inject <target-venv> <pkg1> [<pkg2> ...] injects packages into an
// existing venv. The first positional arg is the target venv (already installed),
// not a package to audit — we skip it and only audit the injected packages.
if args[0] == "inject" {
return p.parseInjectCommand(command, args[1:])
}
var installCmdIndex = -1
for idx, arg := range args {
if slices.Contains(p.Config.InstallCommands, arg) {
installCmdIndex = idx
break
}
}
if installCmdIndex == -1 {
return &ParsedCommand{Command: command, IsKnownNonDownloadCommand: IsFirstNonFlagArgInList(args, p.Config.NonDownloadCommands)}, nil
}
installArgs := args[installCmdIndex+1:]
flagSet := pflag.NewFlagSet("pipx install", pflag.ContinueOnError)
flagSet.ParseErrorsAllowlist.UnknownFlags = true
flagSet.SetOutput(io.Discard)
// Define known pipx install flags. We register flags that take values to prevent
// their values from being misidentified as package names, and boolean flags
// to prevent the flag itself from being treated as an unknown argument.
// registers --pip-args, --python, --spec so their values aren't picked up as packages
setupCommonPipxFlags(flagSet)
flagSet.Bool("force", false, "")
flagSet.Bool("include-deps", false, "")
flagSet.Bool("system-site-packages", false, "")
err := flagSet.Parse(installArgs)
if err != nil {
return &ParsedCommand{Command: command}, nil
}
packages := flagSet.Args()
return p.buildInstallTargets(command, packages)
}
// parseRunCommand handles `pipx run [flags] <package> [args...]`.
// Only the first positional argument is the package; the rest are arguments
// to the executed program.
func (p *pypiPackageExecutor) parseRunCommand(command Command, runArgs []string) (*ParsedCommand, error) {
if len(runArgs) == 0 {
return &ParsedCommand{Command: command}, nil
}
flagSet := pflag.NewFlagSet("pipx run", pflag.ContinueOnError)
flagSet.ParseErrorsAllowlist.UnknownFlags = true
flagSet.SetOutput(io.Discard)
// Define known pipx run flags. We register flags that take values to prevent
// their values from being misidentified as package names, and boolean flags
// to prevent the flag itself from being treated as an unknown argument.
// registers --pip-args, --python, --spec so their values aren't picked up as packages
_, _, specPkg := setupCommonPipxFlags(flagSet)
flagSet.Bool("no-cache", false, "")
err := flagSet.Parse(runArgs)
if err != nil {
return &ParsedCommand{Command: command}, nil
}
// If --spec is provided, that's the package to audit, not the positional arg
if *specPkg != "" {
return p.buildInstallTargets(command, []string{*specPkg})
}
packages := flagSet.Args()
if len(packages) == 0 {
return &ParsedCommand{Command: command}, nil
}
// Only the first positional arg is the package
return p.buildInstallTargets(command, []string{packages[0]})
}
// parseInjectCommand handles `pipx inject [flags] <target-venv> <pkg1> [<pkg2> ...]`.
// The first positional argument is the target venv (already installed, not audited).
// Subsequent positional arguments are the packages being injected.
func (p *pypiPackageExecutor) parseInjectCommand(command Command, injectArgs []string) (*ParsedCommand, error) {
if len(injectArgs) == 0 {
return &ParsedCommand{Command: command}, nil
}
flagSet := pflag.NewFlagSet("pipx inject", pflag.ContinueOnError)
flagSet.ParseErrorsAllowlist.UnknownFlags = true
flagSet.SetOutput(io.Discard)
// Define known pipx inject flags. We register flags that take values to prevent
// their values from being misidentified as package names, and boolean flags
// to prevent the flag itself from being treated as an unknown argument.
// registers --pip-args, --python, --spec so their values aren't picked up as packages
setupCommonPipxFlags(flagSet)
flagSet.Bool("force", false, "")
flagSet.Bool("include-apps", false, "")
flagSet.Bool("include-deps", false, "")
err := flagSet.Parse(injectArgs)
if err != nil {
return &ParsedCommand{Command: command}, nil
}
packages := flagSet.Args()
if len(packages) < 2 {
// Need at least target-venv + one package to inject
return &ParsedCommand{Command: command}, nil
}
// Skip the first positional arg (target venv), audit the rest
return p.buildInstallTargets(command, packages[1:])
}
// buildInstallTargets creates install targets from a list of package specifiers.
func (p *pypiPackageExecutor) buildInstallTargets(command Command, packages []string) (*ParsedCommand, error) {
var installTargets []*PackageInstallTarget
for _, pkg := range packages {
packageName, version, extras, err := pypiParsePackageInfo(pkg)
if err != nil {
return nil, ErrFailedToParsePackage.Wrap(err)
}
isExplicit := version != ""
version, err = pypiGetMatchingVersion(packageName, version)
if err != nil {
return nil, ErrFailedToResolveVersion.Wrap(err)
}
installTargets = append(installTargets, &PackageInstallTarget{
PackageVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
Name: packageName,
},
Version: version,
},
Extras: extras,
IsExplicitVersion: isExplicit,
})
}
return &ParsedCommand{
Command: command,
InstallTargets: installTargets,
IsManifestInstall: false,
}, nil
}
func setupCommonPipxFlags(flagSet *pflag.FlagSet) (pipArgs, pythonPath, specPkg *string) {
pipArgs = flagSet.String("pip-args", "", "")
pythonPath = flagSet.String("python", "", "")
specPkg = flagSet.String("spec", "", "")
return
}