| D-M4-35-1 |
2026-10-20 |
The controller is the only owner of the ephemeral X25519 key: it creates the pair right before openTerminalSession or attachTerminalSession (only its hash leaves), hands it to NkInitiator and calls destroy() of cryptography (the private bytes are zeroed) right after message 2 and on every abort (cancelled step-up, refused request, key mismatch, closed stream). ephemeral_key_test.dart checks that the pair is destroyed and no longer extractable. |
Brief Design note 1, R-CL-15; a weak reference cannot be checked reliably in a VM test, zeroing can. |
| D-M4-35-2 |
2026-10-20 |
The escape filter (render/escape_filter.dart) sits in terminal_ui in front of xterm on the byte stream before UTF-8 decoding. Allow list: text and C0 controls pass; ESC sequences pass except DCS, SOS, PM and APC (dropped with their content); CSI passes except window operations and reports (final t), status reports other than CPR (n unless 6), DA3 (c other than DA1 and DA2), every $ intermediate (DECRQM and the report family), DECREQTPARM (x) and XTVERSION (> q); every OSC is dropped except OSC 8, which becomes inert text plus a link record. The only answers left are CPR, DA1 and DA2. On top, sequences that make xterm 4.0.0 throw or hang are dropped (a parameter above EscapeFilter.maxParameterValue, SGR 38 or 48 without sub-parameters or with a palette index above 255, REP above EscapeFilter.maxRepeatCount, CSI 0 X), and TerminalScreen catches any renderer exception so hostile output cannot end the session as tampering. |
Brief Design note 2, 24 11.2, SEC-TERM-6, TM-T14; the blocks must not depend on the renderer's version. |
| D-M4-35-3 |
2026-10-20 |
Pins compare the key bytes only (base64url decoded): no pin opens and pins noise_public_key, key_generation, fingerprint and first seen after the first successful handshake; the same key writes nothing; a different key shows the warning whatever key_generation says, and a confirmed change sends pin_changed = true (open and attach) and writes the new pin only after message 2. |
Brief Design note 3, 15 23.3, TM-T18. |
| D-M4-35-4 |
2026-10-20 |
lib/testing.dart holds FakeHost (static X25519 key, NkResponder of bothy_noise/testing.dart, checks the token of message 1, records the device frames, sends data, exit, the replay and any close code) and FakeTerminalTransport (in memory, hello and identify). The fakes of the server API, the pins, the step-up, the clock, the links and the prompts are test support of the package (test/support/). |
Brief Design note 4; M4-36 and M4-38 test against the real host service. |
| D-M4-35-5 |
2026-10-20 |
xterm 4.0.0 (2024-02-27, MIT) is adopted: no advisory in OSV or the GitHub advisory database on 2026-10-20, no security issue in its tracker. Its own handlers are the second layer: the view leaves onTitleChange, onIconChange and onPrivateOSC unset (OSC 52 and OSC 8 reach only onPrivateOSC; there is no clipboard handler). Bugs found and handled in the view: TerminalView.onTapUp is never called (a Listener detects link clicks), CPR answers zero-based (the screen adds one to row and column), cursor and selection are painted over the text (the view paints its own, D-M4-35-18); palette index 15 shows as white (not handled; a color, no security effect). |
Brief Design note 5; details and sources in 24 12. |
| D-M4-35-6 |
2026-10-20 |
Names differ from the brief: the server port is TerminalServerApi (the generated TerminalApi of api_client has the brief's name); the step-up port is the typedef TerminalStepUp (session.stepUp.confirm as a tear-off, because StepUpService is a final class built over the internal SessionCore); the snapshot is TerminalTabState (the generated enum TerminalSessionState has the other name); the dialogs answer the new port TerminalPrompts (DialogTerminalPrompts in the package); the controller also takes selfUserId (24 8.4) and AppConfig (client.terminal.scrollback_lines). |
Brief "Boundaries": follow the merged code and list each difference. |
| D-M4-35-7 |
2026-10-20 |
Close codes: no reaction retries by itself. 4100 ends (blocked with a danger banner when close_reason is a revocation: access_revoked, i6_violation, user_disabled, device_revoked, session_revoked, host_disabled, host_retired, host_rekeyed), 1000 ends, 4104, 4002 and 4012 block, 4101 detaches and the next "Reattach" asks for a step-up before the request, 4102, 4103, 1001, 4013, 4014, 4004, 4008 and a broken stream without a code detach with their own text and offer "Reattach" as a manual action. |
15 23.2 item 8, asyncapi x-close-codes; an automatic reattach on mobile foreground or network return is M5's wiring. |
| D-M4-35-8 |
2026-10-20 |
Attach runs its own step-up: on 401 step_up_required it runs one step-up and repeats the request once. An app must therefore not let the StepUpInterceptor of auth answer attachTerminalSession itself (M4-36 wires the adapter accordingly). |
15 23.2 item 6, 24 6.2; the warning of 24 8.4 must come before any step-up. |
| D-M4-35-9 |
2026-10-20 |
Superseded by D-M4-35-18. The cursor is an opaque underline in color.terminal.cursor, not a block: xterm 4.0.0 paints a block over the character and has no field for color.terminal.cursor-text, and a translucent block failed the contrast of components.md 12 (red team: 2.17:1 and 3.66:1). The selection keeps selection-bg at the opacity terminalOverlayAlpha (0.5), because xterm paints it over the text too. Search hit colors use selection-bg and fg (unused by 4.0.0). |
components.md 12 contrast rules; the character under the cursor keeps the text contrast, the cursor its pairing against color.terminal.bg. |
| D-M4-35-10 |
2026-10-20 |
The warning of 24 8.4 compares Host.assigned_user_id with the person's own user ID and nothing else (the server decides every right); it comes after the pin warning and before the dialog "Open terminal" and the step-up, for open and for attach (before the attach request). |
24 8.4, TM-T26, 15 1 principle 1. |
| D-M4-35-11 |
2026-10-20 |
The replay is collected in memory between replay_begin and replay_end; the old screen stays until replay_end and is then replaced by a fresh terminal model; input is not sent while a replay runs. |
15 23.2 item 6. |
| D-M4-35-12 |
2026-10-20 |
Copy and paste in the screen use Cmd on Apple platforms and Ctrl+Shift elsewhere, so Ctrl+C and Ctrl+V stay with the shell; paste goes through the guard (GuardedPasteIntent), never through xterm's own paste action. The extra keys bar latches Ctrl and Alt for one key on a tap and holds them on a long press; latched modifiers apply to single typed characters. |
15 23.4, 01 screen M8; the remaining shortcuts are M4-36. |
| D-M4-35-13 |
2026-10-20 |
The roles color.terminal.* with their contrast pairings and the section "Terminal view" of components.md are brought from the plan into this base as their own commit; this base lacked them and the renderer theme is generated from them. |
Task of this build; brief rule 10. |
| D-M4-35-14 |
2026-10-20 |
Base and landing: branch feat/M4-35-terminal-ui from int/m1-wave47 (9d5db53) with the built but not yet integrated feat/M2-44-terminal-sessions-relay merged in first; the branch is not merged locally and not pushed by this build (the integration lane lands it), which replaces the brief's own landing steps. |
The task of this session. |
| D-M4-35-15 |
2026-10-20 |
make arch rule 10 lets a test library (lib/testing.dart or lib/src/testing/) import the test library of a member it reaches; production code still cannot import any test library, so it cannot reach one through another. tools/arch-check has a test for it. |
The brief puts the fake host on NkResponder of bothy_noise/testing.dart into lib/testing.dart; rule 10 refused that import although both sides are test support. |
| D-M4-35-16 |
2026-10-20 |
Red team round 1 (NEIN, seven Majors), fixed with a test each: pasted text loses ESC, DEL and the C0 and C1 controls other than tab, line feed and carriage return before the confirmation and the send (a single line with ESC[201~ and Ctrl-O ran a command without a dialog); an OSC 8 target must be printable ASCII and the confirmation shows its host first in a scrolling dialog (a target with U+2028 hid the real host); dropped link records dispose their xterm anchors (the view froze); "End", a revocation or a closed tab during a pending attach are no longer overwritten; a closed stream stops its frame pump, so a stale resize tick neither sends nor blocks, and an older stream loop leaves the key of a newer attach alone; CPR answers count from 1; the cursor is an opaque underline (D-M4-35-9). |
docs/quality/red-team-gate.md; red team report of M4-35. |
| D-M4-35-17 |
2026-10-20 |
Class fix of the delta red team (findings M4 of round 1, N2 and N3): one ownership token for every flow of a terminal tab. An opening, an attach and the stream loop it starts own the generation they began with; End, Detach, a revocation, a block and dispose start a new one. The state has one writer, _set(epoch, state), which ignores a flow that lost the tab; every await of a flow goes through _owned, which stops it there; the stream, the pump and the ephemeral key change only under the same check. Proof: a model test interrupts the opening and the attach after every microtask with End, Detach, a revocation and dispose, for a normal handshake, a failing connect and a broken message 2, and checks that a final state of the action stays, that only a connected or exited tab holds a stream, that every key is destroyed, that no frame but close follows the action and that a disposed tab notifies nobody. |
Owner rule of 2026-10-17 (the same class a second time is fixed at one place with a test over the class); red team reports of M4-35 (round 1 and delta). |
| D-M4-35-18 |
2026-10-20 |
Class fix of the delta red team (round 1 M7, N1 and N4): what xterm 4.0.0 paints for the cursor and the selection is not what components.md 12 pairs (a block over the character, a translucent selection over the text, underline and bar in row 0). xterm now gets a transparent cursor and selection, and one painter of the view, TerminalMarksPainter, paints every marked cell opaque in its token pairing, snapped to whole device pixels: the cursor cell in cursor with its character in cursor-text, a selected cell in selection-bg with its character in fg, without the focus a frame in cursor. Proof: a pixel test over light and dark, device pixel ratios 1, 1.25 and 2, three cursor cells, focus on and off and three kinds of cell checks the cell (nothing painted elsewhere), the pairing and the contrast, and a selection test checks the selection in both themes and all three ratios. Supersedes D-M4-35-9. |
components.md 12 (block cursor with cursor-text, selection selection-bg); 01 accessibility (AA in light and dark). |
| D-M4-35-19 |
2026-10-20 |
Red team of the class fixes (NEIN, round 3), fixed with one fix round under the owner rule: a step of a flow starts only while the flow owns the tab and is checked again whether it answers or fails (_owned takes the step as a function), so a late step_up_required after End, a revocation or a closed tab asks for no step-up, and a closed tab during the opening asks for none either (the model test now also counts step-ups after the action and runs the attach with step_up_required); text that the soft keyboard inserts at once with a line break or a control character goes through the same paste guard as the clipboard (the terminal model routes such a text input to the view; single characters and plain text go straight on); the cursor and selection painter skips rows outside the view; shouldRepaint keeps the inherited parameter name. |
Red team report of M4-35 (round 3); D-M4-35-17, D-M4-35-18. |
| D-M4-35-20 |
2026-10-22 |
Landing on main: the ticket's own commits (after the merge of M2-44) are brought onto current main as one change. Dropped because main already has them: the roles color.terminal.*, their pairings in design/design-rules.json, docs/design/contrast.md and section 12 of docs/design/components.md, and the regenerated REST objects (M2-44 landed with them). Links follow the English chapter names of main (15-client-architecture.md, 01-product.md, 08-identity.md, ADR 0070); the root changelog entry and these rulings are fragments (PL/24); the spec notes of 15 section 23.4 and 24 section 12 are added to the English chapters. |
Rules for landing a ticket built on an integration branch; nothing of the ticket's behavior changes. |
| D-M4-35-21 |
2026-10-22 |
Review of the landing pull request: (1) a long input is queued as all its data frames at once, so a key or a renderer reply sent while they are on their way follows the whole input; (2) a failed TerminalTransport.identify (the access token or the DPoP proof could not be made) detaches the tab with DetachCause.authenticationFailed and "Reattach" instead of blocking it as tampering, and a failed HostKeyPinStore.write leaves the checked session connected (the next opening is a first contact again); only Noise and frame errors give the hard warning. A cursor one past the last column needs no clamp: Buffer.cursorX of xterm 4.0.0 already clamps to the last column. |
The order of the frames of one stream is the order of the input (24 5.4); a local failure is no sign of tampering (24 5.3) and must not hide "Reattach". Tests: input_order_test.dart, local_failure_test.dart. |