6 Commits
Author SHA1 Message Date
dc0e3202cc feat: Linux system-wide setup (pmg setup install --system) (#377)
* 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>
2026-07-14 21:34:22 +05:30
02965143d0 fix: fail fast when package manager is not installed (#360)
* 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>
2026-07-06 18:31:18 +05:30
Sahil BansalandGitHub 141894ed8f fix(shim): recognize shims at arbitrary paths via PMG_SHIM_PATH (#323)
* 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
2026-06-09 20:33:14 +05:30
Sahil BansalandGitHub 19b9f2ca1f fix: avoid resolving symlinks in shim PMG binary path (#303)
* 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
2026-05-27 16:00:43 +05:30
Abhisek DattaandGitHub b15ce33fe4 fix: MacOS MDM based Deployment (#277)
* fix: MacOS MDM deployment script

* fix: Handle shell alias for bash on macos

* fix: Code review fixes

* fix: Code review fixes

* feat: Add support for global config file

* feat: Add support for global config file

* fix: Code review fixes

* fix: Avoid blocking CLI for analytics flush
2026-05-21 14:32:30 +05:30
6546116e28 feat: migrate PMG to use PATH shims for package manager wrapper (#246)
* 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>
2026-05-12 22:27:05 +05:30