Skip to content

feat: Linux system-wide setup (pmg setup install --system)#377

Merged
Sahilb315 merged 26 commits into
mainfrom
feat/add-linux-system-install-support
Jul 14, 2026
Merged

feat: Linux system-wide setup (pmg setup install --system)#377
Sahilb315 merged 26 commits into
mainfrom
feat/add-linux-system-install-support

Conversation

@abhisek

@abhisek abhisek commented Jul 14, 2026

Copy link
Copy Markdown
Member

Closes #376

Summary

Adds a root-only Linux system install so one machine or image protects every user account (shared VMs, golden Docker images, CI runners):

  • pmg setup install --system / pmg setup remove --system: shared shims under /usr/local/lib/pmg/bin, managed config at /etc/safedep/pmg/config.yml, PATH via /etc/profile.d/pmg.sh.
  • pmg setup doctor and pmg setup info understand both install planes; doctor verifies package managers actually resolve to a shim (catches nvm/pyenv-style PATH shadowing) and re-validates the system binary's ownership/permissions to detect post-install drift.
  • Docs: docs/system-install.md covering artifacts, Docker ENV PATH usage, limitations (virtualenv, version managers, managed config), and permission remedies.

Supersedes #376; includes all review feedback from that thread.

Security posture

  • The system binary is validated at install: root-owned, world-executable, not group/other-writable, root-owned non-world-writable parent, and searchable by all users along the path. Managed directories are forced to root:root 0755 even when pre-created.
  • remove --system skips binary validation so a broken install can always be uninstalled; removal is best-effort (errors.Join) so one failed step doesn't strand the rest.
  • Event logging stays mandatory (fail-closed). Unwritable-directory failures get a cause-triaged remedy: explicit PMG_CONFIG_DIR, leaked HOME/XDG_* from another account, or a root-created directory in the user's own home — the wrong remedy (chown-ing another account's directory) is never suggested.
  • Per-user paths divert to /root only for sudo-elevated runs (SUDO_USER set), so sudo pmg can't poison the invoking user's home; genuine root keeps honoring HOME/XDG_* (Docker images that set them on purpose). SUDO_USER-based cloud attribution is honored only when euid is 0 to prevent spoofing.

Changes since #376's last review

Resolves the three open threads on #376:

  • realUserHomeDir renamed to currentUserHomeDir; it now fails when the passwd entry has no home directory.
  • UnwritableConfigDirRemedy split into classifyUnwritableDir (cause diagnosis, documented enum) and pure message rendering — one job per function.
  • Removed cmd/setup's duplicate path helper in favor of a shared config.PathWithinDir.

Test plan

  • go test ./... (full suite; also verified green running as root, which exercises the sudo/root path-resolution branches — guard and packagemanager network-dependent tests excluded in sandboxed envs)
  • Hermetic unit tests for shim install/remove, executable validation (private, group-writable, non-root-owned), profile repair, remedy triage, doctor classification
  • CI e2e e2e-system-install: root install, artifact checks, user-owned/private binary rejection, managed-config refusal, non-root user malware block via system shims, clean removal

🤖 Generated with Claude Code

https://claude.ai/code/session_011rWmR6NS47FpedJsrTWk8A


Generated by Claude Code

Sahilb315 and others added 24 commits July 11, 2026 02:19
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>
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
Prefix the warning with [pmg] and drop the redundant continuing clause.

Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
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>
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>
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>
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>
- 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)
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.
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
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.
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.
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.
Belongs with the previous commit; it was left unstaged and e0580b1
does not compile without it.
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.
…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.
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.
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.
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.
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
Copilot AI review requested due to automatic review settings July 14, 2026 11:13
@safedep

safedep Bot commented Jul 14, 2026

Copy link
Copy Markdown

SafeDep Report Summary

Green Malicious Packages Badge Green Vulnerable Packages Badge Green Risky License Badge

Package Details
Package Malware Vulnerability Risky License Report
icon buf.build/gen/go/safedep/api/protocolbuffers/go @ v1.36.11-20260713161921-716fa3011a21.1
go.mod
ok icon
ok icon
ok icon
🔗

View complete scan results →

This report is generated by SafeDep Github App

@codecov-commenter

codecov-commenter commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 49.55437% with 283 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.81%. Comparing base (d3e656e) to head (40a3a6f).

Files with missing lines Patch % Lines
cmd/setup/doctor.go 40.90% 76 Missing and 2 partials ⚠️
cmd/setup/setup.go 16.09% 72 Missing and 1 partial ⚠️
internal/shim/system.go 69.53% 30 Missing and 9 partials ⚠️
config/config.go 68.60% 14 Missing and 13 partials ⚠️
internal/shim/shim.go 57.77% 12 Missing and 7 partials ⚠️
main.go 0.00% 13 Missing ⚠️
cmd/setup/info.go 0.00% 12 Missing ⚠️
internal/fsutil/fsutil.go 65.38% 8 Missing and 1 partial ⚠️
internal/ui/info.go 0.00% 9 Missing ⚠️
internal/audit/cloud_sink.go 87.50% 1 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #377      +/-   ##
==========================================
+ Coverage   55.78%   55.81%   +0.02%     
==========================================
  Files         217      220       +3     
  Lines       14781    15262     +481     
==========================================
+ Hits         8246     8518     +272     
- Misses       5790     5969     +179     
- Partials      745      775      +30     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

vet Summary Report

This report is generated by vet

Policy Checks

  • ✅ Vulnerability
  • ✅ Malware
  • ✅ License
  • ✅ Popularity
  • ✅ Maintenance
  • ✅ Security Posture
  • ✅ Threats

Malicious Package Analysis

Malicious package analysis was performed using SafeDep Cloud API

Malicious Package Analysis Report
Ecosystem Package Version Status Report
  • ℹ️ 0 packages have been actively analyzed for malicious behaviour.
  • ✅ No malicious packages found.

Note: Some of the package analysis jobs may still be running.Please check back later. Consider increasing the timeout for better coverage.

Changed Packages

Changed Packages

  • ✅ [Go] buf.build/gen/go/safedep/api/protocolbuffers/go@1.36.11-20260713161921-716fa3011a21.1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a Linux “system-wide” installation plane for PMG (pmg setup install --system) intended for shared machines/images so a single root install protects all users, including updated setup doctor/info output, hardened shim/profile handling, expanded docs, and a new CI E2E job.

Changes:

  • Introduces system-wide shim/profile installation under /usr/local/lib/pmg/bin and /etc/profile.d/pmg.sh, plus install/remove CLI support via --system.
  • Extends config/runtime path resolution and remedy messaging for sudo/root scenarios; adds system-managed config write/remove helpers.
  • Enhances pmg setup doctor/pmg setup info, adds audit attribution hardening (SUDO_USER only trusted when euid==0), and adds docs + CI E2E coverage for system install.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Links system-wide install docs from the main README.
main.go Routes eventlog init failures through a useful-error classifier with remedies.
internal/ui/info.go Adds completion output for system-wide install (shims/config/profile guidance).
internal/shim/system.go Implements system install paths, profile.d management, and binary safety validation.
internal/shim/system_test.go Unit tests for system shim/profile install/remove and binary validation helpers.
internal/shim/shim.go Adds system-install knobs (skip rc edits, manage profile), best-effort removal, shared markers.
internal/shim/shim_test.go Adds a regression test ensuring remove reports directory-removal errors.
internal/shim/path.go Updates PATH filtering to strip the system shim dir to avoid recursion.
internal/shim/path_test.go Adds test coverage for stripping the system shim dir without env markers.
internal/shim/file_owner_windows.go Windows stub for file owner UID resolution.
internal/shim/file_owner_unix.go Unix implementation for file owner UID resolution via syscall.Stat_t.
internal/doctor/doctor.go Extends check results to carry interception implication + per-result fix overrides.
internal/audit/cloud_sink.go Adds username/uid attribution; hardens SUDO_USER trust to euid==0 only.
internal/audit/cloud_sink_test.go Tests for new invoking-user attribution behavior and spoofing resistance.
go.work.sum Updates module sums (dependency metadata).
go.sum Updates module sums for buf-generated protocolbuffers dependency.
go.mod Bumps buf-generated protocolbuffers dependency version.
docs/system-install.md New documentation for Linux system-wide install, artifacts, Docker PATH, limitations, troubleshooting.
docs/persistent-proxy.md Updates docs to point readers to the new system-install documentation.
docs/development.md Adds system install doc to the development docs index.
docs/config.md Adds a pointer to system-install docs from the config docs.
config/rootdir_test.go Tests for sudo/root home/config/cache dir resolution behavior.
config/remedy_test.go Tests for unwritable-dir remedy classification and messaging.
config/managed_config_test.go Adds tests for writing/removing the system template config.
config/config.go Adds root-dir resolution helpers, unwritable-dir triage, system config helpers, and shared path helper.
cmd/setup/setup.go Adds --system to setup install/remove; best-effort cleanup via errors.Join; platform/root gating.
cmd/setup/setup_test.go Tests platform/root gating for --system.
cmd/setup/info.go Extends setup info to report user vs system shims and alias state consistently.
cmd/setup/doctor.go Expands doctor to detect system installs, validate system binary drift, and verify “resolves to shim” behavior.
cmd/setup/doctor_test.go Unit tests for new doctor helpers (resolution classification, interception implication, event-log probe).
.github/workflows/pmg-e2e.yml Adds a new Linux E2E workflow job to validate system install, artifacts, refusals, and non-root interception.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread main.go
Comment thread main.go
Comment thread config/config.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fe3f77615

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/shim/shim.go Outdated
Comment thread config/config.go Outdated
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
Comment thread config/config.go Outdated
Comment thread config/config.go Outdated
Comment thread cmd/setup/setup.go Outdated
Comment thread internal/audit/cloud_sink.go Outdated
Comment thread internal/shim/system.go Outdated
Comment thread internal/shim/system.go Outdated
Comment thread internal/shim/system.go
Comment thread internal/shim/shim.go Outdated
- 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
@Sahilb315 Sahilb315 merged commit dc0e320 into main Jul 14, 2026
18 checks passed
@Sahilb315 Sahilb315 deleted the feat/add-linux-system-install-support branch July 14, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants