* feat: add experimental Go module support via pmg go
Adds Go modules as a proxy-guarded ecosystem, opt-in only: the command
runs solely when invoked explicitly as `pmg go ...` and is deliberately
excluded from setup aliases and PATH shims so existing users are
unaffected.
- packagemanager: goPackageManager with fail-safe command classification
(vet/fix excluded from non-download since they can fetch on a cold
cache) and pinned-version extraction where only canonical semver
counts as explicit.
- GOPROXY normalization (fail-closed): effective GOPROXY read via
`go env` (honors go env -w), rebuilt comma-joined with `direct`
dropped so a 403 block is terminal and nothing silently falls back to
unanalyzed VCS fetches. GOPRIVATE/GONOPROXY surface a warning;
GOINSECURE is cleared. Contributed to the proxy flow through a new
ProxyRoutingProvider hook (extra child env + dynamic MITM hosts).
- Go interceptor with dynamic host matching from the user's effective
GOPROXY via InterceptorContext.GoProxyHosts. Malware analysis runs on
.zip only (the sole endpoint that delivers code); .info/.mod/@latest/
list pass through; /sumdb/ traffic and sum.golang.org are never
touched so checksum-db verification stays intact; golang.org/toolchain
is allowed on Go's own checksum verification.
- Dependency cooldown: publish time captured from .info responses
(body unmodified), in-window .zip blocked with 403; fails open for
cooldown only when the publish time was never observed.
- Cert gate: on macOS/Windows `pmg go` fails fast with actionable
guidance unless the persisted PMG CA is OS-trusted (Go ignores
SSL_CERT_FILE there); Linux works via the injected bundle.
- proxye2e: GOPROXY-protocol mock registry, Go driver and 10 hermetic
cases (allow/block/confirm, case-escaped paths, cooldown block and
fail-open, toolchain, sumdb passthrough).
Verified end-to-end on Linux: `pmg go get github.com/google/uuid@v1.6.0`
MITMs proxy.golang.org, analyzes the decoded module at the .zip fetch,
and go.sum verification succeeds through the tunneled checksum db.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xuhBeTVpfU4SdqVaarvuK
* fix(go): address review findings on experimental Go support
- Drop fmt/clean from NonDownloadCommands: both load packages via go
list and can download modules on a cold cache, which would bypass the
proxy under install_only.
- Support GOPROXY entries with a base path (e.g. corp Athens/JFrog at
https://corp/goproxy): the interceptor now receives host -> base URL
and strips the path prefix before parsing module URLs, so verdicts
and cooldown key on the real module path.
- Default unschemed GOPROXY entries to https, matching go's own
behavior, so corp mirrors configured as bare hosts are intercepted
instead of silently unanalyzed.
- Memoize the final verdict per module zip: go re-requests a failed
zip during go get's load phase, which double-recorded stats (the
report showed the same blocked module twice) and would have
re-prompted on Confirm verdicts.
- Fetch .info out-of-band on a cooldown cache miss: go serves .info
from its local module cache on any machine that used go before PMG,
which silently disabled cooldown. Failure of the side-fetch still
fails open for cooldown only.
- Move the noop package resolver into packagemanager.
Verified live: cold-cache cooldown block now records once; warm-cache
rerun is blocked via the side-fetch instead of failing open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xuhBeTVpfU4SdqVaarvuK
* docs: collapse Go proxy-mode details by default
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xuhBeTVpfU4SdqVaarvuK
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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#326https://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>
* ci: add E2E for pmg action server-mode
Exercises the safedep/pmg action with server-mode enabled: the action
starts the proxy daemon and injects HTTP_PROXY + CA env vars, then bare
npm/pip installs are intercepted. Asserts benign installs pass, malicious
npm/pip installs are blocked, and 'pmg proxy stop --fail-on-violation'
fails the job on the blocks.
Scoped via paths to action.yml and the proxy server sources so it only
runs when relevant. Complements persistent-proxy-e2e.yml, which covers
the branch's proxy internals via raw pmg proxy commands.
* ci: extract shared proxy E2E assertions into composite action
Both the action server-mode E2E and the persistent-proxy E2E run the same
benign/malicious install assertions. Factor them into a local composite
action (.github/actions/proxy-e2e-assertions) so the assertions live in one
place and both workflows exercise the composite itself.
Proxy teardown (pmg proxy stop --fail-on-violation) stays inline in each
workflow since the setup/teardown differs per binary-under-test (released
binary via the action vs. branch build via make).
* feat(proxy): emit session summary on persistent proxy shutdown
The persistent proxy daemon never ran the per-invocation flow that calls
LogSessionComplete, so no session summary reached the cloud for server-mode
runs. Emit one from the daemon's aggregate stats collector on shutdown,
before the final cloud flush, so it is delivered with the run's events and
carries the CI/invocation context.
Extract the event emission into a shared LogSessionSummary(SessionData); both
the per-invocation flow and the daemon now use it. The daemon serves every
package manager, so the summary carries no single package manager.
* chore(action): shorten default cloud endpoint id prefix to gha/
* refactor(flows): extract SetupCACertificate for reuse
Move the CA load/generate/merge logic out of proxyFlow into an exported
flows.SetupCACertificate so the persistent proxy server can reuse it.
* feat(proxy): add persistent proxy server with start/stop/env/status
Introduces 'pmg proxy' commands backed by internal/proxyserver: a long-lived
MITM proxy that intercepts package managers via env vars (no shims). Supports
--daemon (Unix), --state, --port; generic 'env' output that skips cert vars
when the CA is OS-trusted; opt-in 'stop --fail-on-violation' (fail-closed on
crash) with a synchronous cloud event flush; and the malysis analysis cache.
* feat(action): add server-mode for persistent proxy
When server-mode=true the action starts the proxy daemon and injects proxy
env vars into the job instead of installing shims.
* test(proxy): add persistent proxy server E2E workflow
* docs(readme): document persistent proxy server mode
* fix(proxy): create cache dir before writing state file and daemon log
On a fresh CI runner the cache directory does not exist yet; os.OpenFile and
os.WriteFile do not create parent dirs, so 'pmg proxy start --daemon' failed
with 'no such file or directory'. MkdirAll the parent before writing.
* docs: add persistent proxy server architecture doc
* refactor proxyserver
* fix(proxy): always emit cert env vars instead of skipping on OS-trust status
npm/pip/yarn/requests trust the MITM CA inconsistently across tools, versions,
and configs; many still use bundled CA stores. Always emitting the cert-path
env vars is the conservative choice that works regardless, and is harmless for
tools that read the OS store (they ignore the vars). Skipping them when a
system CA exists would silently break any tool still on a bundled store.
* refactor(proxy): drop redundant audit init in daemon; rely on main.go
main.go's PersistentPreRun already initializes the audit pipeline for every
command (including the daemon's re-exec'd child) and closes it at process exit.
Re-initializing in proxyserver.Run created a second auditor and a second
cloud-sync WAL connection, orphaning the first. Removing it makes the daemon
consistent with the normal proxy flow, which never self-initializes audit.
* fix(proxy): bypass proxy env when flushing events to cloud on stop
pmg proxy stop inherits HTTP(S)_PROXY (injected by 'pmg proxy env') pointing at
the PMG proxy it just shut down. The cloud sync gRPC client honored those vars
and routed api.safedep.io through the dead proxy, failing with 'connection
refused' so no events were delivered. Clear the proxy env vars before the sync
so PMG's own cloud traffic goes direct.
* chore(proxy): address review feedback
- configurable bind host via proxy.server.listen_host (default loopback)
- proxy commands use ui.ErrorExit instead of returning errors to cobra
- rename errcode to ProxyPolicyViolation (covers malware + cooldown)
- share cloud sync via audit.DrainToCloud (de-dup with cmd/cloud/sync)
- centralize proxy CA bundle path in certmanager
- docs: persistent proxy cert trust + bind address
* fix(proxy): show real message on fail-on-violation error
stopExitError set only WithMsg, but ui.ErrorExit renders HumanError, so the
framed error showed 'no human-readable message available'. Set both from one
string, and emit the framed error before the stdout summary so the blocked
count is stated once.
* fix(proxy): flush cloud events from the daemon, not stop
The stop process inherits HTTP_PROXY (from 'pmg proxy env'), so its cloud
client routed api.safedep.io through the already-stopped proxy and failed with
connection refused. Move the flush into the daemon's shutdown, which has no
proxy env (it started before env injection) and dials SafeDep directly.
- daemon flushes on shutdown via audit.DrainToCloud and records the result in
the state file; stop surfaces it (on both success and fail-on-violation
paths) since the daemon's own logs aren't visible to stop
- coordinate stop's wait with the daemon shutdown budget; on timeout, error
out without reading stale state or deleting the file (fail-closed)
- persist blocked count before the flush so the gate stays correct if the
flush hangs or the daemon is killed mid-flush
- remove now-redundant cloud_flush.go
* disable auto-sync for proxy cmds
* feat(proxy): periodic cloud sync + move proxy env vars to packagemanager
- daemon runs a periodic cloud-sync ticker so the shutdown flush stays small;
the run total is reported by stop, and shutdown timeouts are coordinated
- move EnvVarForProxy from config to packagemanager (it is package-manager
knowledge); the shared function now builds the proxy URL and NO_PROXY itself,
removing the duplicated construction in the per-command and persistent paths
- relocate the #319 yarn and #339 IPv6 regression tests alongside the function
- enable cloud sync in the persistent-proxy E2E workflow and fix the stale
internal/proxystate path filter
* refactor(proxy): rename cloudFlushLockTimeout to cloudFlushLockWait
Consistent timeout naming: *LockWait is the lock-acquire bound, *Timeout is
the sync-RPC bound. Previously the final-flush pair was cloudFlushLockTimeout
vs cloudFlushTimeout — two lookalike names for different operations.
* refactor(proxy): extract cloudFlush and trim duplicate shutdown comments
The shutdown's final-flush block is now a cloudFlush helper, symmetric with
startCloudSyncLoop (one-shot vs loop). Removed the triplicated ticker/lock
contention comments, keeping the contract on the function doc and one-line
pointers at the call sites.
* docs: update persistent proxy cloud sync to daemon-owned model
The daemon now owns cloud delivery (periodic sync while serving + final flush
on shutdown); stop signals it, waits, and reports the result. Rewrite the Cloud
event sync section, fix stop attributions, add the cloud_sync state field, and
update the sequence diagram.
* docs: move Usage section up below How it works
Put the copy-paste recipes near the top so users find them before the internals.
* refactor(proxy): address PR review feedback
- configurable bind host/port via --host/--port flags + config (listen_host,
listen_port), bound directly to config fields per PMG's flag pattern
- daemon log path via --log-file and readiness timeout in ProxyDaemonConfig;
Daemonize no longer owns path policy (caller validates, fails fast)
- gate periodic cloud sync on auto_sync; suppress detached background sync for
proxy commands instead of flipping the flag
- pmg proxy env --export emits shell-quoted lines for eval (spaces survive)
- extract shared flows.BuildCachedMalysisAnalyzer, dropping the analyzer+cache
duplication between proxy flow and proxy server
- add internal/proxyserver/doc.go documenting the package + boundary vs flows
- E2E: assert malicious installs are blocked (drop continue-on-error)
- docs: trim Commands/State-file to user contracts; refresh bind address
* refactor(proxy): proactive alignment fixes from whole-PR review
- gate the shutdown cloud flush on auto_sync too, matching the periodic ticker
(auto_sync consistently controls all daemon-driven cloud delivery)
- ResolveStatePath takes cacheDir instead of *RuntimeConfig, keeping state.go
free of config dependency
- drop the empty-host comment in listenAddr; keep the loopback guard so a blank
host never silently binds all interfaces
* fix: Decouple localdb with malysis analyser construction
* fix: Persist global args before proxy server daemon exec
* fix: GitHub Action for cloud auto-sync in server mode
---------
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
* feat(cooldown): respect trusted_packages in dependency cooldown
Trusted packages are now treated as a superset waiver that bypasses every
PMG control (malware analysis, cooldown, and any future controls). A
globally trusted package is automatically exempt from the cooldown window
and no longer needs a duplicate entry in dependency_cooldown.skip.
The skip list remains the narrower, cooldown-only waiver for packages
that must bypass the cooldown wait but still be malware-scanned.
* refactor(cooldown): tag skip reason and audit-log skipped packages
Address review feedback on #342:
- Restore cooldownSkip to a pure single-list function (SRP); the merge
into trusted_packages now happens in a separate mergeCooldownSkip step,
driven by the exported CooldownSkip wrapper.
- Extend CooldownSkipInfo with a CooldownSkipReason (TrustedPackage /
CooldownSkipList) on both SkipAll and per-version entries, so callers
can tell apart the broad waiver from the cooldown-only one. When both
lists match the same package, trusted_packages wins.
- Add audit.LogCooldownSkipped and emit it from the npm and PyPI
interceptors on the SkipAll path, alongside the existing info log,
carrying the source list as the reason.
* refactor(cooldown): inline list merge, audit per-version exemptions
Address further review feedback:
- Drop the separate mergeCooldownSkip helper; cooldownSkip now writes
into a shared *CooldownSkipInfo and is called twice from CooldownSkip
(cooldown skip list first, trusted_packages on top so trusted entries
override the reason on overlap).
- Audit log every exemption, not just SkipAll: a new auditCooldownSkip
helper in proxy/interceptors/cooldown.go emits one event per match
(package-wide or per-version), each tagged with its source list.
LogCooldownSkipped gains a version argument for the per-version case.
- Cover the trusted_packages reason path in TestCooldownSkip.
* fix(cooldown): avoid double-auditing trusted package exemptions
auditCooldownSkip now only emits EventTypeCooldownSkipped for entries
that came from dependency_cooldown.skip. Trusted-package exemptions
already get an EventTypeInstallTrustedAllowed event at tarball-download
time (proxy/interceptors/base_registry.go), so emitting a cooldown event
for them too would double-count the same waiver.
* emit trusted and cooldown skip events to cloud
* fix tests
* refactor(cooldown): return value from collectCooldownSkip, short-circuit on trusted SkipAll
Address PR review feedback:
- Rename cooldownSkip to collectCooldownSkip and return CooldownSkipInfo
instead of mutating an input pointer.
- Add mergeCooldownSkip to combine per-list results with trusted_packages
taking precedence on overlap.
- CooldownSkip now consults trusted_packages first and returns immediately
on a package-wide trusted exemption (DC skip list cannot add anything).
- Extend tests to cover disjoint pinned entries across both lists and the
case where DC version-less subsumes a trusted pinned entry.
* fix(audit): address cooldown review feedback
* fix(cooldown): audit cooldown skips at download time with concrete version
Backend rejects PackageVersion messages without a version, and audit logs
should reflect the runtime fact (a specific version was skipped) rather
than the config rule. Move the audit emission from metadata-request
handling to download-request handling, where the concrete version is
known, and require version in LogCooldownSkipped.
* chore(audit): drop dead scope assignment in LogCooldownSkipped
* refactor(cooldown): move skip-list logic into cooldown handlers
Registry interceptors no longer compute CooldownSkip or branch on SkipAll;
they just call HandleMetadataRequest. The npm and pypi cooldown handlers
own the skip lookup, the package-wide exemption short-circuit, and (for
pypi) the canonical-name denormalization. Also align LogCooldownSkipped
with other LogXxx signatures by taking *packagev1.PackageVersion.
* fix: Simplify audit logging for dependency cooldown skip
* refactor: Simplify cooldown handling and maintain separation of concepts for trusted and DC skip packages
* fix: Code review fixes
* fix: Emit cooldown skipped audit event ONLY when an in-window version is skipped
---------
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
The runtime --sandbox-allow CLI override path never expanded the supported
sandbox variables (${CWD}, ${HOME}, ${TMPDIR}), so a value like
write='${CWD}/**' was treated as a literal path segment and the allow rule
never matched. Profile-loaded sandbox paths already expand these via
sandbox/util.ExpandVariables.
Expand the variables in resolveToAbsolute, the shared chokepoint for
read/write/exec overrides, before resolving to an absolute path. Glob
characters are preserved through expansion and filepath.Clean.
Fixes#257
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Let dependency cooldown respect an explicit skip list so first-party /
internal packages that must be installed the moment they are published
(e.g. to sanity-test a freshly released version) are not held back by the
cooldown window.
Per review, this is a per-control skip list — NOT a second definition of
"trusted package". There remains a single top-level `trusted_packages`
(which waives malware analysis); `dependency_cooldown.skip` waives ONLY
the cooldown wait, so a fast-tracked package is still malware-scanned.
Matching:
- a PURL without a version skips cooldown for all versions of the package
(package-level) — the metadata passes through unmodified;
- a PURL with a version skips cooldown for that version only — that
version is preserved during stripping while other recent versions are
still held.
- config: DependencyCooldownConfig.Skip + CooldownSkip()/CooldownSkipInfo.
- npm/pypi interceptors: bypass on package-level skip; thread per-version
exemptions into the cooldown stripper so pinned versions survive.
- docs + config template; unit tests for the matcher (package/version
level, precedence, mismatches) and the skip-vs-trusted independence.
Signed-off-by: dmdhrumilmistry <56185972+dmdhrumilmistry@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(proxy): do not override explicitly-set CI env var
pmg forces CI=true for non-interactive (non-PTY) proxy runs so package
managers behave non-interactively. This clobbered a CI value the user set
explicitly (e.g. CI=false on a build server), changing downstream tool
behavior unexpectedly.
Only inject CI=true when CI is not already present in the environment,
preserving the user's intent. mergeEnv override semantics are left intact
since other overrides (HTTP_PROXY, etc.) must clobber.
Fixes#335
* test(proxy): snapshot/restore CI env explicitly in override test
Address review feedback: make the unset-CI subtest's intent explicit by
snapshotting the original CI value, unsetting it for the test, and
restoring it in t.Cleanup instead of relying on t.Setenv cleanup.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(sandbox): support pnpm workspaces and macOS cache dir in pnpm profile
pnpm in a workspace (monorepo) creates a node_modules directory inside
every workspace package to symlink direct dependencies. The profile only
allowed writes to the root node_modules, so installs failed with EPERM
on mkdir of e.g. apps/mobile/node_modules.
pnpm on macOS also writes its cache (lockfile verification, metadata)
under ~/Library/Caches/pnpm, while the base profile only covers the XDG
path ~/.cache/pnpm.
Fixes are scoped to the pnpm leaf profile, not the shared
npm-restrictive base.
Ref: https://github.com/safedep/pmg/issues/329
* fix(sandbox): emit regex parent rule for nested-glob allow patterns on Seatbelt
For allow patterns ending in /**, the translator auto-allows the parent
directory so mkdir/stat of the directory itself succeeds. The rule was
always emitted as a literal, which can never match when the parent still
contains glob characters (e.g. ${CWD}/**/node_modules from a workspace
allowance) — silently leaving the directory's own creation denied.
Emit a regex rule for glob-bearing parents instead. This stays strictly
narrower than the Linux drivers (Bubblewrap binds the prefix before the
first /** read-write; Landlock grants the glob expansion or its parent),
and deny rules are emitted after allows, so mandatory credential denies
still override.
Ref: https://github.com/safedep/pmg/issues/329
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(pty): treat background jobs as non-interactive to avoid SIGTTOU stop
IsInteractiveTerminal only checked that stdin/stdout are TTYs. A background
job (pmg npm run test &) still has the TTY on stdin/stdout, so pmg picked
PTY mode and called tcsetattr to enter raw mode. Changing terminal modes
from a background process group makes the kernel stop the process with
SIGTTOU, leaving the job hanging in Stopped state.
Check that the process group is the terminal's foreground process group
(tcgetpgrp == getpgrp) before treating the terminal as interactive, so
background jobs fall through to direct execution.
Fixes#322https://claude.ai/code/session_01PBBo5CKkzg68MMGrgCTQcY
* test(pty): fail on output copy timeout to avoid racy buffer read
Reading the output buffer after a silent select timeout races with the
io.Copy goroutine still writing to it. Fail the test on timeout instead.
Also fix a grammar nit in the IsInteractiveTerminal doc comment.
https://claude.ai/code/session_01PBBo5CKkzg68MMGrgCTQcY
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(shim): recognize shims at arbitrary paths via PMG_SHIM_PATH
The recursion guard in FilterPMGFromPath hardcoded the `/.pmg/bin`
suffix, so shims placed anywhere else (e.g. `/usr/local/lib/pmg/bin`,
`/shims`, or any future system-wide location) would not be stripped
from PATH when PMG resolved the real package manager. The shim would
resolve back to itself and PMG would re-exec it in an infinite loop.
This blocks moving shims out of `~/.pmg/bin` — needed for a future
`pmg setup install --system` (#317) — and also any user attempt to
relocate shims manually.
Have the shim export its own path before exec'ing pmg, and let the
filter use that to strip the exact dir at runtime. Keep the legacy
suffix check as a fallback so already-installed shims keep working
until they are regenerated.
Also drop `PMG_SHIM_PATH` from the env passed to the real package
manager so child processes don't inherit a stale marker.
* docs(shim): clarify PMG_SHIM_PATH is internal and unsupported to set manually
* remove comment
* update comment
* fix(sandbox): bind parent dir for globstar allow_write on bwrap
Fine-grained per-path mounts under read-only project binds broke pip
install into in-project .venv directories. Always mount the parent tree
for ** write rules instead.
Fixes#315
* test(sandbox): tighten globstar bind assertions and ensure ~/.npm exists for e2e
Strengthen TestBubblewrapAllowWriteGlobstarBindsParentOnly to verify the
parent dir is writably bound and the child path is read-only bound, not
just substring presence. Pre-create ~/.npm in the e2e harness so
bubblewrap --bind-try does not skip the npm cache dir on fresh runners.
* switch pnpm to /tmp in sandbox e2e
* test(sandbox): update glob ** test for parent-bind semantics
Globstar allow_write now binds the parent dir only (e2e740d), so the
test should assert the parent is writably bound and child subdirs are
not individually bound, instead of substring-matching subdir names.
* fix(sandbox): bind correct base dir for in-pattern globstar allow_write
Globstar allow_write previously used extractGlobParentDir, which walks past
the first ** and yields the wrong root for patterns like /a/b/**/d/**/e.
Introduce extractGlobstarWriteBaseDir, which takes the prefix before the
first /**, and use it in processWriteRule. Also dedup the coarse-fallback
parent-bind loop to mirror the read-rule fallback.
* fix(proxy): harden MITM proxy reliability and scale for bulk installs
Deep-dive analysis of dropped connections during large installs (5000+
packages with concurrent downloads) surfaced three issues, each verified
with a reproduction test before fixing.
1. Transient upstream errors tore down whole keep-alive tunnels.
goproxy returns false (closing the entire MITM client tunnel) when a
single upstream round-trip errors. Under load, CDN-fronted registries
(e.g. Cloudflare for registry.npmjs.org) intermittently reset
connections, so one transient reset dropped a pooled keep-alive socket
and surfaced to the package manager as ECONNRESET / "socket hang up".
Fix: route upstream round-trips through a resilient round tripper that
retries idempotent, body-less requests with bounded linear backoff,
absorbing transient resets and keeping the tunnel alive. A reproduction
test shows the tunnel count drop from 3 to 1 across a transient failure.
2. Head-of-line amplification against the external analysis service.
Concurrent requests for the same package version each issued their own
gRPC call. Fix: de-duplicate in-flight analyses with singleflight so a
burst of identical requests collapses into one upstream call.
3. Per-request goproxy verbose logging on the hot path.
proxy.Verbose was always on, formatting several log lines per request
even when discarded below debug level. Fix: enable goproxy verbose
logging only when PMG runs at debug level.
Note: the hypothesis that the http.Server Read/WriteTimeout leaks onto
hijacked CONNECT tunnels was investigated and disproven (Go clears the
deadlines on Hijack); the behavioral guard tests for long-lived
connections and slow transfers are retained.
* fix: Type assertion error handling
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: authenticated Malysis analyzer with tenant exclusion support
When SafeDep Cloud credentials are available (keychain or environment),
PMG now uses an authenticated malware analysis query against
api.safedep.io instead of the unauthenticated community endpoint
(community-api.safedep.io). The API key and tenant ID are supplied via
the gRPC connection.
The authenticated response may carry a tenant-specific malicious package
exclusion. This is honored as an opt-in trust signal: a flagged package
is downgraded to allow only when a concrete exclusion (non-empty ID) is
present for the exact package version queried. Exclusions are never
honored for community queries and never weaken the verdict for packages
that were not flagged. Allowed-by-exclusion packages are surfaced as a
warning so the trust decision is never silent.
Changes are additive; non-authenticated usage is unchanged. Credential
resolution is extracted into internal/cloudauth and reused by both the
analyzer factory and the existing cloud sync client.
* fix: surface tenant exclusions in proxy mode; clarify comments
- Warn when proxy interceptor allows a flagged package due to a tenant
exclusion, matching the guard flow so the trust decision is not silent.
- Remove stray doc comment above warnIfExcluded.
- Clarify that a verified-malware verdict can be downgraded by an
exclusion in applyExclusion.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add CloudSinkEnvResolver interface with default implementation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add GitHub Actions environment resolver for cloud sink
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: populate invocation context with CI environment on cloud events
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address lint errors in cloud sink tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: use getter-based CloudSinkCIResolver with nil-when-no-CI
Rename to CloudSinkCIResolver with focused CI concern. Factory returns
nil when no CI is detected, removing the need for IsCI() and a default
resolver. Leaves room for a separate agent resolver in the future.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add CI metadata support using updated API SDK
Update SDK to include SetMetadata on EndpointCIContext. Add Metadata()
to CloudSinkCIResolver interface and GitHub Actions implementation
(workflow, job, run_attempt, server_url). Wire metadata into
buildInvocationContext.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: address review comments on CI resolver
- Inject CloudSinkCIResolver as dependency into newCloudSink for testability
- Check both GITHUB_ACTIONS and GITHUB_RUN_ID for GHA environment detection
- Make factory and constructor package-private (newCloudSinkCIResolver,
newGithubActionsCIResolver)
- Attach invocation context only to session complete events, not every event
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: fail fast on os.Getwd error instead of swallowing it
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: avoid resolving symlinks in shim PMG binary path
Shims hardcoded the resolved Cellar path (e.g.
/opt/homebrew/Cellar/pmg/0.16.0/bin/pmg) instead of the stable
Homebrew symlink. This broke all shims after `brew upgrade` since
the old Cellar directory is removed.
Fixes#302
* fix pip test pkg version
PMG was forcing Accept: application/vnd.pypi.simple.v1+json on all
Simple API requests regardless of client capability. Older pip versions
(< 22.3) that only understand text/html would reject the JSON response,
breaking installs entirely.
Now checks the client's original Accept header before applying cooldown.
If PEP 691 is not supported, the request passes through unchanged with
a warning log recommending pip upgrade.
* feat: add doctor check runner core types and logic
* feat: add doctor checks for config, binary, directory, and aliases
* feat: add doctor checks for sandbox and security features
* feat: add protection verification check using test malicious packages
* feat: add summarized package manager availability check
* feat: add pmg setup doctor command with compact output
* feat: add PATH shim verification to doctor command
* fix: improve doctor command UX and alias detection
- Capitalize all check messages for consistent output
- Dim passing checks, color warn/fail for visual clarity
- Silence empty Cobra error output on doctor failure
- Remove redundant pmg binary check (self-evident)
- Fix alias IsInstalled to skip commented-out source lines
- Improve protection failure message
* refactor: remove package manager availability check from doctor
* fix: handle os.RemoveAll error in doctor protection check
* refactor: use table layout for setup doctor, extract shared table renderer
Move renderTable, truncate, and visibleWidth helpers from cmd/sandbox
to internal/ui so both sandbox and setup doctor share them. Rewrite
setup doctor output to use the same table structure as sandbox doctor.
Fix VisibleWidth to count runes instead of bytes for correct alignment
with multi-byte UTF-8 characters.
* docs: add pmg setup doctor to README, remove manual verification step
* refactor: use constants for check names, rename and inline doctor helpers
Address PR review comments: extract check name constants, rename
CheckConfigFile to CheckFileExists and CheckDirectoryWritable to
CheckDirectoryExists for reusability, inline trivial wrappers
(CheckSandbox, CheckSecurityFeature, CheckProxyMode), and add
fix hints for all checks with correct config keys.
* refactor: inline simple doctor checks into command layer
* fix: skip protection check when aliases and shims are inactive
Protection checks now fail immediately when shell aliases and shims
are both inactive, instead of falsely passing by running through the
pmg binary directly. Also clean up summary messages to remove
redundant fix hints and truncated paths.
* fix(sandbox): classify helper-tool errors with usefulerror
Sandbox helper commands (profile lint/diff/show/init/list) used to bubble
up plain fmt.Errorf chains from the registry layer, which the TUI then
classified as Unknown and decorated with a bug-report link. Wrap each
error path at the cmd/sandbox boundary so the TUI prints NotFound,
InvalidArgument, or PermissionDenied with actionable hints instead.
Closes#269
* refactor(sandbox): classify registry errors via sentinel wrapping
Replace the fragile substring match in profileLoadError with errors.Is
against new sandbox.ErrProfileNotFound / sandbox.ErrProfileInvalid
sentinels. Every fmt.Errorf in registry.go that previously communicated
"missing" or "malformed" by message text now wraps the corresponding
sentinel, so the cmd layer can classify without inspecting strings.
* fix(sandbox): detect IO error class when wrapping helper errors
Replace static ErrCodeUnknown / ErrCodePermissionDenied wrappings with
ioErrorCode, which inspects the error chain for fs.ErrPermission and
fs.ErrNotExist before falling back. Applied to runProfileList (where an
unreadable user profile directory now classifies as PermissionDenied),
registryInitError, and the stat/MkdirAll/WriteFile paths in profile init.
Also drop redundant doc comments on helpers whose names are self-evident.
---------
Co-authored-by: Claude <noreply@anthropic.com>