Files
pmg/internal/ui/exit.go
T
Abhisek DattaandGitHub c7244f921a feat: Add support for environment protection (scrubbing) (#327)
* feat: Add support for environment variable protection for sandbox

* chore: Update dangerous env var list

* fix: Split profiles for improved environment protection

* fix: pipx sandbox profile separation

* chore: Show sandbox scrub info on error exit

* fix: Code review fixes

* test: Add e2e for sandbox environment scrubbing
2026-06-11 11:40:33 +05:30

76 lines
2.0 KiB
Go

package ui
import (
"errors"
"fmt"
"os"
)
// transparentExit is satisfied by *runner.ChildExitError without importing it.
type transparentExit interface {
error
Transparent() bool
ExitCode() int
IsSignaled() bool
}
// scrubbedEnvReporter is optionally satisfied by a transparent exit error to
// surface how many environment variables the sandbox scrubbed from the failed
// run. Kept separate from transparentExit so older implementations still
// classify as transparent.
type scrubbedEnvReporter interface {
ScrubbedEnvCount() int
}
type exitDecision struct {
transparent bool
notice bool
code int
message string
}
// classifyExit is the pure decision behind ExitFromCommandError. The notice is
// suppressed for signal exits (the user initiated the interrupt) and in silent
// mode.
func classifyExit(err error) exitDecision {
var te transparentExit
if !errors.As(err, &te) || !te.Transparent() {
return exitDecision{}
}
d := exitDecision{transparent: true, code: te.ExitCode()}
if !te.IsSignaled() && verbosityLevel != VerbosityLevelSilent {
d.notice = true
d.message = "↳ pmg: " + te.Error()
// Env scrubbing produces no sandbox violation and the child's own
// error (e.g. a registry 401) does not point at the cause, so hint at
// it here. Names are not printed; they are at info level via --debug.
if sr, ok := te.(scrubbedEnvReporter); ok && sr.ScrubbedEnvCount() > 0 {
d.message += fmt.Sprintf(
"\n↳ pmg: sandbox scrubbed %d env var(s) (names via --debug, re-allow with --sandbox-allow env=NAME)",
sr.ScrubbedEnvCount())
}
}
return d
}
// ExitFromCommandError is the single exit point for package-manager commands. A
// child that exited on its own is mirrored transparently; everything else keeps
// the visible PMG error framing.
func ExitFromCommandError(err error) {
if err == nil {
return
}
if d := classifyExit(err); d.transparent {
ClearStatus()
if d.notice {
fmt.Fprintln(os.Stderr, Colors.Dim(d.message))
}
os.Exit(d.code)
}
ErrorExit(err)
}