181 lines
24 KiB
Markdown
181 lines
24 KiB
Markdown
# proxyctl v1 specification
|
|
|
|
## Problem Statement
|
|
|
|
Linux users frequently need one proxy configuration to apply across several independent configuration surfaces. Changing Shell environment variables is insufficient for daemons, container defaults, desktop applications, and commands whose environment changes across privilege boundaries. Each surface has different configuration formats, state semantics, permissions, and restart requirements.
|
|
|
|
Users currently have to remember which files and commands control each surface, repeat the same proxy details, distinguish Docker daemon behavior from container/build behavior, preserve pre-existing settings, and diagnose partial failures manually. This is error-prone and makes switching between locations or networks unnecessarily risky.
|
|
|
|
## Solution
|
|
|
|
Build `proxyctl`, a keyboard-driven Linux TUI that coordinates one Active Profile across independently switchable Targets. The default screen shows only Available Targets, their detected state, the Active Profile, a concise explanation of the selected Target, and immediate operation feedback. Proxy Profile management lives on a separate page.
|
|
|
|
The first release supports Bash/Zsh Shell environments, APT, systemd-managed Docker daemon proxying, Docker container/build defaults, and GNOME desktop proxy settings. Every Target owns its detection, application, restoration, compatibility checks, and explanation behind one common Interface.
|
|
|
|
`proxyctl` preserves user-owned configuration wherever possible. It prefers a tool-owned overlay; when a Target has only a single mutable configuration surface, it records enough original state to restore it safely. It never silently discards unsupported Proxy Profile fields or Bypass rules.
|
|
|
|
## User Stories
|
|
|
|
1. As a Linux user, I want to open `proxyctl` without arguments, so that I immediately see the proxy controls relevant to my machine.
|
|
2. As a Linux user, I want the main screen to show the Active Profile, so that I know which proxy settings an enable action will apply.
|
|
3. As a Linux user, I want only Available Targets on the main screen, so that absent software does not clutter routine use.
|
|
4. As a curious user, I want the help page to list every Target supported by `proxyctl`, so that I can discover integrations not available on my current system.
|
|
5. As a troubleshooting user, I want unavailable Target explanations to state what was not detected, so that I understand why an integration is hidden.
|
|
6. As a keyboard-focused user, I want to navigate with arrow keys or `j`/`k`, so that the TUI feels natural in a terminal.
|
|
7. As a keyboard-focused user, I want to toggle the selected Target with Space, so that routine switching is fast.
|
|
8. As a keyboard-focused user, I want `p` to open Proxy Profile management, so that configuration editing is separate from routine switching.
|
|
9. As a keyboard-focused user, I want `?` to open help, so that explanations are always discoverable.
|
|
10. As a keyboard-focused user, I want `r` to refresh detection, so that newly installed or removed software is reflected without restarting.
|
|
11. As a keyboard-focused user, I want `q` or Escape to leave the current page or exit, so that navigation is predictable.
|
|
12. As a user in a mouse-capable terminal, I want the keyboard Interface to remain complete without mouse support, so that behavior is consistent across terminals.
|
|
13. As a Linux user, I want the selected Target's summary visible on the main screen, so that I understand what its switch affects.
|
|
14. As a Linux user, I want each Target explanation to state what it does not affect, so that names such as Docker daemon are not misleading.
|
|
15. As a Linux user, I want each Target explanation to describe expected permissions and side effects, so that I can anticipate failures or restarts.
|
|
16. As a Linux user, I want detailed help for each Target, so that I can inspect configuration scope and detection evidence when needed.
|
|
17. As a user with several networks, I want to create named Proxy Profiles, so that I can reuse proxy settings without retyping them.
|
|
18. As a user with several Proxy Profiles, I want exactly one of them selected as the Active Profile, so that all Enabled Targets have one unambiguous source.
|
|
19. As a first-time user, I want the Target screen to remain usable when no Proxy Profiles exist, so that I am not forced through an onboarding wizard.
|
|
20. As a first-time user, I want an attempt to enable a Target without an Active Profile to explain the problem and point me to Profile management, so that the failed action is understandable.
|
|
21. As a user with no Active Profile, I want to remain able to disable previously managed Targets, so that stale system configuration can always be removed.
|
|
22. As a user editing a Proxy Profile, I want a default Single proxy mode, so that common local-proxy setups require only one URL and a Bypass list.
|
|
23. As an advanced user, I want separate HTTP, HTTPS, and ALL/SOCKS values, so that I can model proxies with protocol-specific endpoints.
|
|
24. As a user switching from Single proxy to advanced mode, I want my existing value preserved and expanded, so that changing modes does not discard input.
|
|
25. As a user entering a proxy endpoint, I want malformed URLs and unsupported schemes rejected before application, so that bad configuration never reaches a Target.
|
|
26. As a user with an HTTP proxy, I want `http` and `https` proxy URL schemes supported, so that standard enterprise proxies can be represented.
|
|
27. As a user with a SOCKS proxy, I want `socks5` and `socks5h` kept distinct, so that DNS resolution behavior remains explicit.
|
|
28. As a user with internal resources, I want Bypass entries for exact hostnames, domain suffixes, IPv4, IPv6, and CIDR ranges, so that internal traffic can avoid the proxy.
|
|
29. As a user editing Bypass rules, I want entries normalized and deduplicated, so that the Proxy Profile stays understandable.
|
|
30. As a security-conscious user, I want a Target to reject an incompatible Bypass rule rather than silently drop it, so that traffic is not unexpectedly sent through a proxy.
|
|
31. As a user with an authenticated proxy, I want credentials representable in proxy URLs, so that authenticated endpoints work.
|
|
32. As a user with an authenticated proxy, I want passwords obscured in the TUI and removed from logs and errors, so that credentials are not casually exposed.
|
|
33. As a user who values portable configuration, I want Proxy Profiles stored in a human-readable format under the standard user configuration area, so that I can inspect and back them up.
|
|
34. As a user storing credentials, I want the Profile configuration restricted to my user account, so that other local users cannot read it.
|
|
35. As a Linux user, I want Shell proxy variables to include conventional upper- and lower-case forms, so that common command-line tools inherit them reliably.
|
|
36. As a Bash or Zsh user, I want a one-time Shell integration installer, so that later TUI changes can update my current parent Shell after the TUI exits.
|
|
37. As a Bash or Zsh user, I want to uninstall the Shell integration cleanly, so that `proxyctl` leaves no stale startup hook.
|
|
38. As a user of an unsupported Shell, I want the Shell Target hidden and the help page to explain the supported shells, so that the limitation is explicit.
|
|
39. As an APT user, I want persistent APT proxy control, so that package operations remain consistent even when an interactive Shell environment is not propagated.
|
|
40. As an APT user with existing configuration, I want `proxyctl` to preserve that configuration beneath its own overlay, so that disabling the Target restores my prior setup.
|
|
41. As a Docker user, I want Docker daemon proxying separate from Docker container/build defaults, so that I can control registry access independently from injected container variables.
|
|
42. As a Docker user, I want the Docker daemon Target to explain that it affects daemon network operations, so that I do not mistake it for container networking.
|
|
43. As a Docker user, I want a warning before Docker daemon configuration triggers a restart, so that I can avoid disrupting running work unexpectedly.
|
|
44. As a Docker user, I want cancellation of the restart warning to make no changes, so that declining is safe.
|
|
45. As a Docker user, I want daemon configuration and restart failures reflected immediately, so that the TUI never implies a change succeeded when it did not.
|
|
46. As a Docker user, I want container/build defaults to affect only future containers and builds, so that the behavior matches Docker's documented model.
|
|
47. As a GNOME user, I want a desktop proxy Target, so that applications using GNOME system network settings receive the Active Profile.
|
|
48. As a non-GNOME user, I want the GNOME Target hidden, so that unsupported desktop settings are not offered.
|
|
49. As a user toggling a Target, I want an applying state followed immediately by success or a red error, so that every Space press has visible feedback.
|
|
50. As a user whose enable operation fails before writing anything, I want the previous Target state preserved, so that a failed operation does not move the switch.
|
|
51. As a user whose operation partially changes a Target, I want `proxyctl` to re-inspect the real configuration, so that the displayed state reflects what is actually present.
|
|
52. As a user switching the Active Profile, I want every Enabled Target updated to the new Profile, so that enabled configuration remains coordinated.
|
|
53. As a user switching the Active Profile, I want each Target row to update as its operation completes, so that progress and failures are visible immediately.
|
|
54. As a user facing a partial Active Profile switch, I want successful Targets to remain updated and failed Targets to become Drifted when appropriate, so that the tool does not hide partial success.
|
|
55. As a user, I want an Enabled Target to mean its actual managed configuration matches the Active Profile, so that the status has a precise meaning.
|
|
56. As a user, I want a Drifted Target to identify a managed configuration that no longer matches the Active Profile, so that I know it needs repair or removal.
|
|
57. As a user with pre-existing proxy settings, I want a recognized non-`proxyctl` configuration shown as External, so that the tool does not claim ownership of it.
|
|
58. As a user disabling a Target that previously had External configuration, I want that configuration restored and the state to return to External, so that my prior setup is preserved.
|
|
59. As a user with an unrecognized configuration source, I accept that absence of positive detection is displayed as Disabled, so that the first release avoids an Unknown state.
|
|
60. As a user inspecting an External state, I want the details to identify the evidence that was detected, so that I can locate the original configuration.
|
|
61. As a safety-conscious user, I want `proxyctl` to prefer dedicated tool-owned configuration fragments, so that it avoids rewriting unrelated user files.
|
|
62. As a safety-conscious user, I want original values recorded when an isolated fragment is impossible, so that disabling can restore the previous state.
|
|
63. As a safety-conscious user, I want configuration writes to be atomic, so that a crash does not leave a partially written file.
|
|
64. As a user, I want only one `proxyctl` operation to mutate state at a time, so that concurrent sessions cannot corrupt recovery metadata.
|
|
65. As an unprivileged user, I want writable Targets to work normally and permission failures to be reported plainly, so that the TUI does not surprise me with a password prompt.
|
|
66. As an unprivileged user, I want `proxyctl` never to initiate privilege escalation, so that authority changes remain under my control.
|
|
67. As a user who explicitly starts `proxyctl` through `sudo`, I want the invoking user to remain the Profile and user-configuration owner, so that I do not see an unrelated root configuration.
|
|
68. As a user who explicitly starts `proxyctl` through `sudo`, I want system Targets to use the available root authority while user Targets still operate on the invoking user's configuration, so that mixed-scope behavior is predictable.
|
|
69. As a root login user, I want root to be treated as the configuration owner when there is no invoking user, so that direct root sessions remain coherent.
|
|
70. As a troubleshooting user, I want `proxyctl doctor` to report configuration, permission, command, and Target detection problems, so that I can diagnose failures without guessing.
|
|
71. As a user removing `proxyctl`, I want `proxyctl reset` to disable all managed Targets after confirmation and restore prior configuration, so that uninstalling does not strand proxy settings.
|
|
72. As a Shell user, I want explicit Shell install and uninstall maintenance commands, so that startup-file changes are reversible.
|
|
73. As a user, I want a version command, so that I can report the exact build when seeking help.
|
|
74. As a user, I want local diagnostic logs with bounded growth, so that failures can be investigated without consuming unlimited disk space.
|
|
75. As a privacy-conscious user, I want no telemetry or automatic update checks, so that `proxyctl` never contacts the network on its own.
|
|
76. As a user, I want `proxyctl` not to test proxy connectivity automatically, so that toggling configuration has no hidden network side effects.
|
|
77. As a Linux amd64 user, I want a single-file release binary, so that installation is straightforward.
|
|
78. As a Linux arm64 user, I want an equivalent release binary, so that devices such as ARM development boards are supported.
|
|
79. As a release consumer, I want checksums for release artifacts, so that I can verify downloads.
|
|
80. As a developer, I want `go install` support, so that I can build and install directly from source.
|
|
|
|
## Implementation Decisions
|
|
|
|
- The product and executable are named `proxyctl`.
|
|
- The implementation uses Go and Bubble Tea.
|
|
- The default invocation opens the TUI. Maintenance commands cover Shell integration installation/removal, diagnostics, full managed-state reset, and version reporting. Scriptable Profile switching and Target toggling are not part of the first CLI Interface.
|
|
- The TUI is keyboard-only in the first release. Its default page is the Target switch panel; Proxy Profile management and help are separate pages.
|
|
- The Target panel shows only Available Targets. The help page lists all built-in Targets and explains failed detection.
|
|
- The supported first-release Target Adapters are Bash/Zsh Shell environment, APT, systemd-managed Docker daemon, Docker container/build defaults, and GNOME desktop proxy.
|
|
- The core TUI and Shell behavior target general Linux. APT support is limited to Debian-family systems, Docker daemon support to systemd-managed installations, Docker container/build support to the standard Docker client configuration model, and GNOME support to systems exposing the expected settings schema.
|
|
- Target integrations are compiled in. There is no plugin Interface, arbitrary-command Target, or user-authored executable hook.
|
|
- One Application Module coordinates Proxy Profiles, Active Profile transitions, Target operations, persistence, and user-facing results. Both TUI and maintenance commands consume its Interface.
|
|
- A single Target seam hides platform-specific behavior. Target Adapters expose description, inspection, and desired-state operations; callers do not know configuration paths or system commands.
|
|
- Target dependencies such as filesystem access and command execution are accepted at internal seams rather than created globally, permitting hermetic tests.
|
|
- A Proxy Profile has a simple Single proxy editing mode and an advanced HTTP/HTTPS/ALL-SOCKS mode. Single mode expands into protocol values without losing the original value when advanced editing is opened.
|
|
- Accepted URL schemes are HTTP, HTTPS, SOCKS5, and SOCKS5H. Scheme semantics are preserved, and each Target validates compatibility rather than converting unsupported schemes.
|
|
- Bypass rules support exact hostnames, domain suffixes, IPv4, IPv6, and CIDR. They are normalized and deduplicated. A Target must reject an incompatible rule instead of dropping it.
|
|
- Proxy URLs may contain credentials. The human-readable Profile configuration is stored under the XDG user configuration location with owner-only permissions. Secrets are obscured in the TUI and redacted from diagnostics and logs.
|
|
- User-authored configuration, generated Shell integration, runtime recovery metadata, generated Shell environment, and logs are separated according to XDG configuration and state conventions.
|
|
- Configuration and state writes use temporary output followed by atomic replacement. Cross-process mutation is serialized with a lock.
|
|
- `proxyctl` prefers tool-owned overlays or fragments. When a Target cannot provide an isolated overlay, recovery state records the original value and the last applied fingerprint.
|
|
- Target inspection distinguishes Enabled, Disabled, External, and Drifted. External is shown only when an Adapter positively recognizes a non-`proxyctl` proxy setting. If no recognized external setting is found, the state is Disabled; there is no Unknown state in the first release.
|
|
- An operation changes the displayed switch only after success. Failures are rendered immediately on the affected row with full redacted detail in the explanation area.
|
|
- After any partial write or command failure, the Adapter re-inspects the Target and returns the state implied by actual configuration.
|
|
- Changing the Active Profile attempts to update every Enabled Target and reports progress per Target. Cross-Target rollback is not attempted. Failed Targets may become Drifted while successful Targets remain Enabled on the new Profile.
|
|
- No Active Profile is permitted when no Proxy Profiles exist. Attempts to enable a Target in that state fail with a direct route to Profile management. Disabling remains available.
|
|
- Deleting the final Proxy Profile requires confirmation. Existing managed Target configuration is retained and becomes Drifted rather than being modified implicitly.
|
|
- Shell behavior uses a one-time Bash/Zsh integration because a child TUI process cannot mutate its parent Shell environment. The wrapper reloads generated exports/unsets after the TUI exits. Unsupported shells do not expose the Shell Target.
|
|
- Shell output includes conventional upper- and lower-case proxy environment names and explicit unsets when disabled.
|
|
- Docker daemon and Docker container/build defaults remain separate Targets because their documented effects and configuration surfaces differ.
|
|
- Docker daemon changes require an explicit warning before any mutation. Confirmation applies the configuration, reloads the manager as required, and restarts Docker immediately. Cancellation performs no mutation; there is no Pending Restart state.
|
|
- APT remains a dedicated Target for persistence and privilege-boundary reliability even though APT supports proxy environment variables.
|
|
- `proxyctl` never invokes `sudo`, `pkexec`, or another privilege escalation mechanism. Permission failures are normal operation results.
|
|
- When already running under `sudo`, `proxyctl` uses the invoking user's identity and XDG locations for Profiles and user Targets while allowing system Targets to use current authority. With no invoking user, the effective root user is the owner.
|
|
- Local logs contain redacted operation diagnostics, have bounded growth, and never contain unredacted proxy credentials.
|
|
- The program has no telemetry, automatic update requests, or automatic proxy connectivity checks.
|
|
- Release output consists of checksum-verifiable single binaries for Linux amd64 and arm64, plus source installation through Go tooling. Distribution-specific packages are deferred.
|
|
|
|
## Testing Decisions
|
|
|
|
- Good tests assert externally observable behavior through the highest practical seam: returned application state, rendered user-facing outcomes, managed configuration effects, restoration behavior, and invoked system operations. They do not assert private helper calls, internal data layout, or incidental implementation order.
|
|
- The primary test seam is the Application Module Interface used by both the TUI and maintenance commands. Tests submit the same intents a user can trigger and assert resulting snapshots and operation results.
|
|
- Application tests cover Proxy Profile CRUD, Active Profile selection and deletion, Single/Advanced validation, absence of an Active Profile, per-Target immediate feedback, partial Profile switching, reset, diagnostics, and credential redaction.
|
|
- The shared Target Interface is the second seam. Every real Target Adapter is run through common contract tests for availability detection, Disabled, Enabled, External, Drifted, enable, disable, restoration, idempotency, incompatible Proxy Profile rejection, permission failure, partial failure, and redaction.
|
|
- Adapter tests use isolated XDG directories, temporary filesystem roots, and a fake command runner. They must never modify the developer machine's Shell startup files, APT configuration, Docker daemon, Docker client configuration, GNOME settings, or systemd state.
|
|
- Target-specific tests cover Bash and Zsh integration behavior, APT overlay precedence and restoration, Docker daemon restart confirmation and command failures, Docker container/build semantics, and GNOME settings snapshot/restoration.
|
|
- `sudo` identity behavior is tested using injected process identity and environment data rather than requiring tests to run as root.
|
|
- TUI tests remain thin: key-to-intent mapping, page navigation, visible Active Profile, status/error rendering, and selected-Target explanation. Business behavior is not duplicated at the rendering layer.
|
|
- Process-level smoke tests cover default startup, Shell integration maintenance commands, doctor, reset confirmation, and version output.
|
|
- Filesystem tests verify owner-only secret-bearing configuration, atomic replacement, lock behavior, original-state preservation, and cleanup.
|
|
- Failure tests verify that no operation reports success before effects complete, cancellation mutates nothing, unsupported Bypass rules are never silently omitted, and logs/errors redact credentials.
|
|
- There is no existing codebase test prior art. The first tracer implementation must establish reusable application and Target contract harnesses for later Adapters.
|
|
|
|
## Out of Scope
|
|
|
|
- Starting, stopping, configuring, or supervising proxy programs such as Clash, sing-box, or Xray.
|
|
- Proxy subscriptions, node management, latency testing, automatic node selection, and routing-rule management.
|
|
- Transparent proxying, TUN devices, iptables, nftables, or other traffic interception.
|
|
- Automatic proxy connectivity checks.
|
|
- Arbitrary systemd service proxy configuration.
|
|
- Git, DNF, npm, pip, or other tools that already consume conventional Shell proxy variables in the intended first-release scenarios.
|
|
- Firefox-specific policy or profile modification.
|
|
- Chromium-specific launch-flag management.
|
|
- KDE or other desktop proxy integrations beyond GNOME.
|
|
- Snap/Ubuntu Core proxy configuration.
|
|
- Fish and other Shells beyond Bash and Zsh.
|
|
- Mouse interaction.
|
|
- Plugins, custom Targets, and arbitrary user-defined commands.
|
|
- A complete headless CLI for Profile and Target operations.
|
|
- Internal privilege escalation or graphical authorization agents.
|
|
- Cross-Target atomic transactions and rollback.
|
|
- Distribution-specific deb, rpm, AUR, Snap, Flatpak, or similar packages.
|
|
- Windows, macOS, and non-Linux Unix platforms.
|
|
|
|
## Further Notes
|
|
|
|
- Domain terminology is defined in the repository glossary and must be used consistently in code, tests, issues, and user-facing explanations.
|
|
- The supporting research corrects an important premise: APT, DNF, and Git do support conventional proxy environment variables. Dedicated integrations are justified by persistence, process, privilege, or desktop boundaries rather than a blanket lack of environment-variable support.
|
|
- Docker daemon and Docker container/build defaults are deliberately separate because Docker documents them as independent configuration surfaces with different effects.
|
|
- GNOME desktop proxy support is preferred over browser-specific modification because Chromium consumes system network settings and Firefox can use a system proxy mode.
|
|
- External detection is explicitly best-effort. The first release favors a simpler four-state Interface and documents detection evidence rather than claiming exhaustive discovery of every possible configuration source.
|
|
- No ADR is currently required: the decisions are contained within this product specification and do not yet meet the threshold of a surprising, costly-to-reverse architectural trade-off proven against an existing implementation.
|