Support Linux in ts dev proxy - #1171
ChristianPavilonis wants to merge 2 commits into
Conversation
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Extends ts dev proxy from macOS-only to macOS + Linux: dependency and cfg gates widened, Chrome/Chromium and Firefox launcher discovery added, and a new trust.rs that manages the dev CA through the user's NSS database. The CA-trust work is the substantial part, and it is careful — imports are journalled before NSS is touched, full DER equality authorizes every mutation, and two lock layers serialize CA commands and shared-store writes.
No blocking findings. The comments below are design observations, one seedling, and documentation drift; none of them gate the merge.
All comments are prose — nothing is offered as a one-click suggestion, because the two documentation lines worth changing fall outside the diff hunks, the script fix is a file-mode change, and the remainder are judgement calls that should stay the author's.
Non-blocking
🤔 thinking
- Whole-store NSS export scan hard-fails on any single bad entry — see inline at
crates/trusted-server-cli/src/commands/dev/proxy/trust.rs:283 - Every
try_lockfailure reports as "another operation is running" — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/trust.rs:59 - A rejected install still creates a new NSS store — see inline at
crates/trusted-server-cli/src/commands/dev/proxy/trust.rs:332
🌱 seedling
- Persisted nickname derives from
DefaultHasher— see inline atcrates/trusted-server-cli/src/commands/dev/proxy/trust.rs:339
⛏ nitpick
ConfigError::Browseromitssafarion macOS — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/config.rs:50- Firefox trust flags drifted from the documented manual command — below
- The guide's
--helptranscript is now wrong on Linux — below - New script is not executable — below
👍 praise
- Trust-mutation design and its failure-path assertions — see inline at
crates/trusted-server-cli/tests/proxy_trust_linux.rs:76
Cross-cutting / body-level findings
-
⛏ Firefox trust flags drifted from the documented manual command —
import_firefox(trust.rs:94) narrowed the NSS trust flags fromCT,,(the previousbrowser.rscode) toC,,. Fine on the merits: server-authentication CA trust is all a proxy needs, and dropping the client-auth bit is tighter. But the manual import command indocs/guide/ts-dev-proxy.md:200still says-t "CT,,", so a developer following the documented path now grants different trust bits than--launch firefoxdoes. Either align the doc block toC,,or note why they differ. (Body-level: line 200 is outside the diff hunks, so it can't carry an inline comment.) -
⛏ The guide's
--helptranscript is now wrong on Linux —docs/guide/ts-dev-proxy.md:347advertises--launch <LIST> Browsers to launch (chrome,firefox,safari or all), butsafariis rejected on Linux by the newcfginBrowser::parse_list. Line 355's--ca-dirdefault reads~/Library/Application Support/trusted-server/dev-proxy on macOSwith no Linux equivalent, while the prose added above it documents$XDG_DATA_HOME/trusted-server/dev-proxy. Separately, and pre-existing onmain: neither line matches clap's real output — the actual--launchhelp string is "Browsers to launch + configure (comma list orall)", andca_diris anOption<String>with nodefault_value, so clap prints no default at all. The transcript is hand-maintained and already drifting; this PR is a reasonable moment to regenerate or trim it. -
⛏
scripts/test-linux-dev-proxy-browser.pyis mode644— every other file inscripts/is755. It carries a#!/usr/bin/env python3shebang but cannot be executed directly. Nothing breaks, since the guide invokes it aspython3 scripts/..., butgit update-index --chmod=+xwould make it consistent withtest-cli.shand friends.
CI Status
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- format-docs: PASS (required)
- format-typescript: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- vitest: PASS
- prepare integration artifacts: PASS
- CLAUDE.md symlink guard: PASS
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- CodeQL: SKIPPED
- Analyze (rust): PENDING
- integration tests: PENDING
- integration tests (Fastly EC lifecycle): PENDING
- browser integration tests: PENDING
No failed checks. Four checks were still in progress at review time; the Linux and macOS CLI jobs this PR actually exercises have both passed.
|
|
||
| /// NSS named exports include every certificate with the same subject. | ||
| /// Reject a different certificate with that subject before importing anything. | ||
| fn check_subject_conflicts(entry: &Destination) -> Result<()> { |
There was a problem hiding this comment.
🤔 thinking — ca install scans and exports every nickname in the shared user NSS database, and hard-fails on any one of them.
check_subject_conflicts walks all nicknames from nicknames() and runs certutil -L -n <nick> -a per entry. Any failure — a duplicate nickname, an orphaned entry, a cert whose named export isn't parseable PEM — propagates as TrustError::Command / TrustError::State and aborts the install.
On the fixture databases in tests/proxy_trust_linux.rs that is fine. On a real developer's ~/.pki/nssdb (corporate roots, leftovers from mkcert or earlier nss-tools experiments) a single bad entry makes ca install permanently unusable, and there is no documented recovery path other than editing the NSS store by hand — which docs/guide/ts-dev-proxy.md tells the user not to do.
Fail-closed is right for an identity check, but a per-nickname export failure isn't an identity conflict. Two options that keep the guarantee:
- Only hard-fail when a successfully-parsed certificate has a matching subject and different bytes; treat an unparseable/failed export as "not a conflict for this subject" and warn.
- Keep the strict behaviour and document the escape hatch (which nickname failed, and what to do about it) — the
checked()attach already carries the certutil stderr, so the diagnostic exists, it just isn't actionable in the guide.
| .mode(0o600) | ||
| .open(path) | ||
| .change_context(TrustError::Io)?; | ||
| file.try_lock().change_context(TrustError::Busy)?; |
There was a problem hiding this comment.
🤔 thinking — every try_lock failure is reported as "another operation is running".
TryLockError covers WouldBlock and genuine I/O failures — ENOLCK, or a filesystem that doesn't support advisory locks. NFS-mounted home directories are a live case on Linux, which is the platform this PR is adding.
Both map to TrustError::Busy, so the user is told another CA or NSS trust operation is running; retry after it exits and retries forever against a condition that will never clear. Splitting the two keeps the diagnostic honest:
match file.try_lock() {
Ok(()) => {}
Err(std::fs::TryLockError::WouldBlock) => return Err(Report::new(TrustError::Busy)),
Err(err) => return Err(Report::new(err).change_context(TrustError::Io)),
}Apply manually — worth confirming the exact TryLockError variant names against the pinned 1.95 toolchain before committing to this shape.
|
|
||
| fn install_into(ca_dir: &Path, cert_path: &Path, database: &Path) -> Result<()> { | ||
| let mut entries = read_record(ca_dir)?; | ||
| fs::create_dir_all(database).change_context(TrustError::Io)?; |
There was a problem hiding this comment.
🤔 thinking — a rejected install still leaves a freshly created NSS store behind.
install_into runs fs::create_dir_all(database) here and initialize() (which runs certutil -N) on the next line, both before the contains() / check_subject_conflicts() preflight further down. So an install rejected for a same-subject conflict creates ~/.local/share/pki/nssdb with a new cert9.db / key4.db that did not exist beforehand.
It's benign — browsers create that store themselves — but it's a side effect of a command that otherwise advertises "rejected before mutation", and real_nss_same_subject_install_conflict_is_rejected_before_mutation doesn't cover it because the database already exists in that fixture. If the ordering can't move (the preflight needs a queryable database), it may be worth saying so in a comment here.
| initialize(&database)?; | ||
| let certificate = certificate(cert_path)?; | ||
| // The hash only names the entry. Full DER equality authorizes all mutations. | ||
| let mut hash = DefaultHasher::new(); |
There was a problem hiding this comment.
🌱 seedling — the persisted nickname is derived from DefaultHasher, whose output isn't stable across Rust releases.
The resulting ts-dev-proxy-{:016x} string is written to managed-nss-trust.json and into the NSS database. Today that's safe, and the comment right above says why: the entry lookup keys on (database, certificate) and uninstall replays the recorded nickname, so cross-version stability is never required on the happy path.
The one case it bites is recovery: journal lost, or a different --ca-dir used, while the NSS import survives. The orphan can then only be re-derived under the exact toolchain that wrote it, so ca uninstall can never reach it. A stable digest over the DER — SHA-256 is already reachable through the rustls/ring stack this crate pulls in — would make that recoverable.
Not for this PR; the 16-hex-digit shape read_record validates would be unchanged either way.
| /// An unknown browser name was passed to `--launch`. | ||
| #[display("unknown browser `{value}` (expected chrome|firefox|safari|all)")] | ||
| /// An unknown or unsupported browser was passed to `--launch`. | ||
| #[display("unsupported browser `{value}` (use chrome|firefox|all; safari is macOS-only)")] |
There was a problem hiding this comment.
⛏ nitpick — the message drops safari from the accepted list on macOS, where it is accepted.
A macOS user who typos --launch chrom is told to "use chrome|firefox|all", which reads as though safari isn't an option on the platform they're actually on. The trailing clause is doing double duty as both a restriction and an implicit list member.
Something that reads correctly on both targets without needing a cfg:
#[display("unsupported browser `{value}` (use chrome|firefox|all, plus safari on macOS)")]Apply manually — left as prose rather than a one-click suggestion so the wording stays yours.
| bin | ||
| } | ||
|
|
||
| fn assert_rotation_fails_unchanged(&self, path: &Path) { |
There was a problem hiding this comment.
👍 praise — assert_rotation_fails_unchanged re-asserting the key, the certificate and the journal bytes on every failure path is the right shape for testing a trust-store mutator: it turns "rotation never proceeds on an unconfirmed revoke" into a property rather than a comment.
Same for the design it's testing — record-before-mutate journal, full DER equality (not the nickname) as the authorization check, a CA-directory lock plus a per-NSS-directory lock so distinct --ca-dirs serialize against a shared store, and the same-subject preflight that stops NSS's named-export ambiguity from silently rebinding someone else's certificate. That last one is a genuinely non-obvious NSS behaviour to have found and closed before shipping.
aram356
left a comment
There was a problem hiding this comment.
Summary
Extends ts dev proxy from macOS-only to macOS + Linux: NSS-based CA trust, native browser discovery, a Linux CI matrix, and substantial regression coverage. The platform split is well constructed and the shared/cfg'd division is right — certutil, checked, lock, lock_file, and import_firefox are genuinely shared, and only install/uninstall/ensure_can_generate are conditional, which is correct since the keychain and NSS have no common substrate.
Two blocking defects, both in the certutil -L listing parser and both reproduced against a real certutil. They share one root cause: scraping a fixed-width human-readable table cannot losslessly recover an NSS nickname. Their blast radius is wider than it first appears — nicknames() feeds contains(), which uninstall and regenerate also depend on, so an unrelated pre-existing entry in the user's NSS database can make the dev CA impossible to install, remove, or rotate.
All inline comments describe fixes in prose; none is expressible as a one-click suggestion (findings 1 and 2 need a parsing-strategy change plus tests, and the rest either span two call sites or touch lines outside the diff hunks).
Blocking
🔧 wrench
nicknames()strips real trailing whitespace, blocking install, uninstall, and regenerate — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/trust.rs:264- A newline in any nickname hard-fails the same three commands — see inline at
crates/trusted-server-cli/src/commands/dev/proxy/trust.rs:254
Non-blocking
♻️ refactor
Browser::Safariis representable but unconstructible on Linux — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/config.rs:113- Duplicated
which+canonicalizelookup closure — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/browser.rs:278-281
🤔 thinking
contains(entry)?;discards itsboolbeside an identical call that uses it — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/trust.rs:357
⛏ nitpick
safari is macOS-onlyhint is emitted on macOS too — see inline atcrates/trusted-server-cli/src/commands/dev/proxy/config.rs:50
Cross-cutting / body-level findings
-
📝 Checked and found correct, recorded so these are not re-raised — several plausible-looking concerns were investigated against the running system and are not defects:
DefaultHashernicknames are deterministic across processes (and the journal is authoritative regardless, so cross-release hash drift is survivable);File::try_lockis stable on the pinned 1.95.0 and is OS-released onSIGKILL; the macOSerrSecItemNotFound→ exit-code-44 assumption is correct (verified againstsecurity find-certificateon a real host); switching the macOS path from.status()to.output()is safe becausesecurity add-trusted-certauthenticates via a GUI dialog rather than a TTY prompt;write_recordpersists at mode0600;uninstall's ordering is genuinely fail-safe (the record only shrinks after a removal is confirmed); and the absence of a#[cfg(not(any(macos, linux)))]arm inchrome_command/firefox_commandis fine because the wholeproxymodule and its dependency set are gated atdev/mod.rs:1andCargo.toml:35. -
📝 On the platform-abstraction question: do not introduce a
TrustStoretrait. The#[cfg]resolves at compile time, so there is never more than one implementation in a given build — a trait would add a vtable or a generic parameter for zero dispatch, and object-safety would force&selfonto what are genuinely free functions over a&Path. Cfg'd free functions behind a re-export is the correct idiom here and matches whatstd::sysdoes. The asymmetry between the inlinemod linux(9 helpers, aDestinationtype, its own tests) and the bare macOS items (one helper) tracks a real size asymmetry and is justified. Two small follow-ups worth considering, neither blocking: a one-line doc comment above thepub(super) use linux::{...}re-export naming the three-function platform contract (nothing currently makes the compiler check that the two arms agree in signature — they would diverge silently until someone builds the other platform); and ifmod linuxgrows past its current ~375 lines, promoting it to its own file. -
🌱 Consider obtaining nicknames in a machine-readable form. Both blocking findings dissolve if the tool stops parsing the human-readable table. Worth evaluating whether the certificate set can be enumerated without relying on
certutil -L's column layout, which is a presentation format with no stability guarantee across NSS releases.
CI Status
- integration tests (Fastly EC lifecycle): PASS
- integration tests: PASS
- browser integration tests: PASS
- CodeQL: PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- Analyze (javascript-typescript): PASS
- Analyze (actions): PASS
- Analyze (rust): PASS
- CLAUDE.md symlink guard: PASS
- cargo fmt: PASS
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test: PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- format-docs: PASS
- format-typescript: PASS
- prepare integration artifacts: PASS
- vitest: PASS
Reviewer-side verification on the PR head: cargo clippy --package trusted-server-cli --target aarch64-apple-darwin --all-targets -- -D warnings clean, and the macOS CLI test suite passes (29 unit + 7 perf + 1 keychain).
| { | ||
| return Err(Report::new(TrustError::Command).attach("invalid NSS trust attributes")); | ||
| } | ||
| names.push(name.trim_end().to_owned()); |
There was a problem hiding this comment.
🔧 wrench — NSS accepts and preserves trailing whitespace in a nickname, but certutil -L's listing renders it indistinguishably from the column padding. trim_end() here therefore produces a name that certutil -L -n cannot resolve, and checked() turns that non-zero exit into a hard error.
Reproduced against a real certutil (NSS 3.x):
$ certutil -A -n "Some Corp CA " -t "C,," -i c.pem -d sql:$D
IMPORT OK
$ certutil -L -d sql:$D
Some Corp CA C,,
$ certutil -L -n "Some Corp CA" -a -d sql:$D # the name this parser yields
exit 255 certutil: Could not find cert: Some Corp CA
$ certutil -L -n "Some Corp CA " -a -d sql:$D # the real nickname
exit 0
Feeding that captured listing through this function's exact logic returns Ok(["Some Corp CA"]) — the trailing space is gone.
Why the blast radius is larger than install. nicknames() also backs contains() (line 313), which uninstall calls at lines 409 and 412, and which regenerate reaches through uninstall. So a single unrelated third-party certificate with a trailing-space nickname in the user's Chrome/Chromium NSS database makes the dev CA impossible to install, impossible to remove, and impossible to rotate — all reported as a generic browser trust command failed with nothing pointing at the foreign entry as the cause.
Second-order effect on the conflict check. Two distinct certificates under "Corp CA" and "Corp CA " render identically in the listing and both parse to "Corp CA"; the exact-match export then returns only one of them. check_subject_conflicts consequently scans the same certificate twice and never inspects the other, so a same-subject conflicting CA hiding behind a trailing-space nickname defeats the check that lines 281-283 exist to enforce. That is a soundness gap in a security control, not only an availability bug.
Proposed fix (apply manually — a parsing-strategy change plus a regression test; cannot be expressed as a single-range suggestion):
// Split on the fixed column boundary that certutil emits rather than
// rsplit_once + trim_end, so a nickname's own trailing spaces survive.
// Additionally, treat a nickname that fails to re-export as a
// skip-with-warning rather than a hard failure of the whole operation,
// so one foreign entry cannot block install/uninstall/regenerate.A regression test can be written without NSS installed by feeding the captured listing bytes above straight into nicknames().
| { | ||
| continue; | ||
| } | ||
| let (name, trust) = line.rsplit_once(char::is_whitespace).ok_or_else(|| { |
There was a problem hiding this comment.
🔧 wrench — NSS also accepts newlines inside a nickname. The listing then wraps across lines and rsplit_once treats each fragment as its own row, so the first fragment fails the trust-attribute validation below.
Reproduced against a real certutil:
$ certutil -A -n "$(printf 'Line One\nLine Two')" -t "C,," -i c.pem -d sql:$D
IMPORT OK
$ certutil -L -d sql:$D
Line One
Line Two C,,
Through this function's exact logic: Err("invalid NSS trust attributes: \"One\" (line \"Line One\")") — "Line One" splits into ("Line", "One") and "One" fails the three-comma check.
This shares the root cause and the blast radius of the trailing-whitespace finding on line 264: it propagates as TrustError::Command out of install, uninstall, and regenerate alike. Severity is lower because it fails closed — an error, never a wrong trust decision — and a newline nickname is rarer than a trailing space. The same column-boundary parse fixes both.
Proposed fix (apply manually — same change as the line 264 finding):
// Parse against certutil's fixed column layout so a wrapped nickname is
// reassembled rather than being read as two malformed rows.For context, the header-skip logic immediately above this is correct — I confirmed SSL,S/MIME,JAR/XPI is a genuine second header line that equals the literal exactly after str::trim. Only the row parse needs changing.
| return Ok(vec![ | ||
| Self::Chrome, | ||
| Self::Firefox, | ||
| #[cfg(target_os = "macos")] |
There was a problem hiding this comment.
♻️ refactor — Browser::Safari is declared unconditionally at line 99, but both of its construction sites are #[cfg(target_os = "macos")] (this one and the "safari" match arm at 123-124). On Linux the type therefore admits a state the parser can never produce, while browser.rs:80 still dispatches Browser::Safari => launch_safari(cfg) into a function compiled on Linux with an entirely macOS body (networksetup, route, sudo, open -a Safari).
I verified this degrades safely today — detect_network_service() returns None on non-macOS, so launch_safari warns and returns — so this is a latent trap rather than a live bug. It bites when a second constructor appears: a clap ValueEnum, a config-file default, or a test fixture would all compile happily and then take an unreachable-by-design path at runtime instead of failing the build.
Cfg'ing the variant itself makes the state unrepresentable on Linux and gets the dispatch compiler-checked:
pub enum Browser {
Chrome,
Firefox,
#[cfg(target_os = "macos")]
Safari,
}The three existing cfgs in parse_list then stay exactly as they are and become required rather than conventional; the compiler will point at browser.rs:80, which needs a matching #[cfg(target_os = "macos")]; and launch_safari plus its macOS support cast (detect_network_service, get_auto_proxy_state, restore_auto_proxy, manual_restore_command) can then be cfg'd out of the Linux build rather than compiled and never called. The test at 640-659 already spells the cfg'd-element form, so it needs no change.
Apply manually — the change spans config.rs and browser.rs together, so it cannot be a single-file suggestion.
For the record, the cfg-inside-vec![] and cfg-on-a-match-arm technique used here is perfectly legitimate stable Rust and reads fine; the objection is only to the variant's unconditional declaration, not to how parse_list is written.
| find_native_browser(CHROME_LAUNCHERS, |name| { | ||
| which::which(name) | ||
| .ok() | ||
| .and_then(|path| path.canonicalize().ok()) |
There was a problem hiding this comment.
♻️ refactor — this closure is byte-identical to the one in firefox_command() at lines 379-383:
which::which(name).ok().and_then(|path| path.canonicalize().ok())The canonicalize() is the security-relevant half: without it, a symlink from /usr/bin into /snap or /flatpak would slip past the component check in find_native_browser at lines 261-266, which is the mechanism the PR relies on to refuse packaged browsers. Two copies means a future hardening of the lookup has to remember both sites, and a fix applied to one is silently absent from the other.
Proposed fix (apply manually — the change touches two separate hunks in this file, so it cannot be a single-range suggestion):
#[cfg(target_os = "linux")]
fn lookup_on_path(name: &str) -> Option<std::path::PathBuf> {
which::which(name)
.ok()
.and_then(|path| path.canonicalize().ok())
}Both call sites then collapse to find_native_browser(CHROME_LAUNCHERS, lookup_on_path) and find_native_browser(&["firefox"], lookup_on_path).
Injecting lookup as a parameter on find_native_browser was the right call — it is what makes the test at 748-766 possible without touching the real PATH, and that test covers discovery order, total absence, and the snap rejection. This finding is only about the two duplicate closure bodies.
Minor, while you are here: firefox_command() carries a doc comment but chrome_command() has none.
| } | ||
| }; | ||
| let entry = &entries[index]; | ||
| contains(entry)?; |
There was a problem hiding this comment.
🤔 thinking — the bool is discarded here, but the identical call at line 372 uses its value, which makes this line read as a dropped result rather than a deliberate choice.
Having traced it, the behaviour is correct: contains() returns Ok(true) only when the entry is present and DER-identical (line 320 has already raised TrustError::Identity on a mismatch), Ok(false) when absent, and in both cases the right next action is to import. The ? is doing the load-bearing work of surfacing the identity conflict; the presence answer genuinely is not needed. I also confirmed against a real certutil that re-importing an identical certificate under the same nickname is idempotent in NSS — the listing still shows one row and -r returns a single DER — so the re-import is neither incorrect nor meaningfully wasteful.
The suggestion is just to make that intent legible to the next reader, either by binding the value or by stating it above the call:
// Called for its identity-conflict check only; an already-present
// identical certificate is re-imported idempotently either way.
contains(entry)?;This file otherwise comments exactly this class of subtlety well — see the notes at lines 307, 334, and 338 — so this line stands out as the outlier.
| /// An unknown browser name was passed to `--launch`. | ||
| #[display("unknown browser `{value}` (expected chrome|firefox|safari|all)")] | ||
| /// An unknown or unsupported browser was passed to `--launch`. | ||
| #[display("unsupported browser `{value}` (use chrome|firefox|all; safari is macOS-only)")] |
There was a problem hiding this comment.
⛏ nitpick — the parenthetical is unconditional, so on macOS, where safari is a perfectly valid value, a user who typos netscape is told that safari is macOS-only on the one platform where it works.
Either cfg the hint so each platform lists what it actually accepts, or drop the platform note from the shared message and let the Linux-specific rejection carry it.
Summary
ts dev proxyavailable on Linux. It was previously excluded at compile time even though the proxy engine can run natively on Linux.Scope
This is a CLI-only platform extension, not a proxy-engine rewrite. The changes span command registration, browser launch, certificate trust, tests, CI, and documentation because removing the compile gates alone would leave Linux browsers without working trust management.
Most of the new code and tests cover safe certificate installation and removal. A reproduced NSS behavior required an extra check: importing a different certificate with the same subject can make the original certificate's nickname export ambiguous. Installation now rejects that conflict before import, and shared-database locks serialize
tstrust changes.Supported automation is limited to native Chrome/Chromium and Firefox. Safari stays macOS-only. Windows, Snap/Flatpak automation, root operations, and desktop-wide proxy or certificate-store changes are excluded. Known packaging paths are skipped, but arbitrary shell wrappers are not classified.
Changes
CLI paths below are relative to
crates/trusted-server-cli/; other paths are repository-relative..github/workflows/test.ymlCargo.tomlsrc/lib.rssrc/run.rssrc/commands/dev/mod.rssrc/commands/dev/proxy/mod.rssrc/commands/dev/proxy/browser.rssrc/commands/dev/proxy/config.rssrc/commands/dev/proxy/trust.rstests/proxy_cli.rstests/proxy_e2e.rstests/proxy_perf.rstests/proxy_trust_linux.rstests/proxy_trust_macos.rsscripts/test-linux-dev-proxy-browser.pydocs/guide/ts-dev-proxy.mddocs/superpowers/plans/linux-dev-proxy.mdCloses
Closes #1170
Test plan
./scripts/test-cli.sh: 182 unit tests and 42 integration/configuration tests passed.cargo test_cli_linux --test proxy_trust_linux -- --include-ignored: all 11 trust tests passed, including real NSS checks.cargo clippy --package trusted-server-cli --target x86_64-unknown-linux-gnu --all-targets -- -D warningscargo fmt --all -- --checkcd docs && npm run format, with the existing JS dependency directory added to PATH because docs dependencies were not installed separately.All real certificate/browser tests used disposable HOME, XDG, CA, profile, and NSS directories. They did not modify the developer's real trust stores or disable TLS verification or the browser sandbox. Adapter, JavaScript, and WASM suites were not rerun because those implementations are unchanged. Manual performance workloads remain opt-in.
Checklist
unwrap()in production code.