Repository navigation
Add ts dev lint domains and ts dev install-hooks to block non-allowlisted URL hosts - #733
Conversation
fcff9ed to
30f2cc1
Compare
Port the pure-Rust URL-host linter (`ts dev lint domains`) and the
pre-commit hook installer (`ts dev install-hooks`) onto main's
restructured CLI. Both are cross-host (all git access via gitoxide, no
`git` subprocess) and register as siblings of the macOS-only
`ts dev proxy`.
Adapt to main's layout:
- Move the feature under `src/commands/dev/{lint,install_hooks}` and add
the `Lint` / `InstallHooks` variants to `DevCommand`, replacing the
prior empty-enum handling.
- Reintroduce a small `CliError` (`Io`, `Json`, `EnvironmentError`,
`ViolationsFound`) plus `output::write_{stdout,stderr}_line` /
`write_json`; `output` becomes cross-host while `info` / `warn` stay
macOS-only.
- Map the linter's results to the exit contract (0 clean, 1 violations,
2 environment error) in `commands::dev::run`.
- Scope `error-stack` / `derive_more` cross-host and add `gix` /
`gix-config`; add `assert_cmd` / `predicates` / `temp-env` dev-deps.
The former `serve` subcommand is not restored — main replaced it with
`ts dev proxy` independently of this work.
30f2cc1 to
b0b6bd5
Compare
Pin a fixed `user.name` / `user.email` in the repo-local config of the git fixtures. `create_and_checkout_branch` writes a ref through `repo.reference(...)`, whose reflog needs a committer identity; CI machines have no ambient identity, so both `changed_vs` tests failed with `CreateOrUpdateRefLog(MissingCommitter)`. Developer machines passed only because they inherited a global git identity. Repair the "Resolved by the Phase 2 spike" list in the design spec. Its sub-bullets were glued onto preceding lines and its inline code spans were split across line breaks, so prettier re-indented the block deeper on every run and never converged, failing `format-docs`.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated Review:
Summary
Reviewed the domain-lint and hook-installation changes. I found one confirmed self-exclusion defect, submitted inline.
Findings by priority
- P1: 1 inline finding.
CI
All currently reported PR checks are passing. git diff --check is clean.
Existing Reviews
No existing submitted reviews were returned when checked; this review does not duplicate prior feedback.
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Adds ts dev lint domains (four scan modes, gitoxide-only) and ts dev install-hooks. The pure-function layer — host extraction, allowlist matching, suppression markers — is well factored and unusually well tested, including userinfo-bypass and rename regressions. Three defects block: the self-exclusion constant points at a path that no longer exists, so the linter flags its own source and this PR fails its own --changed-vs gate; install-hooks silently disables any hooks already in .git/hooks; and both commands error out when run from a subdirectory.
Verified against the PR head in an isolated worktree with a binary built from it (aarch64-apple-darwin): cargo fmt --all -- --check clean, cargo clippy -p trusted-server-cli --all-targets -- -D warnings clean, cargo test -p trusted-server-cli 236 + 28 + 4 + 29 + 1 pass. Evidence for each defect is in the inline comments.
1 of the inline comments below carries a one-click GitHub
suggestion— use Commit suggestion to apply it as a commit on the PR branch. The remaining comments describe the fix in prose because the change spans several files, touchesCargo.lock, or needs a new helper, and can't be auto-applied.
Blocking
🔧 wrench
SELF_PATHpoints at a path that does not exist — self-exclusion is dead — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:883install-hookssilently disables every hook already in.git/hooks— see inline atcrates/trusted-server-cli/src/commands/dev/install_hooks.rs:229- Both commands fail from any subdirectory (
gix::opendoes not discover the repo root) — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:995
❓ question
- Stage-2 dogfooding policy for the linter's own test file and the two design docs — see the cross-cutting section below.
Non-blocking
♻️ refactor / 🤔 thinking / 🌱 seedling / ⛏ nitpick
- Violation report prints the stale
src/dev/lint/domains.rshint path (suggestion) — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:2054 temp-envdev-dependency added but never used — see inline atcrates/trusted-server-cli/Cargo.toml:81- Four new deps bypass
[workspace.dependencies]— see inline atcrates/trusted-server-cli/Cargo.toml:41 - Absolute-URL regex accepts single-label hosts; protocol-relative one does not — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:341 test_support.rsandtests/common/mod.rsare byte-identical, kept in sync by comment — see inline atcrates/trusted-server-cli/tests/common/mod.rs:8- Diff modes don't skip binary blobs, full-repo mode does — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:950 --stagedwrites a tree object into the object database on every run — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:1025
Cross-cutting / body-level findings
-
❓ Stage-2 dogfooding policy. After the
SELF_PATHfix,ts dev lint domains --changed-vs origin/mainon this head still reports 91 violations across 3 of this PR's own new files:crates/trusted-server-cli/tests/lint_domains_cli.rs(30),docs/superpowers/specs/2026-05-18-check-domains-design.md(34),docs/superpowers/plans/2026-05-18-ts-dev-lint-domains.md(27) — all from the deliberatetest.com/evil.com/cdn.example.evilfixtures the spec's own test cases require. What is the intended resolution: extend the exclusion list to the linter's test file anddocs/superpowers/**, sprinkleallow-domain:markers, or accept that the Stage 2 changed-lines gate cannot be turned on until the Stage 1 cleanup covers these? Worth answering in this PR, because it decides whether the exclusion policy needs to grow before the gate exists. -
📌 Plan Phase 8 (documentation) did not ship. The plan's Task 8.1 (
CONTRIBUTING.md— "Pre-commit URL-host linter" install steps) and Task 8.2 (README.mdmention) have no counterpart in the diff, so nothing in the repo tells a contributor thatts dev install-hooksexists or that a pre-commit linter is expected. Either land those two doc edits here or open a follow-up so the install step isn't discoverable only from the design doc. -
📝 PR description's exit-code contract overstates the implementation. The description lists "130 cancelled", but there is no
CliError::Cancelledvariant and no 130 mapping —finish()incommands/dev/mod.rsmapsViolationsFound→ 1,EnvironmentError→ 2, everything else → 1. The spec leaves the 130 model explicitly undecided ("Pick one model"), so the code is self-consistent; only the description is ahead of it. -
👍 Genuinely strong adversarial testing. The userinfo-bypass regressions (
https://github.com@test.com/path→test.com,https://a@b@c.evil→c.evil) close a real allowlist bypass; the pure-rename and rename+edit cases pin a bug that a naive path-map walk would reintroduce; andworks_without_git_on_path(env_clear+ emptyPATH) actually proves the no-subprocess claim rather than asserting it. The suppression-marker bypass tests (allow-domaininside a URL path, host literally namedallow-domain) are the kind of tests reviewers usually have to ask for.
CI Status
- integration tests (Fastly EC lifecycle): PASS
- integration tests: PASS
- browser integration tests: PASS
- CodeQL: PASS
- Analyze (actions): PASS
- Analyze (rust): PASS
- Analyze (javascript-typescript): PASS
- prepare integration artifacts: PASS
- vitest: PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo test: PASS (required)
- cargo test (ts CLI, native): PASS
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- format-typescript: PASS (required)
- format-docs: PASS (required)
- cargo fmt: PASS (required)
Blocking fixes from review of #733: - SELF_PATH pointed at the pre-move `src/dev/lint/domains.rs`, so the component-aware `Path::ends_with` never matched and the linter flagged its own fixtures (self-scan 38 violations, `--changed-vs origin/main` 129). Correct the path and sweep every stale spelling across code, tests, and the two design docs. Extend the self-exclusion to the E2E test file and the `docs/superpowers/` design docs, whose deliberate `test.com` / `evil.com` fixtures are the remaining 91 — `--changed-vs origin/main` now reports 0 on this PR's own files. - `install-hooks` set `core.hooksPath` without noticing executable hooks already in `.git/hooks`, silently disabling them. Add a preflight that refuses without `--force` (and prints a note under it), plus tests. - `gix::open` does not discover the repo root, so every mode failed from a subdirectory with a misleading "not a git repository". Switch the three collectors and `install_hooks` to `gix::discover`; add a nested-subdirectory regression test. Non-blocking cleanups: - Skip non-UTF-8 blobs in diff modes, matching full-repo's binary skip. - Drop the unused `temp-env` dev-dependency. - Move the `gix` / `gix-config` / `assert_cmd` / `predicates` version pins into `[workspace.dependencies]` so the pin is enforceable. - De-duplicate the fixture helpers: `tests/common/mod.rs` now includes the single `test_support.rs` via `#[path]` instead of a byte-identical copy kept in sync by comment. - Document the `--staged` tree-object write and the intentional single-label-host asymmetry in the spec. - Land the Phase 8 docs: `ts dev install-hooks` steps in CONTRIBUTING.md and a mention in README.md.
Blocking fixes from review of #733: - SELF_PATH pointed at the pre-move `src/dev/lint/domains.rs`, so the component-aware `Path::ends_with` never matched and the linter flagged its own fixtures (self-scan 38 violations, `--changed-vs origin/main` 129). Correct the path and sweep every stale spelling across code, tests, and the two design docs. Extend the self-exclusion to the E2E test file and the `docs/superpowers/` design docs, whose deliberate `test.com` / `evil.com` fixtures are the remaining 91 — `--changed-vs origin/main` now reports 0 on this PR's own files. - `install-hooks` set `core.hooksPath` without noticing executable hooks already in `.git/hooks`, silently disabling them. Add a preflight that refuses without `--force` (and prints a note under it), plus tests. - `gix::open` does not discover the repo root, so every mode failed from a subdirectory with a misleading "not a git repository". Switch the three collectors and `install_hooks` to `gix::discover`; add a nested-subdirectory regression test. Non-blocking cleanups: - Skip non-UTF-8 blobs in diff modes, matching full-repo's binary skip. - Drop the unused `temp-env` dev-dependency. - Move the `gix` / `gix-config` / `assert_cmd` / `predicates` version pins into `[workspace.dependencies]` so the pin is enforceable. - De-duplicate the fixture helpers: `tests/common/mod.rs` now includes the single `test_support.rs` via `#[path]` instead of a byte-identical copy kept in sync by comment. - Document the `--staged` tree-object write and the intentional single-label-host asymmetry in the spec. - Land the Phase 8 docs: `ts dev install-hooks` steps in CONTRIBUTING.md and a mention in README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review addressed — a05cf2aThanks for the thorough pass. All three blocking findings and the non-blocking cleanups are resolved; each inline thread has a reply. Summary of the cross-cutting / body-level items: Stage-2 dogfooding policy (❓). Resolved by extending the self-exclusion rather than sprinkling markers. After the Plan Phase 8 docs (📌). Landed here: CONTRIBUTING.md gains a "Pre-commit URL-host linter" section with the PR description exit-code (📝). Corrected — the description no longer claims "130 cancelled"; it now matches Verified on |
Resolve the Cargo.lock conflict by re-resolving against the merged manifests. Pin kstring to 2.0.2: 2.0.4 raises its MSRV to rustc 1.96, which the workspace's pinned 1.95.0 toolchain cannot build. kstring arrives transitively via gix-config.
The plan linked the spec as a sibling, but it lives in `specs/`, not `plans/`. VitePress builds with the dead-link gate main added in #1066, so this failed the `format-docs` job. Use the `../specs/` form the rest of `docs/superpowers/` already uses.
7c0cc50 to
1c2914a
Compare
|
Round 4 feedback addressed in 944df4d — both findings from @prk-Jr. Each was reproduced against a built CLI before any change and re-verified against the same reproduction afterwards; replies are on the individual threads. Host extraction ( The review warned that deleting delimiters would trade the bypass for Markdown and source-punctuation false positives. I measured rather than guessed: removing every soft delimiter added 115 new violations across a full-repo scan (almost all
Only the leading label and trailing run are affected, so embedded occurrences still reach the allowlist — tests assert IDNA TLDs — applied the suggestion. Canonicalisation maps a Unicode TLD to punycode, whose Effect on the repo: net zero. The violation count is unchanged at 486, with two lines now reporting the more accurate Also in this push: merged Verification — every CI gate run locally, all green: |
prk-Jr
left a comment
There was a problem hiding this comment.
One blocking correctness issue: consecutive single-quoted URLs can cause the linter to silently skip a disallowed host, including in staged mode.
Validation: reproduced against the reviewed commit; the suggested one-line replacement makes the reproduction report the violation and exit 1. All 104 linter unit tests and 38 CLI integration tests pass with the replacement. Scratch changes were restored. All 20 GitHub checks passed when inspected during review.
Address review round 5 on PR #733, applying the suggested one-character change to the terminator class. `'` was left out of the class in the previous round because it is legal inside a WHATWG hostname, with a trailing one trimmed after capture instead. That was wrong in a way the trim could not repair: a match that does not stop at the quote runs past the end of its own string and consumes the next URL's scheme, and `captures_iter` cannot revisit input an earlier match already consumed. const urls = ['https://example.com','https://unapproved.internal']; captured `example.com','https:` as a single authority. Trimming at the quote recovered `example.com`, but the second host was never examined, so the linter exited 0 on a disallowed host in explicit-path and staged modes. Reproduced first: a list of two disallowed hosts reported only the first, which is a silent false negative and the one outcome this linter must not have. Terminating at the quote costs nothing. Measured on a full-repo scan with only this change applied: 486 violations before and after, no additions and no removals. The trailing-quote shapes that motivated the previous round's approach (`expect(new URL('https://host').href)`) are now handled by the terminator rather than the trim, which is the stronger guarantee since it also bounds the match. With the quote terminating the capture, the trim's leading `find('\'')` cut became unreachable. Verified rather than assumed, by asserting on it across a full-repo scan and all unit tests before deleting it, and renamed the multi-byte test that referred to it. A URL inside a quoted string cannot contain the quote anyway. Regression coverage for consecutive single-quoted URLs in unit tests, including three-host lists, double quotes, and mixed quoting, plus a staged-mode CLI test since that is the path the pre-commit hook runs. Spec updated to record why the quote is a terminator despite being host-legal.
|
Round 5 feedback addressed in fcba41d — the single blocking finding from @prk-Jr, applying the suggested one-character change. The bug was mine from round 4. I had removed Reproduced before changing anything, and it was slightly worse than reported — a list of two disallowed hosts reported only one: Both are reported now. A silent false negative is the one outcome this linter must not have. Cost of the fix: zero. Since Dead code removed. With the quote terminating the capture, the trim's leading Verification — all gates green locally: |
Three conflicts, all in the `ts` CLI crate: main added a `ts dev sandbox-probe` subcommand that touches the same files as this branch's `ts dev lint` and `ts dev install-hooks`. Every conflict was additive, so each resolved to the union. - `Cargo.toml`: kept this branch's `gix` block in the cross-host `cfg(not(target_arch = "wasm32"))` section, since the linter runs on every host, and took main's new `cfg(not(target_family = "wasm"))` probe section and its widened macOS+Linux proxy section verbatim. Dev-dependencies merged to the union of `assert_cmd`, `predicates` and main's `flate2`. - `src/commands/dev/mod.rs`: union of the `DevCommand` variants and their match arms, so `proxy`, `sandbox-probe`, `lint` and `install-hooks` all remain. Took main's widened `cfg(any(target_os = "macos", target_os = "linux"))` gate for the proxy module and updated the comment that still called it macOS-only. - `src/lib.rs`: both sides rewrote the same module comments. Merged them into one description covering the proxy's widened targets and the cross-host `output` helpers that both the linter and the probe need. Git also auto-merged both sides' identical addition of `error-stack` to the cross-host dependency block into two lines, which is a duplicate key `cargo` rejects rather than a conflict git reports. Caught by `cargo metadata --locked` and de-duplicated; a scan for other doubled keys found none. Verified the merge changed no linter behaviour: running the pre-merge binary and the merged binary over the merged tree gives identical results (518 violations, no additions, no removals). The rise from 486 is main's new source, not this branch. `ts dev --help` lists all four subcommands, and the reproductions from every earlier review round still report their hosts.
The `main` merge broke the Linux CLI build: `cargo clippy -D warnings` failed with 17 errors, all variants of `cannot find function 'info' / 'warn' in module 'output'`. This branch added `#[cfg(target_os = "macos")]` to `output::info` and `output::warn` in an earlier round, when `ts dev proxy` was their only caller and was macOS-only, so an ungated pair warned as dead code elsewhere. Main has since widened the proxy to macOS and Linux and added `ts dev sandbox-probe`, which builds on every host target and calls both helpers. The gates became wrong, and the premise that justified them no longer holds. Git reported no conflict in `output.rs` because only this branch had touched it since the merge-base, so the stale gated version won silently. The merge compiled on macOS and could not compile on Linux, which is why the local gate run came back clean. The `lib.rs` comment is corrected to match: both helpers are cross-host now. Verified on the target that failed rather than by inference, using the pinned 1.95.0 toolchain in a linux/amd64 container: `cargo clippy -p trusted-server-cli --all-targets -- -D warnings` exits 0, and the library test suite goes from a compile failure to 721 passing. Two remaining failures in that container are artifacts of it rather than defects: `permission_denied_is_hard_error` needs a non-root user to make `chmod 000` deny a read, and `missing_node_modules_fails_with_npm_ci_instruction` needs npm on PATH. Both pass on main's own ubuntu CI job, and `prebid_bundle.rs` is not part of this branch's diff. macOS remains clean: `cargo clippy-cli` passes and 887 CLI tests pass.
Closes #1160
Summary
ts dev lint domains— a pure-Rust source/config/docs linter that flags non-allowlisted URL hosts in four modes (--staged,--changed-vs <ref>, full-repo, explicit paths). All git operations go through gitoxide (no shelling out togit). Captured URL authorities are canonicalised through theurlcrate (percent-decoding, IDNA, case, port) before the allowlist check, so lookalike authorities cannot pass on a prefix match.ts dev install-hooks— writes a managedpre-commithook that runsts dev lint domains --stagedinto git's own.git/hooksdirectory (the main repository's from a linked worktree). It never edits git configuration or the working tree; it refuses whencore.hooksPathis set in any scope, refuses to clobber an unmanaged hook, and with--forcebacks the old hook up untouched.ts devleaf into a subcommand group;lintandinstall-hookssit next toproxy.Design
Allowlists:
EXACT_HOSTS,SUBDOMAIN_HOSTS,REFERENCE_HOSTS, plus RFC 2606 reserved TLDs. Suppression marker:// allow-domain: host(and#,<!--,*comment forms). Scanned extensions cover Rust, TS/JS, configs, Markdown, CSS, HTML, env files, and Dockerfiles, matched case-insensitively. Only the linter's own source, E2E suite, spec and plan are exempt. Exit codes: 0 clean, 1 violations, 2 environment error.Evasion and bypass hardening
Review found four ways the tool could be defeated while reporting success. All are fixed, each with a regression test:
<git dir>/index, but git supplies a temporary index viaGIT_INDEX_FILEforgit commit -aandgit commit -- <path>, so both forms committed violations thatgit add+git commitrejectedGIT_INDEX_FILE, falling back torepo.index_path()//github.com%2eevil%2ecom,https://github.com.com(an invisible soft hyphen) andhttps://%65vil.com— the first two reported an allowlisted prefix, the third nothing at allhttps:\/\/user:pw@host)core.hooksPathwas treated as unset, so installation reported success while git never read.git/hooksThe wider authority capture is the change most likely to cause false positives, so it was checked against the whole repository: a full-repo audit reports exactly the same 484 violations as the pre-change build, byte-identical — no false positives, no lost detections.
Test plan
cargo fmt --all -- --checkcargo clippy --package trusted-server-cli --target <host> --all-targets --all-features -- -D warningscargo clippy-fastly,cargo clippy-axumcargo test --package trusted-server-cli --target <host>— 351 tests across 8 suites, including real-gitend-to-end tests for the installer (branch-planted.githooks/post-checkoutstays inert, linked worktree shares the hook, global and emptycore.hooksPathrefused, temporary-index commit forms blocked)ts dev lint domains --changed-vs origin/mainon this branch: cleants dev lint domainsfull audit: diagnostic only, reports the repo's pre-existing violations (no CI gate in v1)cd docs && npm run format