* feat(sandbox): report landlock seccomp denials via pmg sandbox violations
The landlock driver's seccomp supervisor already emitted structured deny
events over the audit socket, but the driver drained them to io.Discard, so
the violation cache was never populated on Linux and violations list /
explain always came up empty.
Capture the events at the driver, enrich them with access mode and process
name, and implement BestEffortViolation mirroring the seatbelt reporter:
failure-only collection, seccomp_deny events only, (kind, target) dedupe.
The platform-neutral cache/list/explain pipeline picks it up unchanged.
Only the seccomp deny-list layer is observable; denials made by the
Landlock LSM itself (allow-list boundary, delete/rename, network) fail
in-kernel with no userspace signal and are documented as out of scope.
Also make the explain renderer driver-neutral: the raw-log label was
hardcoded as "Seatbelt log" and an empty correlation ID printed a blank
value.
* fix: address review findings on landlock violation reporting
Report the deny rule that fired, not the requested access: an O_RDWR open
denied by a read-only rule now surfaces as a read denial with an effective
override suggestion (allow write= prunes only deny_write). The matched rule
path is emitted as rule_path and mapped to RuleTarget, bringing the
"Matched rule:" line to parity with seatbelt.
Dedupe deny events by (kind, path) at capture time so a retry loop on one
denied path cannot fill the buffer and evict a later distinct denial; the
cap now bounds distinct denials.
Stamp deny events with a timestamp (they rendered "ts":0 in the raw log)
and default unknown syscalls to generic_deny instead of fs_write.
* refactor: single source for the deny dedupe key
Capture-time and extract-time dedupe must agree on what identifies a
denial; building the key in two places risks them drifting apart.
* fix: bound the capture dedupe map by marking keys only on append
seen grew for every distinct deny key even after the buffer was full,
and keys carry attacker-chosen path bytes — a hostile process looping
over crafted unique denied paths could grow the pmg parent's memory for
the run's duration, defeating the cap. Marking keys only when the event
is appended bounds the map at the cap and keeps the one-time drop
warning reachable for distinct denials past it.
* docs(sandbox): AppArmor userns fix for the Landlock driver on Ubuntu 23.10+
The shim fails with "install seccomp: ... permission denied" when
kernel.apparmor_restrict_unprivileged_userns=1. Document the per-binary
AppArmor profile as the recommended fix and the sysctl as the blunt
alternative.
* docs(sandbox): drop em dashes from the landlock sections
* fix(doctor): cover landlock in the AppArmor userns probe
The warn detail only named the bwrap failure and the only suggested fix
was the system-wide sysctl. Name the landlock shim error too and suggest
the per-binary AppArmor profile first, pointing at the new docs section.
* refactor: remove guard mode execution paths and guard-only packages
Guard (non-proxy) mode is removed; all package-manager commands now
always run the proxy flow. Removes the guard engine, the common flow,
the extractor package, the npm/pypi dependency resolvers and the
PackageResolver plumbing that only guard mode consumed.
The guard package retains only PackageManagerGuardInteraction, which
the proxy flow and confirmation interceptors reuse for user prompts.
Proxy behavior is unchanged.
* refactor: remove proxy opt-out surfaces, guard references in config, action and docs
Removes Config.ProxyMode, ProxyConfig.Enabled, IsProxyModeEnabled, the
proxy_mode legacy fallback, PMG_PROXY_ENABLED handling and the
--proxy-mode / --include-dev-dependencies flags. Proxy interception can
no longer be disabled.
Also removes the proxy-mode input from the GitHub Action, the
proxy-mode doctor check and setup info row, updates the E2E workflow to
stop passing --proxy-mode=false, and sweeps guard-mode wording from
docs and the config template.
The legacy proxy_install_only flat key and PMG_PROXY_INSTALL_ONLY env
var remain supported. audit.FlowTypeGuard is kept so previously
recorded audit events still translate for cloud sync.
* feat: fail loudly when a removed proxy opt-out is still configured
A leftover proxy.enabled: false / proxy_mode: false config key or
PMG_PROXY_ENABLED=false / PMG_PROXY_MODE=false env var previously meant
guard mode; silently ignoring it would switch those users to proxy
interception without notice. PMG now exits with an actionable error
naming the exact source. Precedence mirrors the old resolution order:
env (ignored under lockdown) > proxy.enabled > legacy proxy_mode.
The pmg config subtree is exempt so the config file can still be fixed
with pmg config edit/set. The GitHub Action's proxy-mode input is kept
as a tombstone that fails the action when set to false and warns
otherwise.
* refactor: extract flows.RunProxy and address review findings
Collapses the identical parse-then-run body duplicated across the 12
package manager commands into flows.RunProxy. Documents the cache-hit /
offline analysis trade-off versus the removed guard manifest path,
fixes a stale non-proxy label in the E2E workflow and a stale guard
reference in the uvx parser comment.
* fix(config): mirror old proxy opt-out precedence exactly
PMG_PROXY_MODE only ever took effect through the legacy fallback, which
was gated on the presence of a proxy: key in the config file (even a
null one). Promoting it to the top env tier caused two inversions: a
stale PMG_PROXY_MODE=false hard-failed configs that resolved to proxy
mode, and PMG_PROXY_MODE=true silently overrode an explicit
proxy.enabled: false file opt-out. The check now resolves in the old
order: PMG_PROXY_ENABLED > proxy: section (presence gates the legacy
tier) > PMG_PROXY_MODE > flat proxy_mode.
parseOptOutBool also accepts numeric values (0 = false) to match
viper's WeaklyTypedInput/cast.ToBool coercion, so proxy.enabled: 0 and
proxy_mode: 0 are detected as opt-outs.
* refactor: move package manager interaction out of guard
* refactor: trim package manager interaction
* fix(config): normalize config keys viper-style in proxy opt-out check
Viper resolved config file keys case-insensitively and expanded dotted
keys, so spellings like Proxy:, Enabled:, a literal proxy.enabled key or
Proxy_Mode selected guard mode before the removal. The opt-out check now
lowercases keys recursively and nests dotted keys before matching, so
those existing opt-outs fail loudly instead of being silently ignored.
* refactor: remove inert transitive controls, dead parser state and guard audit variant
transitive / transitive_depth lost their only consumers with the
dependency resolvers; remove the config fields, flags, template and doc
entries, and the report/audit plumbing that misreported transitive
analysis as enabled.
Remove write-only parser state (PackageInstallTarget.Extras,
ParsedCommand.ManifestFiles, ShouldExtractFromManifest); IsManifestInstall
stays as it feeds sandbox gating via IsInstallationCommand.
Remove audit.FlowTypeGuard and its cloud mapping; guard events recorded
by pre-removal versions in an unsynced WAL translate to UNSPECIFIED.
* fix: address review findings on the opt-out wiring and cleanups
Move the removed-opt-out rejection from the CLI PersistentPreRun into
proxyFlow.Run: the check now fires exactly for package-manager runs, so
non-install commands (pmg setup remove, doctor, config, version) stay
usable to fix or remove an opted-out installation, and future commands
inherit or avoid the check by construction instead of by exemption list.
Also: make the e2e malicious-package assertion actually fail the job
when an install is not blocked, route pmg go through flows.RunProxy,
and drop the dead extras return from pypiParsePackageInfo (extras are
still stripped from package names).
* fix(config): make the removed opt-out check faithful to the old resolution
The gate that silenced the legacy proxy_mode surfaces matched the raw
proxy key case-sensitively in the old code, while values resolved
viper-style (case-insensitive, dotted keys); applying each semantic
where the old code did fixes both divergences: a case-variant Proxy:
section no longer hides a flat proxy_mode: false opt-out, and a dotted
proxy.enabled: false overridden by proxy_mode: true no longer errors.
Replace the generic key-tree normalization with two targeted lookups
(the check only ever resolves proxy.enabled and proxy_mode), which also
makes colliding spellings resolve deterministically. Coerce legacy-tier
values cast.ToBool-style so PMG_PROXY_MODE=off style opt-outs are
detected, log the config read error instead of swallowing it, and
shorten the error to a one-line statement with the specific remedy in
the help text.
Add lockdown coverage (env inert both directions) and a repeated-run
determinism test.
* fix(config): fall back to defaults for unrecognized proxy opt-out values
The old loader swallowed viper errors and ran on defaults, so values
like proxy.enabled: yes or PMG_PROXY_ENABLED=banana silently discarded
the whole config and defaulted to proxy. Treat them the same way now:
unrecognized values mean the default (proxy on) instead of a hard
error, and the doc comment no longer claims the old loader failed
loudly. Only values that actually meant guard mode fail.
Also check the removed opt-out before the CA trust check in pmg go,
restoring the old error precedence: a config problem must not steer
the user into an unnecessary OS trust store change.
* fix(e2e): PMG_PROXY_MODE assertion must match the legacy gate semantics
The runner's setup step writes the template config, which has a proxy:
section — and with one present the legacy PMG_PROXY_MODE was always
inert, so expecting a loud failure there asserts pre-fidelity-fix
behavior. Assert both sides instead: inert (command succeeds) with the
standard config, loud failure against an empty config dir where the
legacy fallback actually applied.
* refactor(config): collapse parseOptOutBool to ParseBool over the string form
YAML hands us typed values (bool, int), so route them through
fmt.Sprintf %v and strconv.ParseBool instead of a per-type switch.
Identical behavior for every recognized value; numbers other than 0/1
now read as no opinion instead of cast.ToBool's nonzero-true, which no
real config relies on.
`pmg setup doctor` reported npm protection as OK on a machine with no npm
installed. Two compounding defects:
1. The availability gate used a plain `exec.LookPath`, which resolves the PMG
shim on PATH rather than the real binary, so the "skip" branch never fired.
It now uses `shim.ResolveRealBinary` (PATH with shim dirs stripped), matching
the runner, so a missing real binary correctly yields WARN "not available".
2. The result was inferred purely from a non-zero exit, so PackageManagerNotFound
(exit 127) and other failures were misread as a successful block. The check
now requires PMG's block headline in the captured output before reporting
PASS; other non-zero exits report WARN with the error surfaced.
The block headline is extracted into `ui.MalwareBlockedHeadline` so the doctor's
marker stays in sync with what PMG prints across its block-output sites.
* feat: add Linux system-wide setup
Install shared shims, managed configuration, and login-shell PATH integration so golden images and multi-user hosts can protect package installs for every user.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: keep local design documents untracked
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden and simplify Linux system install
Tighten shim detection, profile repair, and install ordering while
trimming over-specific doctor/info hints from the system-install path.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: clarify system-install doctor alias and shim path checks
Use UserBinDir for PATH checks and pass aliases as not required under
system install without treating that as active interception.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: tighten event-log soft-fail warning prefix
Prefix the warning with [pmg] and drop the redundant continuing clause.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: add Linux system-install e2e and pin pnpm for add flake
Cover root system setup, PATH/profile.d, managed config, non-root
interception, and remove. Pin pnpm 11.10.0 on the package-manager e2e
job after an integrity crash on pnpm add.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: bump packageManager to pnpm 11.10.0 for e2e
Align package.json with the pnpm version we want in CI so action-setup
stops erroring on a version mismatch after the e2e integrity flake.
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: use npm init for pnpm e2e to avoid integrity crash
pnpm 11.x `pnpm init` still writes onFail:download; `pnpm add` then
fails after PMG analysis even on 11.10.0. Seed the temp package with
npm init instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: revert packageManager pin to pnpm 11.1.3
The e2e integrity crash is avoided by npm init; the 11.10.0 bump is
no longer needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden system-install review findings
Require root-owned, non-group/other-writable pmg for --system install;
allow remove without that validation. Doctor checks npm resolution for
PATH precedence, uses ImpliesInterception instead of message matching,
and documents version-manager shadowing. Pass profile bin dir from the
shim manager and note that system config ignores per-user files.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden doctor PATH checks and attribute cloud events by OS user
Doctor now verifies every installed package manager against the shim
directory, and system-install validation only requires a safe parent
directory. Cloud sync records username/uid on invocation context for
multi-user hosts sharing one endpoint.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address system-install review findings
- shim: make system executable resolution injectable so tests pass under
umask 002; skip the root-owner test when running as root
- doctor: treat resolution into either the system or per-user shim dir as
intercepted, and collapse the shim-in-PATH check to a single call site
- setup: make remove (both --system and per-user) best-effort with
errors.Join so one failed step no longer strands the other artifact
- shim: allow a group-writable install parent dir (Debian/Ubuntu ship
/usr/local/bin as root:staff 2775) while still rejecting world-writable
and non-root-owned parents
- audit: attribute cloud events to SUDO_USER when running under sudo
- docs: drop the soft-fail event-logging claim (hard-fail is retained)
* ci: normalize /usr/local/bin perms before system-install e2e
The GitHub ubuntu-latest runner ships /usr/local/bin world-writable so
tooling can install without sudo. System install correctly refuses a
world-writable dir for the shared binary (any local user could replace
it and hijack every user's npm/pip). No FHS-compliant distro or Docker
image ships it world-writable — it is always root:root 0755 or
root:staff 2775 — so this normalizes only the anomalous CI runner back
to standard perms and still exercises the real /usr/local/bin path.
* fix: actionable remedy for root-created per-user config dir
A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su
without -) creates the invoking user's ~/.config/safedep as root-owned,
and event-log init then fail-closes every later non-root command.
Make that state self-solvable:
- event-log init permission errors exit with a usefulerror naming the
likely cause and the chown fix instead of a bare fatal
- pmg setup doctor probes event-log dir writability and reports the
same fix via a new per-result Fix override
- document the mechanism and remedy in system-install.md, along with
the binary ownership requirements for --system
- consolidate this branch's doctor tests into doctor_test.go
* fix: resolve per-user paths from root's own home when running as root
Path resolution trusted HOME (and XDG_*), which sudo and su can preserve
from the invoking user (GitHub runners, sudo -E, su without -). Any pmg
run as root then created root-owned ~/.config/safedep inside that user's
home, and event-log init fail-closed every later non-root pmg/npm/pip
run for them. System install made sudo pmg the documented flow, turning
this latent bug into the happy path.
When euid is 0, configDir and cacheDir now resolve from root's passwd
home instead of the environment, so root state lands under /root and
user homes are never touched. PMG_CONFIG_DIR/PMG_CACHE_DIR still win,
non-root resolution is unchanged, and Windows is unaffected (no euid).
Event-log init stays fatal on failure; sudo-run package events are
attributed via SUDO_USER and synced by the exit auto-sync as usual.
E2E: GitHub runners preserve HOME under sudo, so assert that no sudo
pmg run leaks state into the runner's home, and that the managed-config
refusal fails for the documented reason rather than a permission brick.
* fix: triage the unwritable config dir remedy by cause
The chown hint is only correct when another account created files
inside the current user's own home. When a leaked HOME or
XDG_CONFIG_HOME points at another user's home (e.g. sudo -u on GitHub
runners), following it would chown that user's directory and brick
their pmg instead. Classify the failure against the passwd home,
which the leaked environment cannot influence, and prescribe:
- dir inside own home: restore ownership with chown
- dir outside own home: fix the leaked environment, never chown
- explicit PMG_CONFIG_DIR: make it writable
Used by both the fatal event-log error and the doctor check, and the
docs troubleshooting now carries the same two-case triage.
* ci: pin XDG_CONFIG_HOME for the cross-user e2e step; terse doctor fix
GitHub runners export XDG_CONFIG_HOME=/home/runner/.config and it leaks
through sudo -u, so the pmgtest pmg resolved the runner user's config
dir and fail-closed on its runner-owned log file (run 29289868727 shows
the triaged error catching exactly this). Set it inside the login shell
so it wins regardless of how the leak is delivered.
The remedy now returns a full-help and doctor-table pair from a single
triage, and drops the do-not-chown tail from the leak message.
* fix: adapt event-log error to the two-value remedy signature
Belongs with the previous commit; it was left unstaged and e0580b1
does not compile without it.
* fix: fall back to env path resolution when root has no passwd entry
Running as uid 0 without a resolvable root passwd entry (scratch
containers, minimal chroots) panicked at startup on every command,
because the euid-based path resolution had no fallback. Fall back to
env-derived resolution there: without a passwd database there is no
user switching, so the cross-user poisoning that branch prevents
cannot occur.
Also restore the underlying cause in the generic event-log init error
(minimal output hid it after the usefulerror change), and document
that root's per-user data lives under /root regardless of a preserved
HOME.
* fix: gate SUDO_USER trust and root path diversion; add doctor binary check
Address review findings on the system-install PR:
- cloud_sink: honor SUDO_USER for audit attribution only when euid==0.
Without the gate any user could set SUDO_USER and spoof cloud-audit
attribution to another account. Matches the guard in cmd/setup/cert.go.
- config: divert per-user paths to root's passwd home only on an actual
sudo elevation (euid==0 && SUDO_USER set), not for every root euid.
The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently
stopped reading genuine root users' config (golden Docker images),
regressing two tests that only fail when the suite runs as root.
Genuine root honors the environment as before; su without - leaves no
marker and stays a documented, loud-failing residual.
- doctor: add a system-only check re-validating that the binary the
installed shims exec is still root-owned and non-writable, catching
permission/ownership drift after install.
- shim: fold the duplicated shim-scan loop into firstShimContent.
* fix: harden system dirs at install; keep sudo attribution without passwd
Address remaining review comments:
- shim: force root:root 0755 on the managed system dirs (shim tree and
profile.d) after MkdirAll. A pre-created dir with weaker ownership,
possible under Debian's group-writable /usr/local/lib, would let a
non-root user replace the shims every account executes.
- audit: when SUDO_USER has no passwd entry (minimal containers),
attribute cloud events from sudo's recorded SUDO_USER/SUDO_UID env
instead of falling back to root. Still gated on euid 0.
- setup: reword the root-without---system warning; alias/shim install
follows HOME, so claiming it configures only root's home was wrong.
- shim: skip the non-root-owner validation test on Windows, where file
ownership is not resolvable.
* fix: reject system binaries unreachable by other users; consistent info
The system-install validation checked the binary's own permissions and
the parent's tamper-safety but never reachability: a 0755 root-owned
binary under a 0700 directory (e.g. /root/pmg) passed every check while
every non-root user's shim failed with exit 127. Walk the directory
chain to / and require the search bit for others; doctor's system
binary check inherits this. E2E gains a reject case for a binary under
a non-searchable directory.
setup info: render alias/user-shim/system-shim rows through one
installed-state formatter (location when installed, "not installed"
otherwise) instead of a mix of booleans, paths, and prose.
* fix: stop reowning /etc/profile.d; document group-writable and su gaps
writeSystemProfile chowned/chmod'd /etc/profile.d itself, a shared
system directory pmg does not own, silently overriding any perms a
sysadmin set on it. Secure only the file pmg writes (pmg.sh) via
secureSystemFile, which also forces 0644 explicitly so a restrictive
root umask cannot leave the snippet non-world-readable (which would
drop the shim dir from other users' login-shell PATH).
Docs: add Limitations entries for the group-writable install dir
bypass (validation is defeatable on non-sticky group-writable dirs
like Debian's /usr/local/bin) and the elevation-only scope (su without
- can still poison the caller's home; sudo -u cannot poison another
account). Trim the requireSafeParentDir comment to a pointer.
* refactor: separate unwritable-dir diagnosis from remedy rendering
Address the open review threads on #376:
- rename realUserHomeDir to currentUserHomeDir and fail when the passwd
entry has no home directory
- split UnwritableConfigDirRemedy into classifyUnwritableDir (cause
diagnosis) and pure message rendering so each function has one job
- replace cmd/setup's duplicate pathIsUnderDir with the shared
config.PathWithinDir, now guarding empty inputs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A
* fix: repair umask-clamped modes on system shims and managed config
os.WriteFile and os.MkdirAll honor the process umask, so a hardened root
umask (e.g. 077) produced 0700 shims other users cannot execute and a
0600/0700 managed config non-root pmg runs cannot read - silently
disabling the system-wide policy. Chmod/chown the artifacts explicitly
after writing, with unix regression tests running under umask 077.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A
* refactor: address system-install maintainability review
- move PathWithinDir and the root-owned mkdir/chmod helpers into a new
internal/fsutil package; config no longer exports a generic fs util
- create managed-config directories with MkdirAllRootOwned, which only
sets ownership and mode on directories it creates - drops the
hardcoded safedep parent-dir heuristic and never touches pre-existing
directories
- collapse NewSystemShimManagerForRemove into NewSystemShimManager and
validate the executable in Install, where the action needs it; Remove
works regardless of binary state
- rename ShimConfig.ManageProfile to SystemProfile and document it
- name the Linux-only system paths linuxSystemBinDir/ProfilePath and
document the Unix-only validation semantics
- share the PMG_BIN shim variable name between writeShimScript and
parseShimPMGBin via the shimPMGBinVar constant
- cloud attribution falls back to the effective uid when SUDO_UID is
absent, so sudo-invoked commands are not misattributed to root
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A
---------
Co-authored-by: Sahilb315 <bansalsahil315@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
* docs(specs): add custom block messages and package blocklist spec
* docs(specs): add custom block messages and package blocklist implementation plan
* feat(config): add blocked_packages list and custom block messages
* feat(audit): add package_blocklist_blocked event and blocklist model
* feat(proxy): block blocklisted packages in the policy gate before analysis
* feat(guard): block blocklisted packages before trust skip and analysis
* feat(ui): render blocklist blocks and custom messages, fix silent-mode block output
* feat(proxy): append custom messages to malware and go-cooldown block bodies
* test(proxye2e): cover blocklist enforcement and custom block messages
* docs(specs): remove spec and plan documents
* refactor: drop guard-flow blocklist enforcement and trim docs
Guard mode is being deprecated; the blocklist is enforced in proxy mode
only. Remove the trusted_packages mirroring references outside the docs.
* refactor(config): consolidate blocklist and block message under top-level block section
Replace dependency_cooldown.message, malware.message and blocked_packages
with a single block section: block.message is appended to every block
output regardless of which control blocked, and block.packages is the
package blocklist.
* fix(ui): render block.message as info note with clean spacing
* fix(ui): indent wrapped continuation lines in block reasons and messages
* update config template
* refactor(config): replace block section with top-level advisory_message
Remove the package blocklist (will be implemented as part of policies in
the future) and replace block.message with an optional top-level
advisory_message appended to every block output.
* chore(config): move advisory_message near top-level scalar configs in template
* fix: fail fast with clear error when package manager is not installed
When PMG shims intercept a package manager that isn't installed (e.g. a
clean JAMF-provisioned laptop where PMG is set up before dev tooling),
real-binary resolution failed with a generic "unknown" error and a
bug-report link, making it look like PMG itself had crashed.
Introduce a typed BinaryNotFoundError that exits with code 127 (standard
"command not found") and maps to a new PackageManagerNotFound error code
with an actionable message instead of the Unknown classification.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: improve missing package manager help message
Use a concise, dynamic install hint instead of explaining PMG's
PATH forwarding internals.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: mention PATH in missing package manager help text
Covers the common case where a package manager is installed but its bin
directory is not on PATH yet (e.g. after curl | bash install).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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.
* feat: add FilterPMGFromPath utility for PATH shim recursion prevention
* feat: add FilterPMGFromEnv to filter PATH from env slices
* feat: filter ~/.pmg/bin from PATH in proxy subprocess env
* feat: add PathExport method to Shell interface for shim PATH integration
* feat: add ShimManager for PATH shim install/remove lifecycle
* feat: wire ShimManager into setup commands with --use-aliases fallback
* refactor: add DefaultShimConfig helper to reduce setup boilerplate
* fix: resolve real binary path to prevent shim double-invocation
exec.CommandContext resolves the binary using the current process PATH,
which still contains ~/.pmg/bin. This caused pmg to launch the shim
instead of the real package manager, resulting in a second pmg instance
with its own proxy — producing duplicate error messages and wasted work.
ResolveRealBinary searches a filtered PATH (without ~/.pmg/bin) to find
the real package manager binary before execution.
* fix: resolve real binary in runner.Execute and expand path resolution tests
Ensure guard mode and proxy skip paths also resolve through
ResolveRealBinary to prevent infinite shim recursion. Add table-driven
tests covering error cases, multi-binary PATH, and PATH restoration.
* fix: handle error return values from os.Setenv and file Close calls
Address errcheck lint failures: check os.Setenv returns in
ResolveRealBinary, and check f.Close/tempFile.Close in ShimManager.
* feat: auto-migrate shell aliases to PATH shims on setup install
When running `pmg setup install`, detect existing shell aliases and
automatically remove them before installing shims. Existing users
get a seamless migration with no extra flags or commands needed.
* fix: update E2E test to verify shim installation instead of alias RC file
Replace the .pmg.rc file check with assertions that ~/.pmg/bin/ exists
and contains executable shim scripts for npm and pip.
* feat: add FilterPMGFromPath utility for PATH shim recursion prevention
* feat: add FilterPMGFromEnv to filter PATH from env slices
* feat: filter ~/.pmg/bin from PATH in proxy subprocess env
* feat: add PathExport method to Shell interface for shim PATH integration
* feat: add ShimManager for PATH shim install/remove lifecycle
* feat: wire ShimManager into setup commands with --use-aliases fallback
* refactor: add DefaultShimConfig helper to reduce setup boilerplate
* fix: resolve real binary path to prevent shim double-invocation
exec.CommandContext resolves the binary using the current process PATH,
which still contains ~/.pmg/bin. This caused pmg to launch the shim
instead of the real package manager, resulting in a second pmg instance
with its own proxy — producing duplicate error messages and wasted work.
ResolveRealBinary searches a filtered PATH (without ~/.pmg/bin) to find
the real package manager binary before execution.
* fix: resolve real binary in runner.Execute and expand path resolution tests
Ensure guard mode and proxy skip paths also resolve through
ResolveRealBinary to prevent infinite shim recursion. Add table-driven
tests covering error cases, multi-binary PATH, and PATH restoration.
* fix: handle error return values from os.Setenv and file Close calls
Address errcheck lint failures: check os.Setenv returns in
ResolveRealBinary, and check f.Close/tempFile.Close in ShimManager.
* feat: auto-migrate shell aliases to PATH shims on setup install
When running `pmg setup install`, detect existing shell aliases and
automatically remove them before installing shims. Existing users
get a seamless migration with no extra flags or commands needed.
* fix: update E2E test to verify shim installation instead of alias RC file
Replace the .pmg.rc file check with assertions that ~/.pmg/bin/ exists
and contains executable shim scripts for npm and pip.
* feat: install both aliases and shims for full coverage
Aliases win in interactive shells (including venvs), shims catch
non-interactive contexts (IDEs, CI, subprocesses). Remove --use-aliases
flag and migration logic since both are always installed together.
Update E2E to verify all shim scripts and alias RC file.
* feat: address review feedback for shim implementation
- Install both aliases and shims together for full coverage
- Move homeDir resolution into NewDefaultShimManager (internal concern)
- Add mutex to ResolveRealBinary to guard against concurrent PATH mutation
- Use filepath.SplitList for platform-correct PATH splitting
- Add ResolveRealBinary to runner.Execute and proxy flow to prevent
shim recursion in all execution paths
- Remove print side-effects from ShimManager.Remove
- Update E2E to verify all shim scripts and alias RC file
- Expand ResolveRealBinary tests with table-driven cases
* fix: restore errcheck handling and add concurrency test for ResolveRealBinary
- Restore proper defer with log.Warnf for PATH restoration in ResolveRealBinary
- Restore errcheck handling for f.Close() and tempFile.Close() in ShimManager
- Add explanatory comment for ResolveRealBinary call in proxy_flow
- Add TestResolveRealBinaryConcurrent to verify mutex guards concurrent access
* feat: skip shell integration on Windows with informative warning
On Windows, pmg setup install now writes only the config file and
prints a warning that shell aliases and PATH shims require WSL.
* fix: PMG use pre-resolved binary path (#253)
---------
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
* feat: Add dependency cooldown for npm packages
Strip recently-published package versions from npm registry metadata
responses so npm's resolver naturally falls back to older versions.
Overrides the Accept header to force full packument responses (which
include the "time" field needed for publish-date checks).
Reports cooldown blocks only when all versions are stripped (remaining == 0),
matching npm's --min-release-age behavior for silent fallback.
* fix: Report oldest version in cooldown block (shortest wait)
When all versions are blocked by cooldown, report the oldest version
since it exits the cooldown window first — giving the user the
shortest wait time instead of the longest.
* fix: Handle resp.Body.Close error return for errcheck linter
* test: Add dependency cooldown assertions to template config tests
* fix: config template for dependency cooldown
* fix: Prevent npm from caching cooldown-stripped metadata responses
* fix: Restore body on ReadAll failure and log Close errors in response modifier
* fix: Close response body before replacing to prevent connection leak
* fix: Correct daysLeft ceiling math and update ContentLength on error recovery
* fix: Clear Status on status code change and update ContentLength in modifier error path
* refactor: address review comments on dependency cooldown PR
- Make NpmCooldownHandler and constructor package-private
- Pass cooldown days as parameter instead of reading config internally
- Convert standalone functions to methods on npmCooldownHandler
- Set Accept-Encoding: identity to prevent gzip responses breaking JSON parsing
- Return 503 with descriptive message when upstream body read fails
* fix: log errors in stripCooldownVersions instead of swallowing them
* fix: Config preserve fallback defaults
* fix: Code review fixes
* fix: correct cooldown tip to show wait time instead of incorrect trusted_packages advice
* fix: prevent integer overflow in cooldown duration calculation with large days values
* refactor: deduplicate CooldownBlock into internal/models, fix misleading variable names
- Move CooldownBlock struct to internal/models to eliminate duplication
between proxy/interceptors and internal/ui packages
- Simplify proxy_flow.go by using direct assignment instead of field copy
- Rename latestStripped/latestDate to oldestVer/oldestDate for clarity
* fix: Dependency Cooldown Check Encapsulation (#207)
* fix: Encapsulate cooldown check
* feat: Add --skip-dependency-cooldown override
* fix: Code review fixes
---------
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
The spinner goroutine was writing carriage-return + status text to
os.Stdout every 100ms. In the non-interactive TTY path, the child
process also writes directly to os.Stdout, causing both writers to
race on the same file descriptor. The \r emitted by the spinner
resets the cursor to column 0 mid-line, corrupting and truncating
the child process output.
Fix by writing all spinner/status output to os.Stderr, which is the
standard Unix convention for diagnostic and status messages. This is
also consistent with how progress.go and ShowWarning already behave.
https://claude.ai/code/session_012jMiRSS4Jx9Bs7a2F4S6KN
Co-authored-by: Claude <noreply@anthropic.com>
* provide package blocked feedback in proxy mode
* update test case & clear status on block
* refactor reporting
* clearStatus on `continueExecution`
* set spinnerChan to nil
* add sync for spinner
* define contract for package executors
* introduce npx executor
* add npx and pnpx cmd support
* fix typo
* rm PackageExecutor and depend on PackageManager interface
* add support for PTY to handle parent-child process interaction
* refactor PTY handling in proxy flow
* enforce interactiveSession interface check
* close reader explicitly and clean npm version for pkg executors
* rm interaction from interceptors
* add docs and wait for outputRouter before exit
* add support for non interactive TTY for proxy mode
* add support for CI env var check for non interactive tty proxy mode
* update readme to include npx, pnpx support
* Update internal/flows/proxy_flow.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
* update ptyx lib
* fix docs typo
---------
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>