fix!: stop leaking credentials, parse SSE events properly in the tool wall, one error writer - #162
Merged
Merged
Conversation
… wall, one error writer
Security
- WebSocket forwarding builds its URL through forward::upstream_url, so the
gateway key in `?key=` is stripped there too.
- Every credential-bearing reqwest client (data plane, OAuth refresh, model
list, probes, ChatGPT/Z.ai login, replay) no longer follows redirects; 3xx
goes back to the client. The price-sheet fetch carries no credentials and
uses tw_gateway::public_client(), which follows redirects but refuses
https -> http.
- Secret files are created 0600 at open time instead of chmod after write:
config.yaml and history (write_atomic), adopted client configs and their
sidecars, requests.db (pre-created before SQLite opens it), request bodies.
A new unix data directory is created 0700. Adopted configs keep the
original file's mode when it existed.
- Process detection runs /bin/ps, not ps from PATH.
Tool-call wall
- SSE parsing follows the spec: `data:` without a space, and events spread
over several data lines (joined with \n). Frame boundaries come from
tw_dialect::frame::frame_end; the duplicated find_frame_end in wall.rs and
redact/sse.rs is gone.
Errors and dialect helpers
- error::Dialect is removed; GatewayError uses tw_dialect::ir::Dialect and
tw_dialect::convert::{error_body, error_frame}. New error_frame writes a
standalone in-stream error per client format (Responses gets
response.failed, not a Chat-shaped frame). Gemini 502 maps to UNAVAILABLE.
- New tw_dialect::official: is_official_host (also rejects ?/#/\ userinfo
spoofs), ANTHROPIC_VERSION, fallback_max_output_tokens. Provider::
is_official_endpoint is removed.
- tw_guard::redact::replace::redact_plain removed (test-only).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fylorn
force-pushed
the
fix/review-security-dialect
branch
from
September 24, 2026 06:10
a466c21 to
df86012
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes from a review. One PR, two halves.
Security
ws::upstream_urlappended the raw query. It now goes throughforward::upstream_url(which stripskey=) and only swaps the scheme.base_client_buildernow setsredirect::Policy::none(). That builder is behind every credential-bearing client in core (data plane, per-provider clients, OAuth refresh, model list, probes, ChatGPT / Z.ai login, replay), so a 3xx goes back to the client. The price-sheet fetch carries nothing secret and uses the newtw_gateway::public_client(): follows redirects, refuses https→http. Integration test with two local servers (localhostvs127.0.0.1).write_atomic(config.yaml + history), adopted client configs (tmp file is 0600; the original file's mode is restored afterwards when it existed, since those are other apps' files), adoption sidecars,requests.db(pre-created 0600 before SQLite opens it;-wal/-shminherit the main file's mode), request-body blobs and their directories (created 0700). A new unix data directory is created 0700; existing ones are left alone. Windows ACL path unchanged.detect.rsruns/bin/psinstead ofpsfrom PATH.Tool-call wall
data:without a space and events spread over severaldata:lines were invisible to the wall. Lines are still checked one by one (early detection on unfinished frames), and when no single line parsed, the whole event's joineddatais checked at the frame boundary. Frame boundaries come fromtw_dialect::frame::frame_end(now public, along withframe::parseandframe::data_of); both copies offind_frame_endare gone.Wall's public API is unchanged.Errors and shared dialect helpers
error::Dialectremoved.GatewayErrorusestw_dialect::ir::Dialect,tw_dialect::convert::error_bodyand the newtw_dialect::convert::error_frame(client, status, message) -> String(a standalone in-stream error frame; Responses getsresponse.failedinstead of a Chat-shaped frame).KeyPosition::dialect()replacesDialect::from_key_position;ClientApi::error_dialectis gone (usedialect()).authentication_error, 403permission_error, 429rate_limit_error,code: null), same as converted upstream errors. Gemini'sstatusfollows the code (500INTERNAL, 502UNAVAILABLE). Thex-thinkwatch-errorheader still carries the slug.tw_dialect::official:is_official_host(url)(also rejects?@,#@,\@spoofs),ANTHROPIC_VERSION,fallback_max_output_tokens(model).Provider::is_official_endpointis removed; the desktop keeps its price-table layer intranslate::default_max_tokens.tw_guard::redact::replace::redact_plainremoved (test-only).Checked: fmt, clippy
--all-targetsand--lib, bedrock clippy + test, full workspace tests.🤖 Generated with Claude Code