@oneciel-ai/ciel-runtime 0.2.42 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/windows_conpty.py +22 -16
- package/ciel_runtime_support/windows_terminal_modes.py +67 -0
- package/docs/journal/2026/09/08/windows/terminal/input-mode-leak.okf +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ capability, followed by the complete commit ledger merged into each release.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.2.43 — 2026-09-08
|
|
9
|
+
|
|
10
|
+
- Isolate child ConPTY Win32-input and mouse-reporting modes from the parent
|
|
11
|
+
Windows terminal. Preserve display controls, bracketed paste and Unicode.
|
|
12
|
+
- Explicitly reset Win32-input mode on startup/shutdown and restore the parent
|
|
13
|
+
console even if closing the pseudo-console fails.
|
|
14
|
+
|
|
8
15
|
## 0.2.42 — 2026-09-08
|
|
9
16
|
|
|
10
17
|
- Preserve fragmented OSC palette responses and other VT control strings as
|
|
@@ -12,7 +12,10 @@ import time
|
|
|
12
12
|
from collections.abc import Mapping
|
|
13
13
|
from typing import Any, Callable
|
|
14
14
|
|
|
15
|
-
from .
|
|
15
|
+
from .windows_terminal_modes import (
|
|
16
|
+
WINDOWS_TERMINAL_INPUT_MODE_RESET,
|
|
17
|
+
WindowsTerminalModeFilter,
|
|
18
|
+
)
|
|
16
19
|
from .terminal_input_frames import TerminalInputFrames
|
|
17
20
|
from .windows_command_line import command_line_for_create_process
|
|
18
21
|
|
|
@@ -435,19 +438,18 @@ class WindowsConPtySession:
|
|
|
435
438
|
self._input_handle = None
|
|
436
439
|
# Keep the output reader alive while ClosePseudoConsole asks its host
|
|
437
440
|
# to flush and exit. Closing the output pipe first can deadlock conhost.
|
|
438
|
-
|
|
439
|
-
self.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
self._output_thread
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
self._reset_and_restore_parent_console()
|
|
441
|
+
try:
|
|
442
|
+
if self._hpc and self._kernel32:
|
|
443
|
+
self._kernel32.ClosePseudoConsole(self._hpc)
|
|
444
|
+
self._hpc = None
|
|
445
|
+
if self._output_thread is not None:
|
|
446
|
+
self._output_thread.join(timeout=1.0)
|
|
447
|
+
finally:
|
|
448
|
+
self._stop.set()
|
|
449
|
+
# Win32 SetConsoleMode cannot clear emulator-owned DEC state.
|
|
450
|
+
# Reset even when closing conhost fails, then prevent late output
|
|
451
|
+
# from re-enabling modes after the parent console is restored.
|
|
452
|
+
self._reset_and_restore_parent_console()
|
|
451
453
|
if self._output_handle and self._kernel32:
|
|
452
454
|
try:
|
|
453
455
|
self._kernel32.CloseHandle(self._output_handle)
|
|
@@ -459,12 +461,12 @@ class WindowsConPtySession:
|
|
|
459
461
|
self._process_handle = None
|
|
460
462
|
|
|
461
463
|
def _reset_parent_terminal_modes(self) -> None:
|
|
462
|
-
self._write_parent_terminal_modes(
|
|
464
|
+
self._write_parent_terminal_modes(WINDOWS_TERMINAL_INPUT_MODE_RESET)
|
|
463
465
|
|
|
464
466
|
def _reset_and_restore_parent_console(self) -> None:
|
|
465
467
|
with self._parent_output_lock():
|
|
466
468
|
self._write_parent_terminal_modes_unlocked(
|
|
467
|
-
|
|
469
|
+
WINDOWS_TERMINAL_INPUT_MODE_RESET
|
|
468
470
|
)
|
|
469
471
|
self._mirror_output = False
|
|
470
472
|
self._restore_parent_console()
|
|
@@ -848,6 +850,10 @@ class WindowsConPtySession:
|
|
|
848
850
|
with self._parent_output_lock():
|
|
849
851
|
if not self._mirror_output:
|
|
850
852
|
return
|
|
853
|
+
mode_filter = getattr(self, "_parent_mode_filter", None)
|
|
854
|
+
if mode_filter is None:
|
|
855
|
+
mode_filter = self._parent_mode_filter = WindowsTerminalModeFilter()
|
|
856
|
+
data = mode_filter.feed(data, final=final)
|
|
851
857
|
if self._stdout_console_handle is not None and self._output_decoder is not None:
|
|
852
858
|
text = self._output_decoder.decode(data, final=final)
|
|
853
859
|
if text:
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Keep child ConPTY input protocols from taking over the parent terminal."""
|
|
2
|
+
|
|
3
|
+
from .terminal_platform_io import TERMINAL_INPUT_MODE_RESET
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
WINDOWS_TERMINAL_INPUT_MODE_RESET = TERMINAL_INPUT_MODE_RESET + "\x1b[?9001l"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WindowsTerminalModeFilter:
|
|
10
|
+
"""Filter DECSET at the output boundary, not user text at the input boundary.
|
|
11
|
+
|
|
12
|
+
Our parent reader consumes ordinary VT input, not Win32-input-mode records.
|
|
13
|
+
Mouse reporting is also intentionally disabled on Windows. Display modes,
|
|
14
|
+
focus events and bracketed paste are preserved. State spans pipe reads;
|
|
15
|
+
control strings are streamed without buffering their potentially large bodies.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_blocked = {9, 1000, 1001, 1002, 1003, 1005, 1006, 1015, 1016, 9001}
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._pending = bytearray()
|
|
22
|
+
self._string = 0
|
|
23
|
+
self._string_escape = False
|
|
24
|
+
|
|
25
|
+
def feed(self, data: bytes, *, final: bool = False) -> bytes:
|
|
26
|
+
output = bytearray()
|
|
27
|
+
for value in data:
|
|
28
|
+
if self._string:
|
|
29
|
+
output.append(value)
|
|
30
|
+
if (self._string_escape and value == 92) or value in (24, 26) or (
|
|
31
|
+
self._string == 93 and value == 7
|
|
32
|
+
):
|
|
33
|
+
self._string = 0
|
|
34
|
+
self._string_escape = value == 27
|
|
35
|
+
continue
|
|
36
|
+
if value == 27:
|
|
37
|
+
output.extend(self._pending)
|
|
38
|
+
self._pending[:] = b"\x1b"
|
|
39
|
+
continue
|
|
40
|
+
if not self._pending:
|
|
41
|
+
output.append(value)
|
|
42
|
+
continue
|
|
43
|
+
self._pending.append(value)
|
|
44
|
+
if len(self._pending) == 2:
|
|
45
|
+
if value in b"]PX^_":
|
|
46
|
+
self._string = value
|
|
47
|
+
self._string_escape = False
|
|
48
|
+
if value == 91:
|
|
49
|
+
continue
|
|
50
|
+
elif not (64 <= value <= 126) and value not in (24, 26):
|
|
51
|
+
if len(self._pending) < 256:
|
|
52
|
+
continue
|
|
53
|
+
output.extend(self._filter_csi(bytes(self._pending)))
|
|
54
|
+
self._pending.clear()
|
|
55
|
+
if final:
|
|
56
|
+
output.extend(self._pending)
|
|
57
|
+
self._pending.clear()
|
|
58
|
+
return bytes(output)
|
|
59
|
+
|
|
60
|
+
def _filter_csi(self, sequence: bytes) -> bytes:
|
|
61
|
+
if not sequence.startswith(b"\x1b[?") or not sequence.endswith(b"h"):
|
|
62
|
+
return sequence
|
|
63
|
+
parameters = sequence[3:-1].split(b";")
|
|
64
|
+
if not all(part.isdigit() for part in parameters):
|
|
65
|
+
return sequence
|
|
66
|
+
kept = [part for part in parameters if int(part) not in self._blocked]
|
|
67
|
+
return b"\x1b[?" + b";".join(kept) + b"h" if kept else b""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Windows parent-terminal input protocol isolation
|
|
2
|
+
|
|
3
|
+
## Evidence
|
|
4
|
+
- User screenshot: after Codex exits, cmd receives ESC[9;15;9;0;32;1_ and similar records.
|
|
5
|
+
- Microsoft protocol/issues: https://github.com/microsoft/terminal/issues/19576
|
|
6
|
+
- Double encoded mouse reproduction: https://github.com/microsoft/terminal/issues/17851
|
|
7
|
+
- Prior windows_conpty._mirror_bytes forwarded child DECSET without filtering.
|
|
8
|
+
- Prior TERMINAL_INPUT_MODE_RESET omitted DEC private mode 9001.
|
|
9
|
+
- These establish an output-boundary/cleanup defect, not the remote process crash cause.
|
|
10
|
+
|
|
11
|
+
## Changes
|
|
12
|
+
- New streaming WindowsTerminalModeFilter blocks child enabling Win32 input mode9001
|
|
13
|
+
and mouse reporting on the parent terminal; keeps display, paste, focus and Unicode.
|
|
14
|
+
- Parses across read boundaries; streams control strings without accumulating bodies.
|
|
15
|
+
- Parent startup/shutdown reset now includes 9001l; close failure still restores console.
|
|
16
|
+
- User input is not pattern stripped, rewritten, or redirected to router transport.
|
|
17
|
+
|
|
18
|
+
## Executed verification
|
|
19
|
+
- test_windows_terminal_modes.py: 5/5 passed on native Windows.
|
|
20
|
+
- Real ConPTY child exited7, raw output contained9001h; parent mirror did not;
|
|
21
|
+
final parent output ended with9001l. Parent output captured, not a real clicked UI.
|
|
22
|
+
- test_windows_conpty.py: 33/33 passed including native transport and resize tests.
|
|
23
|
+
- test_codex_paste_frames.py: 2/2 passed using locally installed actual Codex executable.
|
|
24
|
+
Split paste and OSC palette replies did not enter the Codex prompt as marker text.
|
|
25
|
+
- test_terminal_input_frames.py: 6/6 passed. Architecture budget: 2/2 passed.
|
|
26
|
+
- Replacing the filter with prior passthrough behavior: same native child test
|
|
27
|
+
failed exactly once, confirming the regression test detects the original leak.
|
|
28
|
+
- Runtime group: 287 tests, OK (19 skipped); ruff repository check passed.
|
|
29
|
+
|
|
30
|
+
## Limits
|
|
31
|
+
- Remote Windows Terminal mouse clicks and the reported process crash have not been
|
|
32
|
+
reproduced directly. Do not report the remote crash as resolved by these tests.
|
|
33
|
+
- This journal contains no authentication tokens or unrelated session content.
|
package/package.json
CHANGED