diff --git a/scripts/setup.sh b/scripts/setup.sh index 6c64f2fc..87c9fe58 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -695,35 +695,27 @@ else esac fi -# ─── 14. npm global packages (system-wide) ────────────────────────────────── +# ─── 14. npm global packages (user-local) ─────────────────────────────────── echo "" -echo "── npm global packages (system-wide) ──" +echo "── npm global packages (user-local) ──" + +# Ensure ~/.local/bin is in PATH for this session +export PATH="$HOME/.local/bin:$PATH" if ! has npm; then warn "npm not found — skipping global package installs" else - # Use explicit path to system npm if available, to avoid nvm conflicts - SYSTEM_NPM="/usr/bin/npm" - if [ ! -f "$SYSTEM_NPM" ]; then - SYSTEM_NPM="$(command -v npm)" - fi - - # Set npm prefix to system location so all users can access packages - echo " Configuring npm global prefix to /usr/local..." - sudo "$SYSTEM_NPM" config set prefix /usr/local - ok "npm prefix set to /usr/local" - - # Update npm itself to latest - echo " Updating npm to latest..." - sudo "$SYSTEM_NPM" install -g npm@latest - ok "npm updated to v$(npm --version 2>/dev/null)" + # Set npm prefix to user-local so no sudo is needed for installs/updates + echo " Configuring npm global prefix to ~/.local..." + npm config set prefix "$HOME/.local" + ok "npm prefix set to $HOME/.local" # Pi (coding agent) if has pi; then skip "pi (@mariozechner/pi-coding-agent)" else echo " Installing pi..." - sudo "$SYSTEM_NPM" install -g @mariozechner/pi-coding-agent + npm install -g @mariozechner/pi-coding-agent if has pi; then ok "pi installed"; else warn "pi install failed"; fi fi @@ -736,20 +728,12 @@ else fi fi - # Fix ~/.pi ownership if needed (in case of mixed user/sudo installs) - if [ -d "$HOME/.pi" ]; then - if find "$HOME/.pi" -not -user "$USER" -print -quit 2>/dev/null | grep -q .; then - sudo chown -R "$USER:$(id -gn)" "$HOME/.pi" - ok "Fixed ~/.pi ownership to $USER" - fi - fi - - # Claude Code + # Claude Code (uses Anthropic's own installer for auto-update support) if has claude; then - skip "claude (@anthropic-ai/claude-code)" + skip "claude (claude-code)" else - echo " Installing claude-code..." - sudo "$SYSTEM_NPM" install -g @anthropic-ai/claude-code + echo " Installing claude-code via Anthropic installer..." + curl -fsSL https://claude.ai/install.sh | sh if has claude; then ok "claude-code installed"; else warn "claude-code install failed"; fi fi @@ -758,7 +742,7 @@ else skip "pm2" else echo " Installing pm2..." - sudo "$SYSTEM_NPM" install -g pm2 + npm install -g pm2 if has pm2; then ok "pm2 installed"; else warn "pm2 install failed"; fi fi fi diff --git a/src/servers/sidecar/claude/claude-manager.ts b/src/servers/sidecar/claude/claude-manager.ts index 09e60f7b..ece56cb0 100644 --- a/src/servers/sidecar/claude/claude-manager.ts +++ b/src/servers/sidecar/claude/claude-manager.ts @@ -8,11 +8,9 @@ import { parseStream } from './stream-parser'; const SEND_TIMEOUT_MS = 5 * 60 * 1000; -// Resolve absolute path to claude binary -const CLAUDE_BIN = (() => { - const result = Bun.spawnSync({ cmd: ['which', 'claude'], stdout: 'pipe', stderr: 'ignore' }); - return result.stdout.toString().trim() || 'claude'; -})(); +// Use /usr/local/bin/claude so it's visible inside bwrap sandbox (which ro-binds /usr). +// The actual binary lives at ~/.local/bin/claude, symlinked from /usr/local/bin/claude. +const CLAUDE_BIN = '/usr/local/bin/claude'; const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data'); diff --git a/src/servers/sidecar/pi/pi-manager.ts b/src/servers/sidecar/pi/pi-manager.ts index c67f9c40..6fbbada5 100644 --- a/src/servers/sidecar/pi/pi-manager.ts +++ b/src/servers/sidecar/pi/pi-manager.ts @@ -26,7 +26,8 @@ function isPidAlive(pid: number): boolean { } } -// Resolve pi as [node, cli.js] +// Resolve pi as [node, cli.js] — the real .js path lives under ~/.local/lib/node_modules +// which is ro-mounted in the sandbox. Node is at /usr/bin/node (under /usr ro-bind). const PI_CMD = (() => { const whichResult = Bun.spawnSync({ cmd: ['which', 'pi'], stdout: 'pipe', stderr: 'ignore' }); const piBin = whichResult.stdout.toString().trim() || 'pi'; diff --git a/src/servers/sidecar/sandbox.ts b/src/servers/sidecar/sandbox.ts index f800f609..dc0f54ac 100644 --- a/src/servers/sidecar/sandbox.ts +++ b/src/servers/sidecar/sandbox.ts @@ -65,9 +65,21 @@ export function buildSandboxPrefix(email: string): string[] { if (existsSync('/lib64')) args.push('--ro-bind', '/lib64', '/lib64'); if (existsSync('/sbin')) args.push('--ro-bind', '/sbin', '/sbin'); + // Ensure intermediate dirs under HOME are traversable after runuser drops privileges + // (bwrap auto-creates them as root-owned drwx------) + const homeDir = process.env.HOME!; + args.push('--perms', '0755', '--dir', homeDir); + // Bun runtime (e.g. /home/pastilhas/.bun) args.push('--ro-bind', BUN_DIR, BUN_DIR); + // npm global packages (~/.local/lib) — pi and its dependencies + const localLib = join(homeDir, '.local', 'lib'); + if (existsSync(localLib)) { + args.push('--perms', '0755', '--dir', join(homeDir, '.local')); + args.push('--ro-bind', localLib, localLib); + } + // Project source (for MCP server) args.push('--ro-bind', PROJECT_ROOT, PROJECT_ROOT);