Mask secrets in the lane's title, step and error before they leave the daemon

Merged
TE
18d ago
+694-61

The Lanes view showed raw prompt text in each lane's title and step line, so a pasted token appeared on screen and went to every fleet peer. acpmux now masks secrets in the lane before it leaves the daemon, and the clients read only the masked lane. Follow-up to #21727, part of cmuxterm-hq#2509.

What changed

  • New acpmux::secret_scan module. find and mask replace each secret with its kind: [github token], [anthropic key], [jwt], [private key], [auth token], [credentials], [secret]. It catches provider keys (Anthropic, OpenAI, Stripe, GitHub, Slack, AWS, Google), JWTs, PEM private keys (truncated ones too), auth headers, credentials in URLs, NAME=value secrets, and long random runs. The masked text no longer holds the secret, so nothing downstream can reveal it. The module only reads text, so transcripts, notifications and the phone relay can use it later.
  • The lane has two new fields, step and title, and lane.error is now masked as well.
    • step is the running prompt or the agent's last reply.
    • title is the session's title, falling back to its first prompt.
    • Both are scanned only within their first 4,096 characters, then masked, then cut to 240 characters. Masking happens before the cut, so a token that crosses the cut can't leave half of itself behind.
  • When an older peer sends a lane without title, this daemon decides that lane again, so the peer's lanes come out masked too.
  • The webview and Swift lanes read only lane.title, lane.step and lane.error. The raw prompt, title and error fallbacks are gone. Against an older daemon, a lane shows the session name and no step.
  • AgentActivityLane.of no longer takes a title:, since the one its callers passed could be the raw first prompt.
  • The protocol IR and generated web types now include step and title. Re-blessing the IR also picked up two stale LifecycleReceipt descriptions from the base branch.

Left alone on purpose: git SHAs and other hex, UUIDs, paths, branch names, camelCase identifiers, sk-learn-… package names, data:…;base64, images, and code or config that only names tokens (max_tokens: 100000, crate::secret_scan::mask, token: expired). For an assignment to be masked, its value must contain a letter and a digit. The module doc lists the known misses: short passwords with no recognizable shape, and values containing ;, , or spaces.

Next: a stacked PR replaces these hand-written patterns with gitleaks' rule set (MIT, pinned to v8.30.1) and adds Nosey Parker's examples as a fixture corpus, keeping the same find/mask API.

Question for Leo

The summary still sends these fields raw to clients and peers: turn.prompt, lastPrompt, lastMessage, preview and title. Should the daemon mask them for peers too (remote chats would then show masked text), or stay with only the lane fields the Lanes view uses?

Verification

  • Red first:
    • 50d0d57bcc and ac4a0b3aab add the tests.
    • On ac4a0b3aab, the webview test a lane's title and error are the daemon's masked ones failed (1 of 8). On Huge Blue, the Rust tests the_lanes_title_is_masked… and ordinary_text_is_left_alone failed, and so did a_lane_from_an_older_peer… once its fixture gained indicator. That fixture change landed in b3b7f148e8.
    • On 50d0d57bcc, the step test failed (1 of 7), and the Rust tests did not compile because the module did not exist yet.
  • Green on the head, run on Huge Blue: cargo test -p acpmux --lib, protocol_ir_is_current, cargo fmt --check, the webview priority and lanes tests, and protocol codegen with no diff.
  • The fake secrets in the tests are assembled at run time, so no committed file contains a token a scanner would flag. Each masked case also checks that the secret is gone and that a second scan finds nothing.
  • Not run locally: the Swift AgentLanesTests change. That is left to CI.

The protocol spec changed (Lane.step, Lane.title), so this may need a coordinator WINDOW.

Changelog

Fixed: The Lanes view masks API keys, tokens and other secrets in each lane's title, step and error, including for fleet peers.

🤖 Generated with Claude Code