fix(mt#4910): Replace six hand-rolled connection-string redactions with maskConnectionString - #3619
Conversation
…th maskConnectionString Three spellings of a hand-rolled userinfo redaction were in use across six sites. Each required a non-empty username AND a non-empty password, so a connection string with either half empty matched nothing — and a regex that matches nothing returns its input unchanged, printing the credential. The empty-USERNAME form is the severe one: postgresql://:realpass@host/db goes through verbatim with a live password. All six were also non-global, so a second connection string embedded in the same text survived regardless. Sites converted: packages/domain/src/persistence/validation-operations.ts:103 packages/domain/src/persistence/postgres-migration-operations.ts:613 scripts/verify-persistence-self-heal.ts:111 scripts/smoke-task-id-reuse.ts:73 scripts/smoke-task-kinds.ts:66 scripts/smoke-memory-domain-routing.ts:51 validation-operations.ts already imported maskConnectionString and called it 55 lines above the weak copy. Adds the multi-occurrence test to setup-db.test.ts — the `g` flag is the one property all six copies dropped, and the existing suite covered every other edge case already. Out of scope, filed as mt#4963: the sanctioned detector shares this blind spot. CREDENTIAL_SHAPES' postgres-url-credentials regex requires non-empty halves too, so the transcript-ingest scrubber, `minsky security check-credentials`, and .gitleaks.toml all pass the empty-username form unredacted. Measured.
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Focused PR replacing six ad-hoc connection-string redactions with the vetted maskConnectionString, plus a missing multi-occurrence test. The changes reduce leakage risk on empty-username/password and ensure global masking. I noted one minor inconsistency: dry-run now prints the helper’s masked form while execute-mode still shows host+pathname; consider standardizing or documenting that divergence. Otherwise, scope matches the spec, tests and typecheck remain green, and no docs updates are needed. Approving.
Findings
- [NON-BLOCKING] packages/domain/src/persistence/postgres-migration-operations.ts:236 — Inconsistent masking form between dry-run and execute paths
getPostgresMigrationsStatusnow usesmaskConnectionString(connectionString)and surfacesDatabase: ${maskedConn}in dry-run output, preserving host/port/db with***:***userinfo. In the execute path, themaskedvalue is still built vianew URL(connectionString)and renders onlyhost + pathname(no scheme / no explicit userinfo mask). Seepackages/domain/src/persistence/postgres-migration-operations.ts, theconst masked = (() => { ... })();block used by the execute-mode logs. This leaves two different display formats for the same "Database:" line depending on--dry-runvs--execute. If this divergence is intentional, consider documenting it inline; otherwise, consider standardizing onmaskConnectionStringfor execute-mode too for consistency and easier greppability by tooling that recognizes the masked form.
Documentation impact
- no-update-needed — I reviewed the diff: it replaces several inline regex redactions with the existing helper
maskConnectionStringand adds a unit test covering multi-occurrence masking. No new user-visible commands, flags, or configuration are introduced, and existing CLI lines continue to display a masked connection string. I did not read specific docs files; this verdict rests on the diff's scope and observed behavior not changing semantics beyond masking implementation. If a doc explicitly described the prior inline forms (unlikely), it would not be invalidated by this internal refactor.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verification pass: the fix commit replaces all six identified hand-rolled redaction sites with maskConnectionString, adds the missing multi-occurrence test, and wires imports correctly. I verified each changed site now calls the shared helper and that the domain-layer relative import resolves. No regressions or new defects surfaced in the touched files; behavior changes are limited to safer masking. The prior round’s non-blocking note about dry-run vs execute display format remains intentionally unchanged and tracked separately. Repo-wide grep verification and planning-run reproductions are out-of-band and thus marked Unverifiable in spec checks, but the localized code changes meet the task’s normative criteria. Approving.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
| The divergence is reproduced and recorded (discharged during planning, 2026-09-04 — see ## Planning Audit for the run). It reproduces AND the spec understated it: the inline pattern also leaves an empty USERNAME string unmasked, which exposes a real password, where the empty-password case exposes only a username and host. | Unverifiable | This criterion depends on live-run planning evidence outside the diff. I cannot verify execution logs from planning within this review. No repo artifact in the diff captures the runtime reproduction. |
| All six leaking sites (enumerated in ## Scope) call maskConnectionString. Decided against connectionTargetHost at every site — it drops the database name, which is the detail all six lines exist to convey. | Met | All six sites now call maskConnectionString: |
- packages/domain/src/persistence/validation-operations.ts: log line uses
maskConnectionString(connectionString)(validation-operations.ts:66–68) - packages/domain/src/persistence/postgres-migration-operations.ts:
maskedConnusesmaskConnectionString(connectionString)(postgres-migration-operations.ts:614–616) - scripts/verify-persistence-self-heal.ts: SKIP line uses
maskConnectionString(LIVE)(verify-persistence-self-heal.ts:114–116) - scripts/smoke-task-id-reuse.ts: prints
Database:viamaskConnectionString(connectionString)(smoke-task-id-reuse.ts:64–73) - scripts/smoke-task-kinds.ts: same (smoke-task-kinds.ts:60–68)
- scripts/smoke-memory-domain-routing.ts: same (smoke-memory-domain-routing.ts:39–52). |
| A repo-wide grep for any inline redaction of a connection-string userinfo returns no leaking site: grep -rnE '.replace(/[^,]@' --include='.ts' packages src scripts services | Unverifiable | This requires executing a repo-wide grep. I cannot run shell commands from the review. The changed files show conversions away from inline regexes, but repo-wide confirmation is not verifiable here. |
| maskConnectionString gains a test for the multi-occurrence case (two connection strings in one string; the g flag is what the inline copies drop). Added to packages/domain/src/setup-db.test.ts. | Met | packages/domain/src/setup-db.test.ts: addstest("masks EVERY occurrence, not just the first", ...)(setup-db.test.ts:102–119) exercising multiple occurrences. |
| Each converted site has a test, or a recorded reason it has none. One test added; five recorded as grep-verified — each site is template(mask(connectionString)). | Met | A new helper-level test covering the missing multi-occurrence case was added (setup-db.test.ts:102–119). The remaining five sites are simple log-template changes that delegate masking to the shared helper (already comprehensively tested in this file). Given their shape (no additional logic beyond calling the helper), per the spec’s rationale, per-site behavioral tests are not required. |
Documentation impact
- no-update-needed — The PR replaces ad-hoc connection-string redactions with the existing helper
maskConnectionStringand adds a unit test for multi-occurrence masking. No commands, flags, or user-facing APIs were added or changed; the displayed "Database:" lines remain masked, only via a safer shared helper. I checked the changed files and found no behavior that would require documentation updates.
Summary
Six sites hand-rolled a connection-string userinfo redaction instead of calling
maskConnectionString, in three different spellings. Every one of them required a non-emptyusername AND a non-empty password, so a connection string with either half empty matched nothing
— and a regex that matches nothing returns its input unchanged, printing the credential.
The empty-username form is the severe one:
postgresql://:realpassword@host/dbgoes throughverbatim with a live password. The empty-password form (
postgresql://user:@host/db, whichPostgres accepts) exposes a username and host. All six were also non-global, so a second connection
string embedded in the same text survived regardless of which halves were populated.
This is the failure mode
terminal-command-best-practices.mdc §Secret handlingnames outright — "apattern matching nothing emits its input UNCHANGED, indistinguishable from a redaction that fired" —
and the repo has already leaked a production password exactly this way (mem#808).
Key changes
Six sites converted to
maskConnectionString:packages/domain/src/persistence/validation-operations.ts:103packages/domain/src/persistence/postgres-migration-operations.ts:613scripts/verify-persistence-self-heal.ts:111scripts/smoke-task-id-reuse.ts:73scripts/smoke-task-kinds.ts:66scripts/smoke-memory-domain-routing.ts:51validation-operations.tsalready importedmaskConnectionStringand called it 55 lines above theweak copy — that site was a one-token fix.
Mask vs. host, decided once for all six:
maskConnectionString, notconnectionTargetHost. Thelatter returns
new URL(cs).host— no database name — and every one of these lines exists to answer"which database?" (three say
Database:outright). Masking keeps host/port/db while replacing bothuserinfo halves globally, so it reaches the same safety without dropping the detail that makes the
line worth printing.
verify-persistence-self-heal.tsgains a second benefit: its old rendering was://***@, andcredential-shape-check.tsrecognizes exactly one masked form —maskConnectionString's own output.://***@was not a form the checker could confirm safe;://***:***@is.Scope found during planning
The task spec named three sites. Its verification grep matched only the
:\/\/-prefixedspelling, so it was blind to the other two spellings — four of the six sites. The widened grep found
them. Full inventory and the corrected grep are in
mt#4910's
## Planning Auditand## Implementationsections.Out of scope — filed as mt#4963
The sanctioned detector shares this exact blind spot.
CREDENTIAL_SHAPES'spostgres-url-credentialsregex (credential-scrubber.ts:151) also requires non-empty halves, andit feeds three consumers: the transcript-ingest scrubber,
minsky security check-credentials, and(by the same shape)
.gitleaks.toml. Measured by running them:That is why AT3 below asserts by reading the line rather than with
check-credentials: on thisdefect class the checker cannot discriminate a fixed tree from a broken one. Filed as mt#4963 rather
than absorbed — different subsystem, different files.
Testing
The task's
## Acceptance Testsnumbering is used verbatim below.Execution evidence:
AT1 — the divergence, reproduced (discharged during planning; both functions run, not read):
AT2 / SC3 — the widened grep returns zero leaking sites. The form the spec originally specified
(
[^)]*) cannot span a), so it could not see the sanctioned helper's own regex — a probe thatcould not fully fail, which is this task's own defect class. Corrected to
[^,]*:Six hits, none leaking: the sanctioned helper (line 23 — expected, and what makes this grep able to
fail); the
:6543→:5432port swap;getConnectionInfo, which masks the empty-half cases correctlyand is parked on mt#4963 behind PR #3412 and mt#3497; an
@mentionstripper in rule prose; and twosites that STRIP userinfo to build a host or substring needle rather than to print a haystack.
AT3 —
persistence checkagainst the live database, asserting the mask fired rather thanprinting the raw line:
AT5 / related tests:
Typecheck 0 errors across 8 projects (session workspace); lint 0 errors / 0 warnings across 4376
files.
Negative control — AT4, per site, against the un-fixed patterns:
Each pre-fix pattern was run verbatim against the failing inputs alongside the new helper. Sites
1–2 leave BOTH empty-half forms unchanged; sites 4–6 leave the empty-password form unchanged; all
three spellings drop the second occurrence.
On the added test, stated plainly: the multi-occurrence test in
setup-db.test.ts(SC4) coversthe
gflag, the one case the existing five-case suite did not. It exercises the HELPER, which thisPR does not change, so it passes on
maintoo and has no negative control of its own — the controlabove is at the six call sites, which is where the behaviour changed.
SC5 — no per-site behavioural test was added, and the reason is the code shape. Each site is
template(mask(connectionString)): a log or format line whose only behaviour IS which function itnames. The value is already tested at the helper across all six edge cases, so a per-site test could
only assert which function is called — and the honest instrument for that is AT2's grep. Reaching
the two domain lines behaviourally would need a live Postgres (
getPostgresMigrationsStatusopens aconnection before computing
maskedConn) or aspyOn(log, "cli"), which/implement-task§6 namesas design feedback rather than a test to write, and which
TEST_LOGGER_SILENCED_FLAGwould silenceanyway.
Runtime import checked, not just typechecked. The
@minsky/domain/persistence/connection-stringsubpath was confirmed to resolve at runtime via a
bun -eimport returning the working function —the mem#577 / mt#2760 hazard where a subpath passes tsconfig
pathsand fails bun's packageexports.Deploy verification: three of the seven changed files are deploy surface per
isDeploySurfaceFile(packages/domain/src/persistence/{postgres-migration-operations,validation-operations}.tsand
packages/domain/src/setup-db.test.ts; the fourscripts/files are not). The post-mergedeploy will be verified per
/implement-task§10 withnotBeforeset to the merge timestamp andexpectCommitShaset to the merge commit.