@oneciel-ai/ciel-runtime 0.2.41 → 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 +13 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/terminal_input_frames.py +19 -8
- 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/release/stable/0.2.42.okf +18 -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,19 @@ 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
|
+
|
|
15
|
+
## 0.2.42 — 2026-09-08
|
|
16
|
+
|
|
17
|
+
- Preserve fragmented OSC palette responses and other VT control strings as
|
|
18
|
+
complete frames in the Windows input bridge. Prevent RGB palette replies
|
|
19
|
+
from appearing in the Codex draft; retain literal text and bounded buffering.
|
|
20
|
+
|
|
8
21
|
## 0.2.41 — 2026-09-07
|
|
9
22
|
|
|
10
23
|
- Automatically authenticate Codex model and Ciel MCP requests to LAN-bound
|
|
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TerminalInputFrames:
|
|
9
|
-
"""Do not expose a partial
|
|
9
|
+
"""Do not expose a partial VT sequence to a child's keyboard parser.
|
|
10
10
|
|
|
11
11
|
A bounded idle timer preserves a standalone Escape key. Nothing is
|
|
12
12
|
stripped or rewritten, including literal text resembling paste markers.
|
|
@@ -32,13 +32,24 @@ class TerminalInputFrames:
|
|
|
32
32
|
ready.append(byte)
|
|
33
33
|
continue
|
|
34
34
|
self.pending.append(byte)
|
|
35
|
-
|
|
36
|
-
# (including
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
introducer = self.pending[1]
|
|
36
|
+
# OSC (including palette replies) and other control strings
|
|
37
|
+
# end at ST, not at their first printable byte. OSC also
|
|
38
|
+
# accepts BEL. An ESC ending one read may start ST in the next.
|
|
39
|
+
if introducer in b']PX^_':
|
|
40
|
+
complete = (
|
|
41
|
+
self.pending.endswith(b'\x1b\\')
|
|
42
|
+
or (introducer == ord(']') and byte == 0x07)
|
|
43
|
+
or byte in (0x18, 0x1a) # CAN/SUB cancel the sequence.
|
|
44
|
+
or len(self.pending) >= 4096
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
# CSI/SS3 final byte, or a two-byte ESC/Alt key.
|
|
48
|
+
complete = (
|
|
49
|
+
len(self.pending) == 2 and introducer not in b'[O'
|
|
50
|
+
) or (
|
|
51
|
+
len(self.pending) >= 3 and 0x40 <= byte <= 0x7e
|
|
52
|
+
) or len(self.pending) >= 64
|
|
42
53
|
if complete:
|
|
43
54
|
ready.extend(self.pending)
|
|
44
55
|
self.pending.clear()
|
|
@@ -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,18 @@
|
|
|
1
|
+
title: Windows OSC palette response framing regression
|
|
2
|
+
date: 2026-09-08
|
|
3
|
+
version: 0.2.42
|
|
4
|
+
request: Continue handling user-reported OSC 4 palette strings exposed in console input.
|
|
5
|
+
cause:
|
|
6
|
+
source: TerminalInputFrames treated ESC ] as a complete two-byte key, forwarding the subsequent OSC body separately.
|
|
7
|
+
actual_cli_before: Codex rendered 4;0;rgb:0c0c/0c0c/0c0c followed by the test paste.
|
|
8
|
+
change:
|
|
9
|
+
protocol: Keep OSC, DCS, SOS, PM and APC strings until ST; OSC also accepts BEL; CAN/SUB cancel.
|
|
10
|
+
safety: Never remove literal text; 4096-byte bound and existing 100ms idle flush retained.
|
|
11
|
+
verification:
|
|
12
|
+
actual_cli_after: Both palette and paste tests passed; palette text absent and 1660377 still visible. No model prompt submitted.
|
|
13
|
+
frames: Six tests passed including every control-string split, BEL/ST, plain literal RGB text, bounded malformed strings, standalone Esc and paste.
|
|
14
|
+
static: Ruff passed.
|
|
15
|
+
reference: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
16
|
+
limitations: Remote user's terminal not directly manipulated; local real Codex reproduction uses simulated split parent-console reads.
|
|
17
|
+
deployment: Continue main stable release workflow; validate npm artifact after publishing.
|
|
18
|
+
excluded: No Kevin AI Net authentication changes or session restart.
|
|
@@ -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