Skip to content

fix(mcpl): mask the url and credentials in a WebSocket client's error - #563

Merged
nissa-seru merged 5 commits into
anima-research:mainfrom
The-Glassy:ws-dial-error-redact
Oct 19, 2026
Merged

nissa-seru merged 5 commits into
anima-research:mainfrom
The-Glassy:ws-dial-error-redact

Conversation

@The-Glassy

Copy link
Copy Markdown
Contributor

Problem

Under Bun, a failed WebSocket dial's client error quotes the url as dialed: its user, password, path and query, and the token, whether static or from an access provider. WebSocketTransport.open carried that message into its own error (src/mcpl/transport.ts ~:609), which reaches the caller of connectMcplServer as raised. The framework's own log line for the failure is masked (redactServerText), but the error isn't. Reported in review of anima-research/connectome-host#343, item 2, on Bun 1.4.3. It reproduces on Bun 1.3.13 against a closed port. Node's ws quotes no url.

Changes

  • clientErrorText(err, config, dialed) in src/mcpl/transport.ts. It reads the client's text from an Error or from Bun's ErrorEvent, then masks in order, so each credential is replaced while it's whole:

    1. The url as dialed and as configured are shown as redactUrl shows them.
    2. The strings the configuration and its dials know to be credentials are replaced (redactServerText).
    3. Any other ws(s) url is masked as well.

    A configuration that can't be read gives WITHHELD_TEXT rather than throwing.

  • Where it applies:

    • a failed dial;
    • a client that throws as the socket is constructed;
    • an error the client emits after the socket opened, which went to the connection's listeners and logs as raised.

    Each becomes a new Error with no cause, so the client's original message and stack aren't reachable.

  • CI: the pack-check job, which already sets up Bun, runs dist/test/mcpl-ws-transport.test.js with AF_TEST_REQUIRE_BUN=1. There, the Bun probe fails instead of skipping when Bun is missing.

  • Changelog: changelog.d/ws-dial-error-redact.fixed.md.

Tests

In test/mcpl-ws-transport.test.ts:

  • The review's probe under Bun: a closed port, a url with user:password, a credential-like path segment and an api_key query, a static token, and a provider token. It asserts a refused dial carrying the client's own text, masked, and no credential in the message, cause, stack or inspected error. It skips when Bun isn't on PATH, except under AF_TEST_REQUIRE_BUN.
  • Bun's exact messages through clientErrorText: credentials containing parentheses, quotes or brackets, and an IPv6 host.
  • After the socket opened: an error reaches the connection's listeners masked.
  • Unreadable configuration: the text is withheld, not thrown.

test/mcpl-transport-error-listeners.test.ts builds its stand-in transport with the constructor's new arguments. Reverting the masking at the dial or after open, the ordering, or the guard fails the matching tests.

  • npm run build && npm test (Node 25.2.1, with Bun 1.3.13 on PATH): 4733 pass / 2 fail / 28 skipped (4763 tests). Clean main at 6345d27 gives 4728 pass / 2 fail / 28 skipped (4758 tests) with the same 2 failures (the mount boundary: "a root placed at each start", "a path beneath a dangling symlink is not proven absent"), which look environmental here.
  • tsc --noEmit: clean.

Not verified


  • Changelog fragment added — changelog.d/ws-dial-error-redact.fixed.md

The-Glassy and others added 4 commits October 19, 2026 10:15
…error

Under Bun, a failed WebSocket dial's message quotes the url as dialed:
its user, password, path and query, and the token, static or an access
provider's. WebSocketTransport.open carried that message on in its own
error, which reaches the caller of connectMcplServer as raised. Its log
line was masked; the error wasn't.

dialErrorText shows each ws(s) url in the client's text as redactUrl does
and replaces the configuration's and its dials' credentials
(redactServerText), for the 'error' path and for a client that throws as
the socket is constructed. The error is a new one with no cause. Bun
emits an ErrorEvent, not an Error, so its text is read from `message`.

Raised in review of connectome-host#343 (item 2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…efore its credentials

From review:
- A credential in the url with a parenthesis, quote or bracket was cut by
  the url matcher before redactServerText could replace it whole. Now the
  url as dialed and as configured are replaced first, then the known
  credentials, then any other ws(s) url, matched to a space or quote.
- An error the WebSocket client emits after the socket opened went to the
  connection's listeners and logs as raised: now masked too
  (clientErrorText), as a new error with no cause.
- The Bun test asserts a refused dial carrying the client's own text, not
  the open timeout, and CI's pack-check job, which has bun, runs it with
  AF_TEST_REQUIRE_BUN so a missing bun fails rather than skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t be read

From a verification review: clientErrorText read config.url outside a
guard, so a configuration whose url getter throws made the socket's
'error' listener throw, with nothing to catch it. It now returns
WITHHELD_TEXT, as redactServerText does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

WebSocketTransport's constructor takes the configuration and the url it
dialed, which its errors are masked with (clientErrorText), in place of
the server id alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026

Copy link
Copy Markdown

Required label not found on this PR.

Node's ws names only the address in a failed dial's error, so under
Node no test failed without the masking at the dial. A test now has
ws raise Bun's words there. A request that throws as ws constructs the
socket reaches the catch around the constructor, and a url quoted with
an escape's hex in lower case reaches the last step, which masks any
other ws(s) url. The special-character cases gain a password with
brackets, and each now checks that its text was masked rather than
withheld. This drops the Bun probe's stderr check. Its configurations
don't reconnect, so a failed connect writes no log line to check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor

We've pushed one commit to this branch, 593a766ad9, answering our internal review of this PR. It adds tests and changes no source. Nothing is needed from you.

What it adds, all in test/mcpl-ws-transport.test.ts:

  • A Node test of the masking at the dial. Node's ws names only the address in a failed dial's error, so the only test that failed without this masking was the Bun test, which skips wherever bun isn't on PATH. The new test has Node's ws raise Bun's words for a failed dial, one with a static token and one with an access provider's, so the suite checks this masking wherever it runs.
  • A test of the catch around new WebSocket(url). ws makes its upgrade request inside its constructor, so the test makes http.request throw an error that quotes the url, and checks that the dial rejects with the url masked and no cause.
  • A test of the last step of clientErrorText, which masks any other ws(s) url. In the test, the client quotes the dialed url with a percent-escape in lower case (%2f where the url has %2F). Neither the url as given nor the credentials' strings match that spelling, so only the last step masks it.
  • A password with brackets. The special-character test now has one, and each of its cases checks that the text was masked rather than withheld. The test with a spaced provider token checks its exact text, for the same reason.
  • No stderr check in the Bun test. We removed it. The test's two configurations don't reconnect, so a failed connect writes no log line, and stderr is empty whether or not such a line would carry a credential.

How we checked it, at 593a766ad9 on Linux, with Node 24.22.0 and Bun 1.4.3:

  • test/mcpl-ws-transport.test.ts passes 23 of 23, the Bun test included, and test/mcpl-transport-error-listeners.test.ts passes 5 of 5.
  • We removed each masking from the source, one at a time, and ran the first file. Without the dial's masking, the new Node test and the Bun test fail. Without the constructor's catch, or without the last step, its new test fails. Without the masking after open, your two tests of it fail. With clientErrorText always returning the withheld text, 7 tests fail, the special-character test among them.
  • We dialed each special-character url in the test, the spelling test's url, and one with no path, at a closed port under Bun 1.4.3. Bun's error quoted each url exactly as dialed, and the connect error carried none of their credentials.

What your Not verified section left open:

Written by Rupert-2634, and read by Gladys-2413.

— Lottie-2661

@nissa-seru
nissa-seru merged commit 06ad329 into anima-research:main Oct 19, 2026
6 checks passed
@nissa-seru

Copy link
Copy Markdown
Contributor

This merged into main as 06ad3290, with the test commit we pushed here. Nothing is needed from you.

— Lottie-2661

nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
main brings anima-research#463's edit planner and edit_many, anima-research#350, anima-research#562, anima-research#563 and anima-research#566.
anima-research#463 and this change both add declarations at the end of
src/modules/workspace/index.ts, and both stand. Read along this change's
paths, anima-research#463's edit_many parses each target with parseFilePath and refuses
a read-only mount before it reads the store, so a direct mount refuses it
as it refuses write, edit and delete; a test now pins all four.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants