feat: publish verifiable proxyctl v1

This commit is contained in:
proxyctl coordinator
2026-07-17 09:54:08 +08:00
parent 5ec2b1dae1
commit 2b161e86e6
11 changed files with 536 additions and 28 deletions
+36
View File
@@ -0,0 +1,36 @@
name: hermetic-ci
on:
push:
pull_request:
permissions:
contents: read
env:
GOFLAGS: -mod=readonly
GOTOOLCHAIN: local
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Download locked modules
run: go mod download
- name: Formatting
run: test -z "$(gofmt -l .)"
- name: Hermetic full test suite
run: ./scripts/test-hermetic.sh
- name: Static analysis
run: go vet ./...
- name: Documentation contract
run: ./scripts/check-docs.sh
- name: Reproducible cross-builds and checksums
run: ./scripts/test-release.sh
- name: Clean-machine verification
run: ./scripts/verify-clean-machine.sh
+135
View File
@@ -0,0 +1,135 @@
# proxyctl
`proxyctl` is a keyboard-driven Linux TUI for coordinating one **Active Profile** across independently switchable proxy **Targets**. It manages proxy configuration surfaces; it does not run a proxy server.
## Supported systems
The v1 binaries target Linux on amd64 and arm64. General TUI behavior and the Shell Target are distribution-independent. The supported Target boundaries are:
- **Shell:** Bash and Zsh only.
- **APT:** Debian-family systems with `apt-config` and `apt.conf.d`.
- **Docker daemon:** Docker installations managed by systemd.
- **Docker containers/builds:** the standard per-user Docker client `config.json` model.
- **GNOME:** sessions exposing the expected `org.gnome.system.proxy` settings schemas.
Unavailable Targets are hidden from the main panel; `?` shows every built-in Target and its detection evidence.
## Install
### Verified release binary
Download the binary for your architecture together with `SHA256SUMS`, then verify it before installing:
```sh
grep 'proxyctl-linux-amd64$' SHA256SUMS | sha256sum -c - # substitute arm64 when needed
chmod +x proxyctl-linux-amd64 # use proxyctl-linux-arm64 on arm64
install -m 0755 proxyctl-linux-amd64 "$HOME/.local/bin/proxyctl"
proxyctl version
proxyctl version --short # exact value only, for scripts
```
Release output is two single binaries, not packages or archives. The project does not publish deb, rpm, AUR, Snap, Flatpak, or similar packages.
### Install from source with Go
Go 1.24 or newer is required. From a source checkout:
```sh
go install ./cmd/proxyctl
```
The executable is placed in `GOBIN`, or in `$(go env GOPATH)/bin` when `GOBIN` is unset. Ensure that directory is on `PATH`. Source builds report `dev` unless a release version is injected by release automation.
## First use and Profiles
Start the Target panel with no arguments:
```sh
proxyctl
```
Press `p` for Proxy Profile management, `n` to create a Profile, enter a name and proxy URL, and press Ctrl+S to save. Single mode applies one URL to HTTP, HTTPS, and ALL/SOCKS. Ctrl+T selects advanced mode for separate values. Bypass accepts hostnames, domain suffixes, IPv4, IPv6, and CIDR entries. Select the Profile and press `a` or Enter to make it the Active Profile.
The main panel shows the Active Profile. Use arrows or `j`/`k` to select an Available Target, Space to toggle it, `r` to refresh detection, `?` for details, and `q` or Escape to leave. A Target cannot be enabled without an Active Profile, but previously managed configuration can still be disabled.
Profiles are human-readable under the user's XDG configuration directory (normally `~/.config/proxyctl/profiles.json`) and are stored owner-only because proxy URLs may contain credentials.
## Shell integration
A child TUI cannot change its parent Shell. Install the one-time Bash/Zsh startup hook:
```sh
proxyctl shell install
```
Restart or source the startup file once. Thereafter the wrapper reloads proxy exports and unsets after `proxyctl` exits. The Shell Target emits conventional upper- and lower-case `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` forms. Remove the hook with:
```sh
proxyctl shell uninstall
```
Fish and other Shells are intentionally unsupported in v1.
## What each Target affects
1. **Shell** — commands started by integrated Bash/Zsh sessions. It does not change existing processes, system services, Docker, or desktop settings.
2. **APT** — persistent APT package operations across Shell and privilege boundaries through a tool-owned `apt.conf.d` overlay. It does not configure other package managers and requires no service restart.
3. **Docker daemon** — daemon operations such as registry access. It does not set container networking or container/build defaults. Every change reloads systemd and immediately restarts Docker; confirm only when disruption to running work is acceptable.
4. **Docker containers/builds** — proxy variables and build arguments for future containers and builds via the owning user's Docker client configuration. It does not affect existing containers, the Docker CLI connection, or the daemon.
5. **GNOME** — GNOME system proxy settings used by compatible desktop applications. It does not rewrite browser-specific profiles or configure non-GNOME desktops.
proxyctl prefers isolated, tool-owned overlays. Where that is impossible it records original values and a last-applied fingerprint so disable/reset can restore prior state safely.
## Target states
- **Enabled:** actual managed configuration matches the Active Profile.
- **Disabled:** no managed or positively recognized external proxy configuration was detected.
- **External:** a recognized proxy configuration exists but is not owned by proxyctl.
- **Drifted:** managed configuration exists but no longer matches the Active Profile, including partial failures or configuration left when the final Profile is deleted.
External detection is best-effort; Disabled does not claim that every possible configuration source was exhaustively searched.
## Permissions, sudo, and restarts
User Targets normally need only user-file permissions. APT and Docker daemon files generally require authority to write system locations, and Docker daemon operations require authority to reload/restart its systemd service.
proxyctl **never** runs `sudo`, `pkexec`, or another elevation mechanism. Permission failures are reported normally. If system Targets are wanted, the user must explicitly start `sudo proxyctl`; under sudo, the invoking user remains the owner of Profiles, Shell, Docker client, and GNOME configuration while current root authority is available to system Targets. In a direct root login, root is the owner. Review the Docker restart warning before confirming any daemon change.
## Diagnose, reset, and remove
Run read-only diagnostics for Profile validity, storage permissions, required commands, Target availability/state, and Target-specific authority:
```sh
proxyctl doctor
```
Before removal, restore every managed Target. The command asks for confirmation and does nothing when declined:
```sh
proxyctl reset
proxyctl shell uninstall
```
Inspect failures and rerun until reset reports no failed managed Target. Then remove the installed executable and, after retaining any desired logs, remove empty proxyctl configuration/state directories. Do not delete recovery state before a successful reset because it may be needed to restore original settings.
## Privacy and safety boundaries
proxyctl has no telemetry, automatic update checks, or automatic proxy-connectivity tests. It does not contact the network on its own. Local diagnostic logs are credential-redacted and bounded in size. Proxy credentials remain present where a selected Target requires them, so normal local file security still matters.
proxyctl does **not** manage or supervise proxy processes such as Clash, sing-box, or Xray. It does not manage subscriptions, nodes, latency, routing rules, transparent proxying, TUN/iptables/nftables, arbitrary services or commands, plugins, or a complete headless Profile/Target CLI.
Deferred integrations include DNF, Git-specific settings, npm/pip, arbitrary systemd services, Firefox policies, Chromium launch flags, KDE and other desktops, Snap/Ubuntu Core, Fish, Windows, macOS, distribution packages, mouse interaction, and cross-Target rollback. Conventional Shell variables may already cover some deferred command-line tools.
Support is limited to the Linux architectures and Target boundaries above. When reporting an issue, include `proxyctl version`, redacted `proxyctl doctor` output, distribution/desktop/Shell details, and the affected Target/state. Never include unredacted proxy credentials.
## Build and verification
Create reproducible Linux binaries and checksums without packages or archives:
```sh
VERSION=v1.0.0 ./scripts/build-release.sh
sha256sum -c dist/SHA256SUMS
file dist/proxyctl-linux-amd64 dist/proxyctl-linux-arm64
```
`./scripts/test-release.sh` builds twice, compares binaries and checksum manifests, verifies both checksums, and checks ELF architectures. `./scripts/verify-clean-machine.sh` uses an isolated identity and XDG roots to exercise `go install`, Profile and hermetic Target workflows, doctor, reset cancellation, Shell install/uninstall, removal, and unchanged host-state snapshots.
+22 -4
View File
@@ -27,6 +27,9 @@ import (
"proxyctl/internal/tui"
)
// version is overridden for releases with -ldflags "-X main.version=<version>".
var version = "dev"
func main() {
if err := run(os.Args[1:], os.Stdin, os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, "proxyctl:", err)
@@ -50,11 +53,17 @@ func runWithSystemTargets(args []string, input io.Reader, output io.Writer, syst
type processEnvironment struct{}
var (
processCurrentUser = profile.SystemCurrentUser
processLookupUser = profile.SystemLookupUser
processProductionTargets = productionTargets
)
func (processEnvironment) CurrentUser() (profile.UserIdentity, error) {
return profile.SystemCurrentUser()
return processCurrentUser()
}
func (processEnvironment) LookupUser(name string) (profile.UserIdentity, error) {
return profile.SystemLookupUser(name)
return processLookupUser(name)
}
func (processEnvironment) LookupEnv(name string) (string, bool) { return os.LookupEnv(name) }
@@ -63,6 +72,15 @@ func runWithEnvironment(args []string, input io.Reader, output io.Writer, enviro
}
func runWithEnvironmentAndTargets(args []string, input io.Reader, output io.Writer, environment profile.IdentityEnvironment, files shell.FileSystem, executable string, systemTargets []target.Target) error {
if len(args) == 1 && args[0] == "version" {
fmt.Fprintf(output, "proxyctl version %s\n", version)
return nil
}
if len(args) == 2 && args[0] == "version" && args[1] == "--short" {
fmt.Fprintln(output, version)
return nil
}
profileConfiguration, err := profile.ResolveConfiguration(environment)
if err != nil {
if len(args) == 1 && args[0] == "doctor" {
@@ -79,7 +97,7 @@ func runWithEnvironmentAndTargets(args []string, input io.Reader, output io.Writ
}
shellTarget := shell.New(shellConfiguration, files, environment)
if systemTargets == nil {
systemTargets, err = productionTargets(environment)
systemTargets, err = processProductionTargets(environment)
if err != nil {
return err
}
@@ -129,7 +147,7 @@ func runWithEnvironmentAndTargets(args []string, input io.Reader, output io.Writ
case len(args) == 1 && args[0] == "reset":
return runMaintenanceCommand(args, input, output, store, targets, mutationLock, logger, nil)
default:
return fmt.Errorf("usage: proxyctl [doctor|reset|shell install|shell uninstall]")
return fmt.Errorf("usage: proxyctl [version [--short]|doctor|reset|shell install|shell uninstall]")
}
}
+29
View File
@@ -11,6 +11,35 @@ import (
"proxyctl/internal/target"
)
func TestVersionOutputIsHumanAndScriptFriendly(t *testing.T) {
previous := version
version = "v1.2.3"
t.Cleanup(func() { version = previous })
var output bytes.Buffer
if err := run([]string{"version"}, strings.NewReader(""), &output); err != nil {
t.Fatalf("run version: %v", err)
}
if got, want := output.String(), "proxyctl version v1.2.3\n"; got != want {
t.Fatalf("human version output = %q, want %q", got, want)
}
output.Reset()
if err := run([]string{"version", "--short"}, strings.NewReader(""), &output); err != nil {
t.Fatalf("run short version: %v", err)
}
if got, want := output.String(), "v1.2.3\n"; got != want {
t.Fatalf("script version output = %q, want %q", got, want)
}
}
func TestVersionDoesNotRequireUserOrTargetResolution(t *testing.T) {
var output bytes.Buffer
if err := runWithEnvironmentAndTargets([]string{"version", "--short"}, strings.NewReader(""), &output, failingIdentityEnvironment{}, nil, "", nil); err != nil {
t.Fatalf("version depended on runtime environment: %v", err)
}
}
func TestProductionCompositionRegistersAPTContainerGNOMEAndDaemonTargets(t *testing.T) {
home := t.TempDir()
environment := commandEnvironment{identity: profile.UserIdentity{Username: "tester", HomeDir: home, UID: os.Getuid(), GID: os.Getgid()}, env: map[string]string{"SHELL": "/bin/bash"}}
+61 -24
View File
@@ -116,39 +116,75 @@ func TestDoctorStoragePermissionCheckIsReadOnly(t *testing.T) {
}
}
func TestMaintenanceCommandsRunThroughBuiltBinaryWithIsolatedXDG(t *testing.T) {
func TestCommandsRunAsRealProcessesInsideIsolatedUserState(t *testing.T) {
root := t.TempDir()
binary := filepath.Join(root, "proxyctl")
build := exec.Command("go", "build", "-o", binary, ".")
binary := filepath.Join(root, "bin", "proxyctl")
if err := os.MkdirAll(filepath.Dir(binary), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(root, "home"), 0o700); err != nil {
t.Fatal(err)
}
build := exec.Command("go", "build", "-tags", "proxyctl_process_test", "-o", binary, ".")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build proxyctl: %v\n%s", err, output)
}
environment := isolatedProcessEnvironment(root)
doctor := exec.Command(binary, "doctor")
doctor.Env = environment
doctorOutput, doctorErr := doctor.CombinedOutput()
if doctorErr == nil || !strings.Contains(string(doctorOutput), "[FAIL]") || !strings.Contains(string(doctorOutput), "Target availability") {
invoke := func(input string, args ...string) ([]byte, error) {
t.Helper()
command := exec.Command(binary, args...)
command.Env = environment
command.Stdin = strings.NewReader(input)
return command.CombinedOutput()
}
defaultOutput, err := invoke("q")
if err != nil || !strings.Contains(string(defaultOutput), "proxyctl — Target panel") {
t.Fatalf("default process err=%v output=%q", err, defaultOutput)
}
versionOutput, err := invoke("", "version", "--short")
if err != nil || strings.TrimSpace(string(versionOutput)) == "" || strings.Contains(string(versionOutput), " ") {
t.Fatalf("version process err=%v output=%q", err, versionOutput)
}
doctorOutput, doctorErr := invoke("", "doctor")
if !strings.Contains(string(doctorOutput), "Profile validity") || (doctorErr != nil && !strings.Contains(string(doctorOutput), "[FAIL]")) {
t.Fatalf("doctor process err=%v output=%q", doctorErr, doctorOutput)
}
cancel := exec.Command(binary, "reset")
cancel.Env = environment
cancel.Stdin = strings.NewReader("no\n")
cancelOutput, err := cancel.CombinedOutput()
if err != nil || !strings.Contains(string(cancelOutput), "Reset cancelled") {
t.Fatalf("cancel process err=%v output=%q", err, cancelOutput)
resetOutput, err := invoke("no\n", "reset")
if err != nil || !strings.Contains(string(resetOutput), "Reset cancelled") {
t.Fatalf("reset cancellation process err=%v output=%q", err, resetOutput)
}
resetOutput, err = invoke("yes\n", "reset")
if err != nil || !strings.Contains(string(resetOutput), "shell: skipped (Disabled)") {
t.Fatalf("confirmed reset process err=%v output=%q", err, resetOutput)
}
installOutput, err := invoke("", "shell", "install")
if err != nil || !strings.Contains(string(installOutput), "installed") {
t.Fatalf("shell install process err=%v output=%q", err, installOutput)
}
startup := filepath.Join(root, "home", ".bashrc")
startupData, err := os.ReadFile(startup)
if err != nil || !strings.Contains(string(startupData), "proxyctl shell integration") {
t.Fatalf("isolated startup after install err=%v data=%q", err, startupData)
}
uninstallOutput, err := invoke("", "shell", "uninstall")
if err != nil || !strings.Contains(string(uninstallOutput), "uninstalled") {
t.Fatalf("shell uninstall process err=%v output=%q", err, uninstallOutput)
}
startupData, err = os.ReadFile(startup)
if err != nil || strings.Contains(string(startupData), "proxyctl shell integration") {
t.Fatalf("isolated startup after uninstall err=%v data=%q", err, startupData)
}
confirmed := exec.Command(binary, "reset")
confirmed.Env = environment
confirmed.Stdin = strings.NewReader("yes\n")
confirmedOutput, err := confirmed.CombinedOutput()
if err != nil || !strings.Contains(string(confirmedOutput), "shell: skipped (Disabled)") {
t.Fatalf("confirmed process err=%v output=%q", err, confirmedOutput)
}
if _, err := os.Stat(filepath.Join(root, "home", ".bashrc")); !os.IsNotExist(err) {
t.Fatalf("process smoke test touched a Shell startup file: %v", err)
for _, path := range []string{
filepath.Join(root, "etc"),
filepath.Join(root, "var"),
filepath.Join(root, "home", ".docker"),
} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("process smoke created prohibited system/Docker fixture %s: %v", path, err)
}
}
}
@@ -165,7 +201,8 @@ func isolatedProcessEnvironment(root string) []string {
"HOME="+filepath.Join(root, "home"),
"XDG_CONFIG_HOME="+filepath.Join(root, "config"),
"XDG_STATE_HOME="+filepath.Join(root, "state"),
"SHELL=/bin/fish",
"SHELL=/bin/bash",
"PROXYCTL_PROCESS_TEST_HOME="+filepath.Join(root, "home"),
)
}
+41
View File
@@ -0,0 +1,41 @@
//go:build proxyctl_process_test
package main
import (
"errors"
"os"
"os/user"
"strconv"
"proxyctl/internal/profile"
"proxyctl/internal/target"
)
// This file is compiled only into the process-smoke binary. It gives that real
// executable an identity whose home is inside the test sandbox and omits
// production system Targets, so no command can reach developer or host state.
func init() {
processProductionTargets = func(profile.IdentityEnvironment) ([]target.Target, error) {
return []target.Target{}, nil
}
processCurrentUser = func() (profile.UserIdentity, error) {
home := os.Getenv("PROXYCTL_PROCESS_TEST_HOME")
if home == "" {
return profile.UserIdentity{}, errors.New("PROXYCTL_PROCESS_TEST_HOME is required by proxyctl_process_test builds")
}
current, err := user.Current()
if err != nil {
return profile.UserIdentity{}, err
}
uid, err := strconv.Atoi(current.Uid)
if err != nil {
return profile.UserIdentity{}, err
}
gid, err := strconv.Atoi(current.Gid)
if err != nil {
return profile.UserIdentity{}, err
}
return profile.UserIdentity{Username: current.Username, HomeDir: home, UID: uid, GID: gid}, nil
}
}
+39
View File
@@ -0,0 +1,39 @@
#!/bin/sh
set -eu
repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
out=${1:-"$repo/dist"}
: "${VERSION:?set VERSION to the release version, for example v1.0.0}"
case $VERSION in
*[!A-Za-z0-9._+-]*|'')
echo "VERSION contains unsupported characters: $VERSION" >&2
exit 2
;;
esac
mkdir -p "$out"
out=$(CDPATH= cd -- "$out" && pwd)
rm -f "$out/proxyctl-linux-amd64" "$out/proxyctl-linux-arm64" "$out/SHA256SUMS"
build() {
arch=$1
output="$out/proxyctl-linux-$arch"
(
cd "$repo"
CGO_ENABLED=0 GOOS=linux GOARCH="$arch" \
go build -mod=readonly -buildvcs=false -trimpath \
-ldflags="-s -w -X main.version=$VERSION" \
-o "$output" ./cmd/proxyctl
)
}
umask 022
build amd64
build arm64
(
cd "$out"
sha256sum proxyctl-linux-amd64 proxyctl-linux-arm64 > SHA256SUMS
)
printf 'built Linux amd64 and arm64 binaries in %s\n' "$out"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
set -eu
repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
readme="$repo/README.md"
test -s "$readme"
for text in \
'## Install' 'go install ./cmd/proxyctl' '## First use and Profiles' \
'## Shell integration' 'Shell' 'APT' 'Docker daemon' \
'Docker containers/builds' 'GNOME' '## Target states' 'Enabled' \
'Disabled' 'External' 'Drifted' '## Permissions, sudo, and restarts' \
'proxyctl doctor' 'proxyctl reset' 'proxyctl shell uninstall' \
'no telemetry' 'automatic update checks' 'proxy-connectivity tests' \
'does **not** manage or supervise proxy processes' 'Support is limited'
do
grep -Fq "$text" "$readme" || {
echo "README is missing required documentation: $text" >&2
exit 1
}
done
printf 'README documentation contract passed\n'
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh
set -eu
repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT HUP INT TERM
snapshot() {
for path in \
"$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.docker/config.json" \
/etc/apt/apt.conf.d /etc/docker/daemon.json /var/lib/proxyctl \
/etc/systemd/system/docker.service.d
do
if [ -d "$path" ]; then
find "$path" -type f -print0 2>/dev/null | sort -z | xargs -0 -r sha256sum 2>/dev/null || ls -ld "$path"
elif [ -r "$path" ]; then
sha256sum "$path"
elif [ -e "$path" ]; then
ls -ld "$path"
else
printf 'absent %s\n' "$path"
fi
done
if command -v gsettings >/dev/null 2>&1; then
gsettings list-recursively org.gnome.system.proxy 2>&1 || true
else
printf 'gsettings absent\n'
fi
if command -v systemctl >/dev/null 2>&1; then
systemctl show docker.service -p ActiveState -p SubState -p NRestarts -p ExecMainStartTimestampMonotonic 2>&1 || true
else
printf 'systemctl absent\n'
fi
}
snapshot >"$work/before"
(
cd "$repo"
go test ./...
)
snapshot >"$work/after"
cmp "$work/before" "$work/after"
printf 'full Go suite passed without changing host Shell, APT, Docker, GNOME, or system-service state\n'
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
set -eu
repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT HUP INT TERM
VERSION=v1.0.0 "$repo/scripts/build-release.sh" "$work/first"
VERSION=v1.0.0 "$repo/scripts/build-release.sh" "$work/second"
for arch in amd64 arm64; do
binary="proxyctl-linux-$arch"
test -x "$work/first/$binary"
cmp "$work/first/$binary" "$work/second/$binary"
done
cmp "$work/first/SHA256SUMS" "$work/second/SHA256SUMS"
(
cd "$work/first"
sha256sum -c SHA256SUMS
)
amd64_format=$(file -b "$work/first/proxyctl-linux-amd64")
arm64_format=$(file -b "$work/first/proxyctl-linux-arm64")
printf '%s\n' "$amd64_format" | grep -Eq 'ELF 64-bit.*(x86-64|x86_64)'
printf '%s\n' "$arm64_format" | grep -Eq 'ELF 64-bit.*(ARM aarch64|aarch64)'
# Only execute the binary matching the build host; format checks cover both.
case $(go env GOARCH) in
amd64) test "$($work/first/proxyctl-linux-amd64 version --short)" = v1.0.0 ;;
arm64) test "$($work/first/proxyctl-linux-arm64 version --short)" = v1.0.0 ;;
esac
printf 'amd64: %s\narm64: %s\nrelease automation is reproducible and checksums verify\n' "$amd64_format" "$arm64_format"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
set -eu
repo=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT HUP INT TERM
home="$work/home"
config="$work/config"
state="$work/state"
bin="$work/bin"
mkdir -p "$home" "$config" "$state" "$bin"
snapshot_host() {
for path in \
"$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.docker/config.json" \
/etc/apt/apt.conf.d/99proxyctl /etc/docker/daemon.json \
/var/lib/proxyctl/docker-daemon.json
do
if [ -r "$path" ]; then
sha256sum "$path"
elif [ -e "$path" ]; then
ls -ld "$path"
else
printf 'absent %s\n' "$path"
fi
done
if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.system.proxy mode 2>&1 || true
else
printf 'gsettings absent\n'
fi
}
snapshot_host >"$work/host-before"
(
cd "$repo"
GOFLAGS='-mod=readonly' GOBIN="$bin" go install ./cmd/proxyctl
)
test "$("$bin/proxyctl" version --short)" != ""
mv "$bin/proxyctl" "$bin/proxyctl-production"
(
cd "$repo"
GOFLAGS='-mod=readonly -tags=proxyctl_process_test' GOBIN="$bin" go install ./cmd/proxyctl
go test ./internal/app -run 'TestUserCanCreateAndActivatePersistentSingleProxyProfile|TestApplicationCoordinatesMultipleRealAndFakeHermeticAdaptersThroughPartialFailure' -count=1
)
env_run() {
env -i \
PATH="$PATH" HOME="$home" XDG_CONFIG_HOME="$config" XDG_STATE_HOME="$state" \
SHELL=/bin/bash PROXYCTL_PROCESS_TEST_HOME="$home" \
"$bin/proxyctl" "$@"
}
test "$(env_run version --short)" != ""
doctor_output=$(env_run doctor 2>&1 || true)
printf '%s\n' "$doctor_output" | grep -q 'Profile validity'
printf 'no\n' | env_run reset | grep -q 'Reset cancelled'
printf 'yes\n' | env_run reset | grep -q 'shell: skipped (Disabled)'
env_run shell install | grep -q 'installed'
grep -q 'proxyctl shell integration' "$home/.bashrc"
env_run shell uninstall | grep -q 'uninstalled'
if grep -q 'proxyctl shell integration' "$home/.bashrc"; then
echo 'Shell integration remained after uninstall' >&2
exit 1
fi
rm "$bin/proxyctl" "$bin/proxyctl-production"
test ! -e "$bin/proxyctl"
test ! -e "$bin/proxyctl-production"
snapshot_host >"$work/host-after"
cmp "$work/host-before" "$work/host-after"
printf 'clean-machine verification passed: go install, Profile workflow, hermetic Target workflow, doctor, reset cancellation/confirmation, Shell install/uninstall, binary removal, and unchanged host state\n'