Skip to content

[RT-SW-399] fix: contender stderr redacts a handoff ticket the tail cut or the pipe end splits - #404

Merged
Blind-Striker merged 1 commit into
masterfrom
fix/399-contender-redaction-at-the-cut
Oct 16, 2026
Merged

Blind-Striker merged 1 commit into
masterfrom
fix/399-contender-redaction-at-the-cut

Conversation

@Blind-Striker

@Blind-Striker Blind-Striker commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

What

The background-service contender's stderr tail no longer carries any part of the PTY-handoff ticket. That includes a ticket split by the 8 KiB cut, and a ticket the pipe ended inside.

Closes #399.

Cause

ServiceContender cut the raw stderr to its final 8 KiB first (AppendTailLocked). Only then did it redact the ticket from the cut tail (RenderStderrLocked → RedactTicket), and only whole occurrences. When a ticket straddled the cut, the part left inside the tail no longer matched, so it stayed unredacted in Stderr and in the contender's failure messages (OpenCodeServerException).

The change

  • Slack before the window. The raw tail keeps the rendered 8 KiB plus one ticket length minus one byte. A ticket the 8 KiB edge cuts through is therefore still whole in the retained bytes.
  • Byte-level matching. Rendering matches the ticket in bytes, starting that slack before the window. Every occurrence that reaches the window becomes [redacted], and only then are the bytes decoded and trimmed.
  • A ticket the pipe ended inside. If the tail ends with a proper prefix of the ticket (the pipe ended or faulted mid-write), that prefix is redacted as well.
  • No ticket, no change. Without a ticket the output is byte-identical to before: the final 8 KiB, decoded and trimmed.

Upstream

At the pin (external/opencode 042d083), upstream's contender does not have this bug, because it redacts nothing:

  • packages/client/src/service-contender.ts:28-34 (onStderr) keeps the last 8 KiB raw;
  • :48 (stderr()) returns it trimmed;
  • :124/:126 put it verbatim into startupError (:136-138, new Error(message + "\n" + stderr)).

The ticket travels as OPENCODE_PTY_HANDOFF = JSON.stringify(handoff) (packages/client/src/pty-handoff.ts:77). The server deletes it from its own environment at startup (packages/cli/src/server-process.ts:48-49). Upstream's exposure therefore rests on a child that never echoes the ticket.

The SDK's masking is its own stricter floor (ADR-0028's principle: secrets never reach diagnostics), and this PR closes a gap in that floor; it is not a parity change. Not reported upstream (maintainer, 2026-10-17).

Behaviour

Diagnostics only. A contender failure message or Stderr read no longer shows a fragment of the ticket. Everything else is unchanged.

The CHANGELOG has an entry under 🐛 Fixes.

Proof

  • Red first. A new test file, ServiceContenderTests.cs, uses an in-memory stderr, so the drain finishes inside the constructor and every verdict is decided before the first assertion. On the unchanged code, four of its first six rows failed on net10.0 and net472: the cut falling at 1, 13 and 64 of the ticket's 65 bytes, and the pipe-ended case. The whole-ticket and no-ticket rows were green.
  • Review round. Two more rows came from the first review: overlapping occurrences, and an unfinished ticket longer than 8 KiB. Both were red on the first fix and are green now. After the fix, 8/8 pass on net10.0 and net472.
  • Mutations (each builds; an earlier non-compiling form was discarded as stale binaries):
    • retaining exactly 8 KiB, with no slack, turns the three straddle rows and the long unfinished ticket red;
    • no unfinished-prefix check turns the pipe-ended row and the long unfinished ticket red;
    • a non-overlapping search turns only the overlap row red.
  • Checks: a full build with 0 warnings and 0 errors on every TFM; both format passes; verify-clones with 0 new.
  • Local check: GATE PASS (windows-gate.ps1 -Part all -NoNativeAot on 54b47ea2, this branch on master d13d9833): slopwatch, build, both format passes, clone-detector, verify-clones, 10,013 tests including net472 with 0 failed, election.
  • Reviews: the Runtime coordinator and two fresh read-only Sol rounds.
    • Round 1 found two blockers: overlapping occurrences, and an unfinished ticket longer than 8 KiB. Both are fixed: covered bytes are marked in a mask, every occurrence is searched, and the prefix check runs over the whole retained tail.
    • Round 2 found nothing.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…pe end splits (#399)

The contender cut its stderr to the final 8 KiB before redacting, and matched
whole tickets only, so a ticket straddling the cut kept its suffix in Stderr and
in failure messages. The raw tail now keeps one ticket length of slack before
the rendered 8 KiB, tickets are matched in bytes before decoding, and a ticket
the pipe ended inside is redacted as the prefix it is.
@Blind-Striker
Blind-Striker added this pull request to the merge queue Oct 16, 2026
Merged via the queue into master with commit 4f41b45 Oct 16, 2026
12 checks passed
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.

Contender stderr redaction misses a handoff ticket that straddles the 8 KiB tail cut

1 participant