fix(cli-memd): withhold URLs whose credentials cannot be attributed - #165
Conversation
Consumed revision: R1 ## Problem A first-time user's most common failure is a missing prerequisite they cannot name. Verified at the base revision: the only signals were a per-command `not logged in` hint and raw transport errors, and the hint sent somebody with no server at all to `mem auth login` against a server that does not exist yet. ## Goal and expected behavior REQ-001: a read-only `mem doctor` reporting four checks in a fixed order -- server reachability, credential presence, the workspace the server resolved, CLI/server version skew -- each carrying its SPEC 7.1 exit code, with `--format json` per SPEC.md:579. REQ-002: every command that fails closed on a missing credential names the documented deployment path when the host has no configuration at all. REQ-003: diagnosis only -- zero writes, zero remediation, zero dependency installation, no secret value or DSN printed. Non-goals: no environment-certification matrix, no host container-runtime detection, no TTY wizard, no SPEC edit. ## Implementation - `server/cmd/mem/cmds_doctor.go`: the command, the four probes, and the shared probe-failure classification that maps `apiclient.APIError` kinds onto SPEC 7.1 codes. Reachability is probed without a credential so a bad token is not misread as an outage. A check an earlier failure made impossible is reported `skipped`, naming the blocker, instead of guessed as a pass. - `errNotLoggedIn()` replaces 23 duplicated `newCliError(3, "not logged in", ...)` constructions. It branches on `configFileExists()`, the only signal that separates "never configured" from "configured, but not logged in", because `loadConfig` deliberately succeeds without a file. Hosts that already have a configuration keep the previous hint text unchanged. - `cliVersion` becomes a variable so the skew check has something to compare. Nothing injects it today -- `release.yml` passes only `-trimpath -ldflags=-s -w` -- so the check reports "skew not computable" rather than inventing a comparison. - `redactURL` strips userinfo from any reported server URL, because a URL is a place operators put credentials. `mem version` now uses it too. - `docs/schemas/mem-doctor.v1.schema.json` pins the `mem.doctor` v1 document: closed status and name enums, fixed check order, `additionalProperties: false`. ## Acceptance-criteria mapping - AC-001: `cmds_doctor_test.go` fixtures for unreachable server, missing credential, rejected credential, unresolved workspace, quota refusal and version skew, each asserting its named check, its exit code and a hint that names the documented path, over an `httptest` stub following the `cmds_ingest_test.go` pattern. Plus real compiled-binary runs: connection-refused (exit 5), a stalled listener (exit 5, timeout hint), and a healthy stack stand-in (exit 0, empty stderr). - AC-002: the stub transport records every request and fails the test on any non-GET path, any unexpected path, or any request body; a separate assertion scans stdout and stderr for the token value. A server-side audit of the compiled-binary runs logged 7 requests, all GET, all body=0. A URL carrying credentials is reported redacted, with zero occurrences of either secret in either stream. - AC-003: `--format json` is validated against the checked-in schema by an in-test structural validator (closed enums, required keys, pinned `prefixItems` names) and against a golden file, and independently by `python3 -m jsonschema` (Draft 2020-12, 4.19.2) on the compiled binary's real stdout. ## Deliberate assertion change `TestAuthStatusWithoutTokenReturnsAuthExitCode` asserted the old hint by exact equality, which REQ-002 makes conditional on config presence. It is widened, not deleted: the exit code and the login step are still pinned, and the deployment path is now required. ## Configuration and environment - Base: `731a468` (`origin/main`) - OS: Linux 6.18.33.2 WSL2, x86_64; Go 1.25 - No new third-party dependency; `go.mod`/`go.sum` untouched Refs #112
A configured URL that carries credentials in a shape url.Parse does not report as userinfo reaches output today. "admin:pw@host" parses as Scheme="admin" with the credential in Opaque and User unset, so any implementation that gates on User != nil echoes the credential verbatim. Measured on a binary built from 87d235b: `mem auth status` against such a base URL prints the password in clear text, and the same holds for every http.Client.Do failure in the client, which no redaction covered at all. Route both egresses through one shared gate that redacts a value it can prove is a transport URL and withholds the value whole otherwise. Error text is not scrubbed, because that cannot be made tight: url.Error renders with %q, so a quote inside a password arrives escaped and a scanner that pairs quotes mis-pairs and replaces nothing. memd's fatal line is included because it carries third-party errors that embed a whole DSN -- asynq puts the URI it failed to parse into the message -- and slog renders an error value as its text. The gate proves the absence of userinfo, not of every credential: a secret supplied as a query parameter parses as a clean URL and is still echoed. That shape is out of scope here and is pinned by a named characterization test rather than left as folklore.
Carries the `mem doctor` surface from pull/164 (which itself carries the
commit from pull/131) so the CLI feature and the URL gate land together.
`server/internal/apiclient/{apiclient.go,apiclient_test.go}` keep this
branch's implementation: it gates request construction and all four
`http.Client.Do` sites, and its test file already contains the same
`TestRequestBuildErrorRedactsCredentialedURL` plus the schemeless
transport cases. `CHANGELOG.md` keeps both entries.
… credential URL gate)
`mem doctor` had its own copy of the redaction rule. Two of its shapes were wrong in the same way the API client was: `redactURL` returned the value unchanged whenever `url.Parse` succeeded without reporting userinfo, and `sanitizeProbeError` scanned error text for a quoted `://` substring, which cannot be made tight because `url.Error` renders with %q. Both now delegate to internal/redact, so the CLI, the API client and memd share one policy and there is no second implementation left to drift. TestRedactURLStripsUserinfo and the server_reachability assertion in TestDoctorMalformedServerURLDoesNotLeakCredentials required the value to come back as a cleaned URL. The gate withholds it instead, which is the stronger behavior, so both now accept either outcome while still asserting that neither the credential nor the raw URL appears. TestDoctorSchemelessServerURLDoesNotLeakCredentials is new and is what makes this change load-bearing: with the previous scrubber restored, it is the only test that fails.
The CHANGELOG, DEPLOYMENT.md and cmds_doctor.go all claimed the doctor report contains no secret value. Measured on this tree, a credential in the server URL's query string is still printed verbatim, because it parses as a clean URL with no userinfo and so is outside what the gate proves. Narrow the claim to what is actually guaranteed and name the residual.
Scope widened: this branch now carries
|
| shape | #164 head fffcd4c |
this head |
|---|---|---|
well-formed http://admin:PW@127.0.0.1:1 |
0 | 0 |
http://admin:PW@127.0.0.1:%zz |
0 | 0 |
| space in host | 0 | 0 |
| space in password | 0 | 0 |
http://admin:PW@% |
0 | 0 |
| non-numeric port | 0 | 0 |
admin:PW@mem.internal:8787 (no scheme) |
6 | 0 |
That table nearly misled me and I want the failure mode on record. Run against
b229537 — this PR's previous head — it returned all zeros, and the only
reason it looked wrong is that I checked: that build has no doctor subcommand
and answers Error: unknown command "doctor" for "mem". The guard only counted
unknown flag, which is a different cobra error. It counts unknown command
now, and every row above was taken with both counters at 0.
Mutation control. Restoring #164's original redactURL /
sanitizeProbeError into this tree fails exactly one test —
TestDoctorSchemelessServerURLDoesNotLeakCredentials — and its binary leaks 2 on
shape 7. Before that test was added the same mutation failed zero tests, so
the merge by itself was unprotected. Full detail, including the two assertions I
widened (one of them authored by PeterGuy326), is on #164.
One extra commit, and why it is here
c9e0e63 is documentation-only, and it fixes a claim I wrote. CHANGELOG.md,
docs/DEPLOYMENT.md:194 and a comment in cmds_doctor.go all asserted the
doctor report contains no secret value. Measured on this head:
mem doctor --server "http://mem.internal:8787?password=s3ntinel…"
→ 3 sentinel occurrences per format, text and json alike
A credential in the query string parses as a clean URL with User == nil, so
the gate has nothing to act on. This PR has always declared query-string handling
a non-goal — that stands, and it is pinned by
TestTextKnownGapQueryParameterCredentialsAreEchoed. What was wrong is the prose
promising a property the code never had. The three statements now say what is
guaranteed and name the residual. The docs under-promise rather than
over-promise; no new behavior, so I did not add a CLI-level test for a gap the
gate-level characterization test already pins.
A review point of mine was wrong — retracted
I had carried probeVersion → apiclient.New(server, "") (empty token) as an
open defect from #131. It is not one: /v1/version is registered on the
public router at internal/api/api.go:211, outside the
r.Group(... r.Use(s.authMiddleware) ...) block at :220, and apiclient.New's
doc names it as a valid unauthenticated call. All three client sites in
cmds_doctor.go are correct. Retracted on #131 as well.
Current state
| head | c9e0e63, tree 9fa9c14de074b4ddb5320d4f83803f1657918b34 — equal to the remote head's tree |
go test -count=1 ./... |
32 packages ok, 0 FAIL |
gofmt -l . / go vet ./... |
clean / pass |
| doctor tests | 16 (14 from #131, +1 from #164, +1 new) — none dropped |
origin/main (7a194f1) ancestor of HEAD |
yes — no conflict, nothing silently reverted |
| CI | 15/15 success on c9e0e63 at run_attempt 1, including Go and PostgreSQL integration; also 15/15 on d30f1ff |
mergeable_state |
blocked |
What this PR needs is one independent human approval, and I am not it. I
authored b229537, d30f1ff and c9e0e63; 15/15 checks and my own sign-off are
not a review. The doctor half in particular should not be approved by me, since
I changed an assertion another author wrote in order to make the gate pass.
Not a merge authorization and not acceptance of #112, #131 or #164.
PeterGuy326
left a comment
There was a problem hiding this comment.
Blocking security review finding:\n\n leaves query strings untouched after parsing a URL, and the PR's characterization test explicitly accepts secrets in and . That means a credential-bearing server/DSN URL can still be emitted by the doctor/error surfaces. The body documents this as a known gap, but this PR cannot be approved as a complete credential egress gate while that path is intentionally echoed.\n\nPlease either extend the gate to withhold/redact credential-bearing query/fragment values (with regression tests), or split this into an explicitly narrow, non-security-complete change with a separately tracked blocking follow-up. Do not treat the current green checks as proof of no credential leakage.
PeterGuy326
left a comment
There was a problem hiding this comment.
Blocking security review finding:
server/internal/redact/redact.go leaves query strings untouched after parsing a URL, and the PR's characterization test explicitly accepts secrets in redis://.../?password=... and postgres://...?password=.... That means a credential-bearing server/DSN URL can still be emitted by the doctor/error surfaces. The body documents this as a known gap, but this PR cannot be approved as a complete credential egress gate while that path is intentionally echoed.
Please either extend the gate to withhold/redact credential-bearing query/fragment values (with regression tests), or split this into an explicitly narrow, non-security-complete change with a separately tracked blocking follow-up. Do not treat the current green checks as proof of no credential leakage.
Local Verification Summary for Issue #112Branch tested: Issue #112 Requirements Coverage
Acceptance Criteria Coverage
Test Results
Manual VerificationText output (unreachable server): JSON output: Correctly structured with Credential redaction test:
Key Implementation Details
Schema and Golden File
Notes
|
pgx reads postgres://host/db?password=... as the real password, so a URL that parses cleanly with no userinfo is not thereby proven credential-free. The gate masked userinfo and let the parameter through, which put a database password in memd's startup log and in three fields of a doctor report. Every query value now goes to REDACTED and only the parameter names survive, so a log line still says which settings are on. A fragment has no name to keep, so such a URL is withheld whole. No key-name blacklist: that is fail-open.
|
Fixed on Why this is not a documentable residual.
So the one shape the userinfo gate lets through is exactly the shape where the value is a live credential. On the binary built from the previous head, The userinfo half of the gate fired correctly on the same string. What the gate does now.
Evidence, with both sides anchored to a tree sha. The "before" numbers come from a checkout whose
Residual I am not claiming away. Parameter names still egress, so a secret smuggled into a name would still show. No driver I can find treats a name position as a credential, which is why I took the trade rather than withholding every URL that has a query string - I am the author, so I have not voted on this and this comment carries no approval. Both |
PeterGuy326
left a comment
There was a problem hiding this comment.
Review result
APPROVE for the current head e0b7972b4fe5951a754e1ca8329e2edcbfba2158.
The follow-up now blanks query values and withholds fragment-bearing URLs. I ran go test ./internal/redact ./cmd/mem locally: both packages passed. The current head has green Go, PostgreSQL integration, HTTP/CLI/MCP, Web, Worker, wrapper-compatibility, CodeQL, dependency-review, and policy checks. This approval supersedes the earlier changes-requested review at the old head.
Resolves the only collision, in CHANGELOG.md's [Unreleased] "Fixed" list, by keeping both entries: this branch's CI Web bullet and main's #165 bullet about withholding unverifiable URL credentials. No duplicate "### Fixed" heading was introduced. main's "### Changed" side won on its own: the branch's "retaining the published npm scope, MCP identity, and existing cache paths" wording is false after #162 changed mcpName to io.github.bytefolk/mem-mcp, and the merged text no longer carries that clause. GOVERNANCE.md arrived from main without conflict because this branch never added one. This commit is the conflict resolution only; the CI evidence fixes follow.
## Canonical requirement Refs bytefolk/.github#32 - Canonical Issue URL: bytefolk/.github#32 - Consumed revision: R1 - No automatic close keywords: acknowledged Decision reference: the initial R1 Issue body. It explicitly records that local candidates preceded this prospective publication record; no retrospective approval is claimed. ## Requirement trace | REQ/AC IDs | Changed files / domain | Tests or review evidence | |---|---|---| | REQ-001 / AC-001 | 4 exact-pinned version annotations | Exact expected-byte replacement PASS | | REQ-002 / AC-002 | 2 files in bytefolk/mem | Repository inventory PASS; aggregate 7 repositories, 13 files, 21 lines | | REQ-003 / AC-003 | Existing workflow content and modes | Parsed YAML and comment-stripped bytes identical | | REQ-004 / AC-004 | Current-head CI and independent review | Local independent replay recorded in the canonical R1 Issue linked above; hosted CI collected on head `f464f686` (19 of 20 checks succeed, see Validation); independent human review requested and still pending | ## File domains `.github/workflows/bytefolk-scorecard.yml` (47); `.github/workflows/bytefolk-security.yml` (58, 65, 68). Prepared parent / merge base: `2986fe38175f54d99f15dd38a498708c6ecd88cd` PR base at publication: `87db0dfe0507be2190fe2fdcce0e267be8224f4d`. Since that baseline `main` advanced by six commits through `3c13f04e` (#162, #160, #165, #188, #158, #204) — not only `web/package-lock.json` as previously stated here. None of them touched `.github/workflows/`, so the F9 workflow blobs and the PR diff are unchanged. The reviewed commit and original parent are preserved. Head: `f464f68636adc6bb5295c3818aa6654c46a3caad` — `d2a9ec5` plus one non-forced `Merge branch 'main'` commit (`f464f686`) that brought the branch up to `3c13f04e` so it is no longer `BEHIND`. Verified: `git rev-parse d2a9ec5:.github/workflows/bytefolk-scorecard.yml` and `...:bytefolk-security.yml` return the same blobs (`2058126c`, `48a507e0`) as at `f464f686`, and `git diff main...f464f68` is still exactly these 2 files, `+4/-4`. The comment-only payload is therefore byte-identical to the reviewed commit and the equality proof above holds on the current head. ## Scope and non-goals Correct only `# v4.37.4` to `# v4.37.9` on CodeQL uses-lines pinned to `cdf488f595d80d6e07e03d4674febd5ab45fa938`. The [official tag object](https://api.github.com/repos/github/codeql-action/git/tags/a35ac6e6798d72df5475948b28efb89edc2e19ca) resolves to that existing pin. Action SHAs, permissions, triggers, steps, matrices, other pins, and runtime code are unchanged. ## Validation - Exact commands: `ruby evidence/verify.rb --baseline` and `ruby evidence/verify.rb --committed` from the retained review packet; `git diff --check 2986fe3 d2a9ec5` from this repository. - Observed counts/results: PASS 2/2 files and 4/4 replacements here; aggregate PASS 13/13 files and 21/21 replacements. Baseline intentionally exits 1 after detecting all 21 stale annotations; committed verification exits 0. - Check URLs: collected on head `f464f686` — 19 of 20 checks succeed. The single failure is [`HTTP, CLI and MCP lifecycle`](https://github.com/bytefolk/mem/actions/runs/34807101354/job/103861020551), whose log is `pull access denied for minio/minio` at ~13s: a container-image pull failure in an unrelated job. The same workflow was green on `main` at `3c13f04e`, and the identical failure is present on #198 and #199, so it is not caused by this comment-only change. Root-cause tracking is separate and open. The strict verifier checks the changed-file allowlist; exact old blobs and line inventory; complete expected-byte replacement; absence of stale target annotations; parsed YAML equality; comment-stripped byte equality and SHA-256 digests; whitespace and unchanged modes; one commit with the exact parent; and clean worktrees with no untracked files. All passed. The independent replay is recorded in canonical R1. The verifier and inventory are retained outside repository commits. | ID | REQ/AC | Observable acceptance criterion | Command or manual steps | Environment | Expected | Observed | Status | |---|---|---|---|---|---|---|---| | V1 | AC-001, AC-002, AC-003 | Exact annotations with executable YAML unchanged | `ruby evidence/verify.rb --committed` | Ruby 2.6.10, Psych 3.1.0, isolated review packet | Exact scoped replacements and equality | 2/2 files; 4/4 lines; all invariants pass | PASS | | V2 | AC-004 | Hosted checks on this exact head | Inspect this PR's checks at `f464f686` | GitHub Actions | Applicable checks succeed | 19 of 20 succeed; `HTTP, CLI and MCP lifecycle` fails on `pull access denied for minio/minio` (infra, unrelated job, also failing on #198/#199, green on `main`) | PARTIAL | ## Security and compatibility Documentation annotation only. No dependencies, permissions, credentials, data flows, or runtime behavior change. The diff and commit identity were inspected for public-safe content. No CHANGELOG entry or behavior-documentation update is needed because only explanatory comments change. ## Known limitations Runtime suites, build, coverage, and dependency audits were not rerun for this comment-only change; no runtime test result is claimed. Hosted CI is separate from local equality proof. Two limits now apply: (1) the strict verifier's `one commit with the exact parent` invariant describes the reviewed payload commit `d2a9ec5`, not the current branch shape, which carries two additional `Merge branch 'main'` commits; (2) this PR is **not merge-ready yet** — repository `AGENTS.md` step 6 requires passing CI *and* an approval from someone other than the author, and `HTTP, CLI and MCP lifecycle` is red on the unrelated `minio` pull, so the green-CI half is unmet until that infrastructure failure is fixed. ## Risk and rollback Low-risk annotation correction. Roll back through an ordinary revert of this single commit. There is no migration or release action. ## Product review handoff - Implementation/publication owner: @PeterGuy326 - Automated pre-review result: independent local replay recorded in R1; no human approval implied. - Human final review: PENDING; no human review requested by this publication. - Merge ledger owner: @PeterGuy326 - Product reviewer: @PeterGuy326 - Milestone or release packet: N/A: bounded documentation annotation maintenance - Merge, CI, release, and model judgment do not accept or close the Issue: acknowledged ## Maintenance update (2026-09-14, @waterbro-8) Records written by the maintainer account, not by the implementation owner: - `f464f686 Merge branch 'main'` was pushed to this head branch (non-forced, `main` at `3c13f04e` is an ancestor of the head) to clear the `BEHIND` state this PR's own body said blocked merging. No workflow file content changed: both blobs are identical to `d2a9ec5`. - The stale facts above were corrected in place: the recorded head SHA, the "Main advanced only `web/package-lock.json` in PR #192" claim, the `NOT VERIFIED` hosted-CI rows, and the "this is a draft, not merge-ready" note. - This PR was marked ready for review and an independent review was requested. The maintainer account that pushed the merge commit did **not** approve it: `AGENTS.md` step 6 requires an approval from someone other than the author, and a commit author on the head cannot supply that approval for their own push. `@PeterGuy326` remains implementation and merge-ledger owner. Co-authored-by: 勒布朗-詹姆斯 <2986253039@qq.com>
Refs #112
Summary
A configured URL that carries credentials in a shape
url.Parsedoes not reportas userinfo reaches output today. This adds one shared gate that redacts such
a value when it can prove it is a transport URL, and withholds the value whole
when it cannot, and wires it into all three egresses R3 names: the API client,
memd's log lines, andmem doctor.The adjudicated direction this implements is recorded on #112 as R3
(comment 5536659086): gate on "parses as a recognized transport scheme with
userinfo redacted, otherwise withhold the whole value".
Scope: all three named egresses
R3 names three egresses — doctor, apiclient,
memd's log line. All three arecovered here.
mainat the basehttp.Client.DositesnewRequeston the workspace-transfer pathworkspace_transfer.go)memdstartup log lineUser != nilgate)memdfatal log linemem doctor(text + JSON)How doctor got onto this branch
mem doctoris not onmain; it only existed on #164 (which sits on #131'soriginal doctor commit). Because #131 was closed as superseded by #164, doctor
had exactly one carrier, so the surviving branch had to absorb it before #164
could be closed. Rather than re-author it, this branch merges the work:
Both earlier commits are ancestors of this head, so #164's and #131's authorship
is preserved on the merge rather than claimed as mine. #164's unrelated
per-probe
--timeoutfix and itsworkspace_transfer.gocoverage came acrosswith the merge and are kept.
What
d30f1ffitself does is replace doctor's two local string-level helperswith calls into
internal/redact, so there is one policy rather than three:c9e0e63is prose only. It corrects three statements — inCHANGELOG.md,docs/DEPLOYMENT.mdand a comment incmds_doctor.go— that asserted the doctorreport contains no secret value. That was never true of the query-parameter
shape described under Known gap, measured at 3 sentinel occurrences per output
format on this head. The non-goal stands; the promise was the thing that was
wrong, so the docs now name the residual instead of denying it.
The shape, and why the previous gate missed it
A gate written as
if parsed.User == nil { return raw }therefore echoes thecredential verbatim — not as a corner case, but as the normal path for any
value whose scheme happens to contain a colon.
The gate also refuses to scrub error text, because that cannot be made tight:
url.Errorrenders with%q, so a"inside a password arrives escaped and ascanner that pairs quotes mis-pairs, replaces nothing, and leaves its cursor
inside the URL. Withholding is chosen over partial trimming for the same reason:
a delimiter inside a credential splits the message into pieces that no longer
look like a URL, and the piece without the
@is exactly the half that leaked.memd's fatal line is included becausequeue.NewClientwraps asynq's parseerror, which embeds the whole DSN:
slog renders an error value as its text, so that reaches the log unmodified.
Known gap, stated rather than smoothed over
The gate proves the absence of userinfo, not of every credential. A secret
supplied as a query parameter (
redis://host:6379/?password=x) parses as aclean URL with
User == niland is still echoed. This is the adjudicatedscope, not an oversight, and closing it is a separate decision. It is pinned by
a named characterization test
(
TestTextKnownGapQueryParameterCredentialsAreEchoed) that fails if someonecloses the gap without updating the expectation, so the residual cannot become
folklore.
Withholding also has a real diagnosability cost, accepted by design: the reason
a request failed is still reported, the host is not.
Validation ledger
Evidence level claimed: E3 — independently reproduced. Not E4: I am the
author, and E4 requires a non-author to verify the acceptance criteria end to
end.
Environment: Linux x64, Go 1.25.0, exact tree
9fa9c14de074b4ddb5320d4f83803f1657918b34—HEAD^{tree}of the current headc9e0e63, read back fromGET /repos/bytefolk/mem/commits/c9e0e63…and equal toit byte-for-byte, so the tree measured here and the tree on the PR are the same
object. (This head was pushed through the Git Data API because
github.com:443was down on this box; the local commit sha differs from the remote one, the tree
does not.)
go build ./...go vet ./...gofmt -l .go test -count=1 ./...cmd/mem(includes 16mem doctortests)internal/redactinternal/apiclientcmd/memdgit merge-base --is-ancestor origin/main HEADsuccessonc9e0e63(run_attempt1), includingGoandPostgreSQL integration; also 15/15 on the immediately precedingd30f1ffmem doctorend-to-end, measured on built binariesSeven malformed credential URL shapes, each run through
mem doctor --server <shape> --timeout 1sin bothtextandjson,counting occurrences of a sentinel password in everything the process wrote:
fffcd4cc9e0e63http://admin:PW@127.0.0.1:1http://admin:PW@127.0.0.1:%zzhttp://admin:PW@ho st.example.comhttp://admin:PW x@127.0.0.1:1http://admin:PW@%http://admin:PW@mem.internal:99999999admin:PW@mem.internal:8787(no scheme)Shape 7 is the
url.Parse→User=nilcase from the section above: #164prints the configured URL three times (
server,detail,hint) in each ofthe two formats, so 6 occurrences is one leak per field.
A zero from a binary that has no
doctorcommand means nothing. This tablewas first produced against a build of
b229537and came back 0/0/0/0/0/0/0,which looked like a pass — the actual output was
Error: unknown command "doctor" for "mem", because that head had no doctor atall. The harness now counts
unknown commandalongsideunknown flagand everyrow above was taken with both counters at 0.
Mutation controls
Run against a copy, with the mutation confirmed present in the source before the
suite is trusted:
User != nilform turns 10 named casesred across 3 packages (
internal/redact,cmd/memd,cmd/mem).Dosite turns red exactly thatsite's test case and leaves the other three green. The four cases map
one-to-one onto the four sites, so a regression at a single site cannot hide.
Control 2 was found by this method — an earlier version of this port left one
site raw and the table caught it.
redactURL/sanitizeProbeErrorback into thistree — i.e. merging fix(cli): redact malformed URLs and request build errors #164 and not rewiring doctor — fails exactly one
test,
TestDoctorSchemelessServerURLDoesNotLeakCredentials, and its builtbinary leaks 2 sentinel occurrences on shape 7. Before that test was added,
the same mutation failed zero tests. So the merge on its own was
unprotected, and the one new test is what closes that.
Two pre-existing assertions were relaxed — named, because one is not mine
The gate withholds a value it cannot prove is a transport URL, where #164's
local helper echoed a partially cleaned one. Two assertions demanded the echo
shape specifically, so they were widened to accept redact or withhold:
cmds_doctor_test.go:418TestRedactURLStripsUserinfocmds_doctor_test.go:456TestDoctorMalformedServerURLDoesNotLeakCredentialsBoth keep their original first clause — the secret must not appear, and a
cleaned URL must still carry
REDACTED@<host>. Only the form of anacceptable answer widened. Concretely at
:418,strings.Contains(got, secret)still fails the test, and the
REDACTED@mem.internal:8787check two lines aboveit is untouched.
No assertion was deleted, and the count went up, not down: #164's 15 doctor
tests are all present on this head (0 dropped), plus
TestRedactURLStripsUserinfoand the new
TestDoctorSchemelessServerURLDoesNotLeakCredentials= 16. Theway to check this claim is mutation control 3 above: if the widening had quietly
weakened coverage, restoring #164's scrubber would have gone green, and instead
it fails a test.
Anyone who considers the withhold form unacceptable for
:456should say so on#164's thread rather than assume I settled it unilaterally — I changed an
assertion another author wrote to pass my gate.
What I did not verify
c9e0e63after the docs commit;that commit is prose and comments only, and the result is unchanged (0 on all
seven shapes). Earlier heads
d30f1ffandb229537each had 15/15 CI at thetime they were written.
fix(cli): redact malformed URLs and request build errors #164's head
fffcd4chas 0check-runsand no workflow run has ever beenrecorded for it, so doctor has never been green in CI anywhere — including
here, where it is now inside the
Gojob for the first time. That is a gain,not a regression, but it does mean no one has seen this code pass CI before.
memdwas not driven end-to-end to its fatal redis path. Reaching itrequires a reachable PostgreSQL (the queue client is constructed after
db Open), and the only local server on 5432 is an unrelated instance whosecredentials I do not have — my attempt failed at
db open: ... failed SASL authbefore touching the queue. That egress is therefore evidenced byexecuting the gate on the asynq error string measured from the real library
in this tree, not by a live
memdrun.#112's exit-code contract, R2/R3 acceptance, orfeat(cli): add mem doctor and first-run guidance that names the documented deploy/compose path #131/fix(cli): redact malformed URLs and request build errors #164 merge readiness.
Non-goals
#164's or#131's branches; nothing here is pushed to anybodyelse's ref. fix(cli): redact malformed URLs and request build errors #164 is pulled into this branch by merge, which is why fix(cli): redact malformed URLs and request build errors #164 can
be closed without losing its work.
--timeoutbehaviour, its check set andits JSON schema are fix(cli): redact malformed URLs and request build errors #164's, unchanged except where they called the old local
helpers.
This PR stays
Refson #112 rather thanCloses, because merging it would notby itself settle #112's acceptance — that needs a reviewer's decision, not just
a diff landing.
Review strength of this PR, stated plainly: 15/15 checks pass on
c9e0e63, and I am the author ofb229537,d30f1ffandc9e0e63— automated checks andmy own sign-off are not a review. What this PR is missing is exactly one
independent human review; nothing else blocks it (
mergeable=true, no conflictwith
main, which is an ancestor of this head). It needs one human approval,and I am not that reviewer — on the doctor half especially, since I edited an
assertion PeterGuy326 wrote.