Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8f6d0fc
feat: add Linux system-wide setup
Sahilb315 Jul 10, 2026
7922606
chore: keep local design documents untracked
Sahilb315 Jul 10, 2026
ffd0e7e
fix: harden and simplify Linux system install
Sahilb315 Jul 10, 2026
8d1a4b6
fix: clarify system-install doctor alias and shim path checks
Sahilb315 Jul 13, 2026
fdd1aea
fix: tighten event-log soft-fail warning prefix
Sahilb315 Jul 13, 2026
5d710a7
ci: add Linux system-install e2e and pin pnpm for add flake
Sahilb315 Jul 13, 2026
2b53abb
chore: bump packageManager to pnpm 11.10.0 for e2e
Sahilb315 Jul 13, 2026
37eed63
ci: use npm init for pnpm e2e to avoid integrity crash
Sahilb315 Jul 13, 2026
0d0399f
chore: revert packageManager pin to pnpm 11.1.3
Sahilb315 Jul 13, 2026
1c9b16f
fix: harden system-install review findings
Sahilb315 Jul 13, 2026
b1aa217
fix: harden doctor PATH checks and attribute cloud events by OS user
Sahilb315 Jul 13, 2026
1276a1e
fix: address system-install review findings
Sahilb315 Jul 13, 2026
1b17eb2
ci: normalize /usr/local/bin perms before system-install e2e
Sahilb315 Jul 13, 2026
479f546
fix: actionable remedy for root-created per-user config dir
Sahilb315 Jul 13, 2026
de0fa41
fix: resolve per-user paths from root's own home when running as root
Sahilb315 Jul 13, 2026
cd9b45b
fix: triage the unwritable config dir remedy by cause
Sahilb315 Jul 13, 2026
e0580b1
ci: pin XDG_CONFIG_HOME for the cross-user e2e step; terse doctor fix
Sahilb315 Jul 13, 2026
ad16d8c
fix: adapt event-log error to the two-value remedy signature
Sahilb315 Jul 13, 2026
297f516
fix: fall back to env path resolution when root has no passwd entry
Sahilb315 Jul 13, 2026
4b2a25a
fix: gate SUDO_USER trust and root path diversion; add doctor binary …
Sahilb315 Jul 14, 2026
748d40c
fix: harden system dirs at install; keep sudo attribution without passwd
Sahilb315 Jul 14, 2026
f251a07
fix: reject system binaries unreachable by other users; consistent info
Sahilb315 Jul 14, 2026
4d65a2c
fix: stop reowning /etc/profile.d; document group-writable and su gaps
Sahilb315 Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 166 additions & 1 deletion .github/workflows/pmg-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,10 @@ jobs:
run: |
echo "Testing PNPM single package installation..."
PNPM_TESTDIR=$(mktemp -d) && cd "$PNPM_TESTDIR"
pmg --proxy-mode=false pnpm init
# Avoid `pnpm init`: it writes devEngines.packageManager with
# onFail:download, and the following `pnpm add` then crashes with
# "Cannot use 'in' operator to search for 'integrity' in undefined".
npm init -y
pmg --proxy-mode=false pnpm add express@5.2.1
pmg --proxy-mode=false pnpm add lodash@4.17.21

Expand Down Expand Up @@ -874,3 +877,165 @@ jobs:

- name: Run Package Manager E2E Test
run: pmg --sandbox --sandbox-enforce npm exec -- node test/pm-e2e.js

# Linux system-wide install: root install, ENV PATH (Docker-style), non-root user,
# managed config, and remove. Profile.d login shells are covered by sourcing the snippet.
e2e-system-install:
name: PMG E2E - System Install (Linux)
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
shell: bash
steps:
- name: Checkout Source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version-file: go.mod
cache: true

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"

- name: Build PMG
run: make

- name: Reject private PMG binary for system install
run: |
sudo mkdir -p /root/pmg-private
sudo cp bin/pmg /root/pmg-private/pmg
sudo chmod 700 /root/pmg-private /root/pmg-private/pmg
if sudo /root/pmg-private/pmg setup install --system; then
echo "ERROR: system install accepted a non-world-executable binary"
exit 1
fi
echo "SUCCESS: private binary rejected"

- name: Reject user-owned PMG binary for system install
run: |
mkdir -p "$HOME/pmg-user-writable"
cp bin/pmg "$HOME/pmg-user-writable/pmg"
chmod 755 "$HOME/pmg-user-writable/pmg"
if sudo "$HOME/pmg-user-writable/pmg" setup install --system; then
echo "ERROR: system install accepted a user-owned binary"
exit 1
fi
echo "SUCCESS: user-owned binary rejected"

- name: Reject unreachable PMG binary for system install
run: |
# Binary is 0755 and root-owned, but sits under a 0700 dir: other
# users cannot traverse to it, so every shim would exit 127.
sudo mkdir -p /root/pmg-unreachable
sudo install -m 755 bin/pmg /root/pmg-unreachable/pmg
sudo chmod 700 /root/pmg-unreachable
if sudo /root/pmg-unreachable/pmg setup install --system; then
echo "ERROR: system install accepted a binary under a non-searchable directory"
exit 1
fi
echo "SUCCESS: unreachable binary rejected"

- name: Install PMG system-wide
run: |
# GitHub runners ship /usr/local/bin world-writable; system install
# (correctly) requires a root-owned, non-world-writable dir for the
# shared binary. Normalize to the standard production perms first.
ls -ld /usr/local/bin
sudo chown root:root /usr/local/bin
sudo chmod 755 /usr/local/bin
sudo install -m 755 bin/pmg /usr/local/bin/pmg
sudo pmg setup install --system

- name: Verify system install artifacts
run: |
test -f /etc/safedep/pmg/config.yml
test -f /etc/profile.d/pmg.sh
grep -q '/usr/local/lib/pmg/bin' /etc/profile.d/pmg.sh
for shim in npm pip pip3 pipx pnpm bun uv uvx yarn poetry npx pnpx; do
test -x "/usr/local/lib/pmg/bin/$shim" || { echo "Missing shim: $shim"; exit 1; }
done

- name: Root runs keep per-user state out of the invoking user's home
run: |
# GitHub runner sudo preserves HOME. Every sudo pmg run above used to
# create root-owned ~/.config/safedep for the runner user, which
# fail-closes all their later pmg/npm runs. Must run before any
# non-root pmg invocation legitimately creates that directory.
sudo sh -c 'echo "sudo sees HOME=$HOME"'
if [ -e "$HOME/.config/safedep" ]; then
echo "ERROR: root-created state leaked into $HOME/.config/safedep"
ls -laR "$HOME/.config/safedep"
exit 1
fi
sudo test -d /root/.config/safedep/pmg/logs
echo "SUCCESS: root state stayed under /root"

- name: PATH and profile.d activate shims
run: |
# Docker-style: non-login shells need PATH (or source profile.d)
export PATH="/usr/local/lib/pmg/bin:$PATH"
which npm | grep -q '/usr/local/lib/pmg/bin/npm'
source /etc/profile.d/pmg.sh
which npm | grep -q '/usr/local/lib/pmg/bin/npm'

- name: Managed config refuses CLI mutation
run: |
# Assert the refusal reason: a permission-denied brick (poisoned home)
# would also make config set fail and mask a regression.
if out=$(pmg config set dependency_cooldown.days 7 2>&1); then
echo "ERROR: config set should fail under system config"
exit 1
fi
echo "$out" | grep -qi 'globally managed' || { echo "ERROR: failed for the wrong reason:"; echo "$out"; exit 1; }
if out=$(sudo pmg config set dependency_cooldown.days 7 2>&1); then
echo "ERROR: config set should fail under system config even as root"
exit 1
fi
echo "$out" | grep -qi 'globally managed' || { echo "ERROR: root run failed for the wrong reason:"; echo "$out"; exit 1; }
echo "SUCCESS: managed config is locked"

- name: Doctor reports system install state
run: |
export PATH="/usr/local/lib/pmg/bin:$PATH"
out=$(pmg setup doctor 2>&1 || true)
echo "$out"
echo "$out" | grep -q 'No aliases (system install)'
echo "$out" | grep -Eq 'Package managers resolve to System shim directory|System shim directory is in PATH'

- name: Non-root user interception via system shims
run: |
sudo useradd -m pmgtest || true
# Pass runner PATH so setup-node's npm remains visible after FilterPMGFromPath.
sudo -u pmgtest env "PATH=/usr/local/lib/pmg/bin:$PATH" HOME=/home/pmgtest bash -lc '
set -euo pipefail
# GH runners export XDG_CONFIG_HOME=/home/runner/.config and it
# leaks through sudo -u, so pmg would resolve the runner user
# config dir and fail on its runner-owned log file.
export XDG_CONFIG_HOME="$HOME/.config"
echo "HOME=$HOME XDG_CONFIG_HOME=$XDG_CONFIG_HOME"
which npm | grep -q /usr/local/lib/pmg/bin/npm
mkdir -p "$HOME/sys-e2e" && cd "$HOME/sys-e2e"
npm init -y
if npm install --no-cache --prefer-online safedep-test-pkg@0.1.3; then
echo "ERROR: safedep-test-pkg was not blocked for non-root user"
exit 1
fi
if [ -d node_modules/safedep-test-pkg ]; then
echo "ERROR: safedep-test-pkg present in node_modules"
exit 1
fi
echo "SUCCESS: non-root user blocked malicious package via system shims"
'

- name: Remove system install
run: |
sudo pmg setup remove --system --config-file
test ! -e /etc/profile.d/pmg.sh
test ! -e /etc/safedep/pmg/config.yml
test ! -d /usr/local/lib/pmg/bin
echo "SUCCESS: system install removed"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ pmg setup install
```

> **Tip:** Re-run `pmg setup install` after upgrading PMG to pick up new configuration options.
>
> Linux all-users / golden images: `sudo pmg setup install --system` — see [docs/system-install.md](./docs/system-install.md).

Validate your installation and verify protection is working:

Expand Down
Loading
Loading