Stop printing the database password

The report opened with the connection string it was about to work on, taken
straight from AIDND_DATABASE_URL. On the hosted deploy that string carries the
Neon password, so the first line of every run put a live credential into the
console — and from there into scrollback, a screenshot, or a pasted bug report.
Nothing in the output said it was there to notice.

It now prints scheme, user, host and database name. The query string goes whole:
sslmode is the only part worth reading, and some drivers accept a password
there as well.

631 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tqgupw5CZGjSZrUTNUd4fW
This commit is contained in:
Claude
2026-08-31 15:13:47 +00:00
parent eeab8ef5bb
commit 745a4ea9f3
4 changed files with 40 additions and 3 deletions
+17
View File
@@ -243,6 +243,23 @@ def test_the_rewrite_prompt_is_the_one_the_app_sends(db):
# --------------------------------------------------------------------- the tool
def test_the_database_line_carries_no_password():
"""The report names the database it is about to rewrite. That line ends up
in a console, a screenshot or a pasted bug report."""
shown = rewrite_memories.safe_dsn(
"postgresql://parth:hunter2@ep-cool-frost.us-east-1.aws.neon.tech/aidnd"
"?sslmode=require")
assert "hunter2" not in shown
assert "sslmode" not in shown # a password can be passed there too
assert shown == ("postgresql://parth@ep-cool-frost.us-east-1.aws.neon.tech"
"/aidnd")
def test_an_unparseable_database_url_shows_nothing_at_all():
assert rewrite_memories.safe_dsn("not-a-url") == "(configured)"
def test_without_write_nothing_changes(db, monkeypatch):
adventure = make_adventure(db)
first, _ = fill_bank(db, adventure)
+21 -1
View File
@@ -93,9 +93,29 @@ import asyncio
import sys
from pathlib import Path
from urllib.parse import urlsplit
from sqlalchemy import func, inspect as sa_inspect, select
def safe_dsn(url: str) -> str:
"""A connection string with the credentials taken out.
The report says which database it is about to rewrite, which is worth
printing. The password in a Neon URL is not: this output goes to a console,
a screenshot, or a pasted bug report, and the operator has no way to know
the line carried a credential until it is somewhere else.
"""
parsed = urlsplit(url)
if not parsed.hostname:
return "(configured)"
who = f"{parsed.username}@" if parsed.username else ""
port = f":{parsed.port}" if parsed.port else ""
# The query string is dropped whole. `sslmode` is the only part anyone
# wants to see, and some drivers accept a password there too.
return f"{parsed.scheme}://{who}{parsed.hostname}{port}{parsed.path}"
def words(text: str) -> int:
return len(text.split())
@@ -111,7 +131,7 @@ async def main(args) -> int:
from app.providers import OpenAICompatibleProvider, ProviderError
db = SessionLocal()
print(f"database: {DATABASE_URL or DB_PATH}")
print(f"database: {safe_dsn(DATABASE_URL) if DATABASE_URL else DB_PATH}")
if not sa_inspect(db.get_bind()).has_table(models.Adventure.__tablename__):
# A mistyped path creates an empty SQLite file rather than failing, so
# say what is wrong instead of raising "no such table: adventures".