@oneciel-ai/ciel-runtime 0.2.40 → 0.2.42
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 +15 -0
- package/ciel_runtime_support/codex_router_auth.py +88 -0
- package/ciel_runtime_support/router_access.py +5 -3
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/runtime_launch.py +3 -0
- package/ciel_runtime_support/terminal_input_frames.py +89 -0
- package/ciel_runtime_support/windows_conpty.py +13 -8
- package/docs/journal/2026/09/07/diagnostics/windows/remote-backend-log-access.okf +14 -0
- package/docs/journal/2026/09/07/diagnostics/windows/remote-console-capture.okf +36 -0
- package/docs/journal/2026/09/07/release/stable/0.2.41.okf +26 -0
- package/docs/journal/2026/09/08/release/stable/0.2.42.okf +18 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ capability, followed by the complete commit ledger merged into each release.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.2.42 — 2026-09-08
|
|
9
|
+
|
|
10
|
+
- Preserve fragmented OSC palette responses and other VT control strings as
|
|
11
|
+
complete frames in the Windows input bridge. Prevent RGB palette replies
|
|
12
|
+
from appearing in the Codex draft; retain literal text and bounded buffering.
|
|
13
|
+
|
|
14
|
+
## 0.2.41 — 2026-09-07
|
|
15
|
+
|
|
16
|
+
- Automatically authenticate Codex model and Ciel MCP requests to LAN-bound
|
|
17
|
+
routers using a separate process-local header. Preserve native OpenAI
|
|
18
|
+
authorization and strip Ciel credentials before forwarding upstream.
|
|
19
|
+
- Reassemble fragmented Windows console VT input sequences before forwarding
|
|
20
|
+
them to ConPTY, preventing split paste boundaries from entering the Codex
|
|
21
|
+
draft as text. Preserve literal text and standalone Escape (100ms idle bound).
|
|
22
|
+
|
|
8
23
|
## 0.2.40 — 2026-09-07
|
|
9
24
|
|
|
10
25
|
- Authenticate internal router health checks when the web backend uses a
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Attach router authentication without replacing native OpenAI credentials."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
from .router_access import is_loopback_address
|
|
8
|
+
|
|
9
|
+
CLIENT_TOKEN_ENV = "CIEL_RUNTIME_ROUTER_CLIENT_TOKEN"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def authenticated_codex_command(cmd: list[str], env: dict[str, str], router_base: str) -> list[str]:
|
|
13
|
+
router = urlparse(router_base)
|
|
14
|
+
targets = []
|
|
15
|
+
builtin_base = None
|
|
16
|
+
overrides = {}
|
|
17
|
+
index = 1
|
|
18
|
+
while index < len(cmd):
|
|
19
|
+
argument = cmd[index]
|
|
20
|
+
index += 1
|
|
21
|
+
if argument == '--':
|
|
22
|
+
break
|
|
23
|
+
if argument in ('-c', '--config') and index < len(cmd):
|
|
24
|
+
setting = cmd[index]
|
|
25
|
+
index += 1
|
|
26
|
+
elif argument.startswith('--config='):
|
|
27
|
+
setting = argument.split('=', 1)[1]
|
|
28
|
+
else:
|
|
29
|
+
continue
|
|
30
|
+
key, separator, raw = setting.partition('=')
|
|
31
|
+
if separator:
|
|
32
|
+
key = key.strip()
|
|
33
|
+
# Do not attach credentials based on an overridden earlier URL.
|
|
34
|
+
overrides = {old: value for old, value in overrides.items()
|
|
35
|
+
if not old.startswith(key + '.')}
|
|
36
|
+
overrides[key] = raw
|
|
37
|
+
for key, raw in overrides.items():
|
|
38
|
+
if not raw:
|
|
39
|
+
continue
|
|
40
|
+
match = re.fullmatch(r"model_providers\.([\w-]+)\.base_url", key)
|
|
41
|
+
if match:
|
|
42
|
+
prefix = f"model_providers.{match.group(1)}"
|
|
43
|
+
elif key == "openai_base_url":
|
|
44
|
+
prefix = "model_providers.ciel-runtime-native-auth"
|
|
45
|
+
elif key == "mcp_servers.ciel-runtime-router.url":
|
|
46
|
+
prefix = "mcp_servers.ciel-runtime-router"
|
|
47
|
+
else:
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
url = urlparse(json.loads(raw))
|
|
51
|
+
except (ValueError, TypeError):
|
|
52
|
+
continue
|
|
53
|
+
# These are Ciel's generated local-router routes, not arbitrary
|
|
54
|
+
# upstream provider URLs. Only authorize this launch's exact origin.
|
|
55
|
+
if (not is_loopback_address(url.hostname) and url.scheme == router.scheme
|
|
56
|
+
and url.netloc == router.netloc
|
|
57
|
+
and url.path.rstrip('/') in ('/v1', '/backend-api/codex', '/ca/mcp')):
|
|
58
|
+
targets.append(prefix)
|
|
59
|
+
if key == 'openai_base_url':
|
|
60
|
+
builtin_base = raw
|
|
61
|
+
if not targets:
|
|
62
|
+
return cmd
|
|
63
|
+
token = str(env.get("CIEL_RUNTIME_ROUTER_EXTERNAL_TOKEN") or "").strip()
|
|
64
|
+
if not token:
|
|
65
|
+
state = env.get("CIEL_RUNTIME_STATE_DIR")
|
|
66
|
+
if state:
|
|
67
|
+
try:
|
|
68
|
+
token = (Path(state) / "router-external-token").read_text(encoding="utf-8").strip()
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
if not token:
|
|
72
|
+
return cmd
|
|
73
|
+
env[CLIENT_TOKEN_ENV] = token
|
|
74
|
+
options = []
|
|
75
|
+
if builtin_base is not None:
|
|
76
|
+
# Modern Codex forbids overriding the built-in `openai` table.
|
|
77
|
+
# Use a per-launch provider retaining native OpenAI authentication.
|
|
78
|
+
provider = 'ciel-runtime-native-auth'
|
|
79
|
+
cmd = [f'model_provider="{provider}"' if item == 'model_provider="openai"' else item for item in cmd]
|
|
80
|
+
for setting in (f'model_provider="{provider}"',
|
|
81
|
+
f'model_providers.{provider}.name="Ciel Runtime Codex"',
|
|
82
|
+
f'model_providers.{provider}.base_url={builtin_base}',
|
|
83
|
+
f'model_providers.{provider}.wire_api="responses"',
|
|
84
|
+
f'model_providers.{provider}.requires_openai_auth=true'):
|
|
85
|
+
options.extend(['-c', setting])
|
|
86
|
+
for prefix in dict.fromkeys(targets):
|
|
87
|
+
options.extend(['-c', f'{prefix}.env_http_headers.x-ciel-runtime-token="{CLIENT_TOKEN_ENV}"'])
|
|
88
|
+
return [cmd[0], *options, *cmd[1:]]
|
|
@@ -201,9 +201,11 @@ class RouterAccessPolicy:
|
|
|
201
201
|
and hmac.compare_digest(expected, bridge_expected)
|
|
202
202
|
):
|
|
203
203
|
return False
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
local_token = str(handler.headers.get("x-ciel-runtime-token") or "").strip()
|
|
205
|
+
return bool(expected and (
|
|
206
|
+
(supplied and hmac.compare_digest(expected, supplied))
|
|
207
|
+
or (local_token and hmac.compare_digest(expected, local_token))
|
|
208
|
+
))
|
|
207
209
|
return False
|
|
208
210
|
|
|
209
211
|
|
|
@@ -9,6 +9,7 @@ from dataclasses import dataclass
|
|
|
9
9
|
|
|
10
10
|
from ciel_runtime_support.claude_environment import CLAUDE_PROJECTED_ENV_KEYS
|
|
11
11
|
from ciel_runtime_support.managed_tool_injection import should_inject_tool, codex_native_web_tool_overrides
|
|
12
|
+
from ciel_runtime_support.codex_router_auth import authenticated_codex_command
|
|
12
13
|
from ciel_runtime_support.runtime_constants import (
|
|
13
14
|
CLAUDE_SERVER_SIDE_WEB_TOOLS,
|
|
14
15
|
CODEX_RUNTIME_API_KEY_ENV,
|
|
@@ -973,6 +974,7 @@ def run_codex(
|
|
|
973
974
|
if workspace_mcp is not None and workspace_mcp_launch is not None:
|
|
974
975
|
workspace_mcp.finish(workspace_mcp_launch)
|
|
975
976
|
raise
|
|
977
|
+
cmd = authenticated_codex_command(cmd, env, ROUTER_BASE)
|
|
976
978
|
_log_codex_command_for_diagnostics(cmd, env)
|
|
977
979
|
record_launch_state_for_cwd(
|
|
978
980
|
current_launch_cwd_key(),
|
|
@@ -1287,6 +1289,7 @@ def run_codex_app_server(
|
|
|
1287
1289
|
print(f"Codex App Server listen: {cmd[cmd.index('--listen') + 1]}", flush=True)
|
|
1288
1290
|
except Exception:
|
|
1289
1291
|
pass
|
|
1292
|
+
cmd = authenticated_codex_command(cmd, env, ROUTER_BASE)
|
|
1290
1293
|
_log_codex_app_server_command_for_diagnostics(cmd, env)
|
|
1291
1294
|
record_launch_state_for_cwd(
|
|
1292
1295
|
current_launch_cwd_key(),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Reassemble short VT input sequences across parent-console reads."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TerminalInputFrames:
|
|
9
|
+
"""Do not expose a partial VT sequence to a child's keyboard parser.
|
|
10
|
+
|
|
11
|
+
A bounded idle timer preserves a standalone Escape key. Nothing is
|
|
12
|
+
stripped or rewritten, including literal text resembling paste markers.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, write: Callable[[bytes], None], *, idle_seconds: float = 0.1):
|
|
16
|
+
self.write = write
|
|
17
|
+
self.idle_seconds = idle_seconds
|
|
18
|
+
self.pending = bytearray()
|
|
19
|
+
self.lock = threading.Lock()
|
|
20
|
+
self.timer: threading.Timer | None = None
|
|
21
|
+
self.generation = 0
|
|
22
|
+
|
|
23
|
+
def feed(self, data: bytes) -> None:
|
|
24
|
+
with self.lock:
|
|
25
|
+
self._cancel()
|
|
26
|
+
ready = bytearray()
|
|
27
|
+
for byte in data:
|
|
28
|
+
if not self.pending:
|
|
29
|
+
if byte == 0x1b:
|
|
30
|
+
self.pending.append(byte)
|
|
31
|
+
else:
|
|
32
|
+
ready.append(byte)
|
|
33
|
+
continue
|
|
34
|
+
self.pending.append(byte)
|
|
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
|
|
53
|
+
if complete:
|
|
54
|
+
ready.extend(self.pending)
|
|
55
|
+
self.pending.clear()
|
|
56
|
+
if ready:
|
|
57
|
+
self.write(bytes(ready))
|
|
58
|
+
if self.pending:
|
|
59
|
+
generation = self.generation
|
|
60
|
+
self.timer = threading.Timer(self.idle_seconds, self._expire, args=(generation,))
|
|
61
|
+
self.timer.daemon = True
|
|
62
|
+
self.timer.start()
|
|
63
|
+
|
|
64
|
+
def _cancel(self) -> None:
|
|
65
|
+
self.generation += 1
|
|
66
|
+
if self.timer is not None:
|
|
67
|
+
self.timer.cancel()
|
|
68
|
+
self.timer = None
|
|
69
|
+
|
|
70
|
+
def _expire(self, generation: int) -> None:
|
|
71
|
+
with self.lock:
|
|
72
|
+
if generation != self.generation:
|
|
73
|
+
return
|
|
74
|
+
self.timer = None
|
|
75
|
+
self._flush()
|
|
76
|
+
|
|
77
|
+
def _flush(self) -> None:
|
|
78
|
+
if self.pending:
|
|
79
|
+
data = bytes(self.pending)
|
|
80
|
+
self.pending.clear()
|
|
81
|
+
try:
|
|
82
|
+
self.write(data)
|
|
83
|
+
except OSError:
|
|
84
|
+
pass # Child may have exited while Escape was pending.
|
|
85
|
+
|
|
86
|
+
def close(self) -> None:
|
|
87
|
+
with self.lock:
|
|
88
|
+
self._cancel()
|
|
89
|
+
self._flush()
|
|
@@ -13,6 +13,7 @@ from collections.abc import Mapping
|
|
|
13
13
|
from typing import Any, Callable
|
|
14
14
|
|
|
15
15
|
from .terminal_platform_io import TERMINAL_INPUT_MODE_RESET
|
|
16
|
+
from .terminal_input_frames import TerminalInputFrames
|
|
16
17
|
from .windows_command_line import command_line_for_create_process
|
|
17
18
|
|
|
18
19
|
|
|
@@ -980,15 +981,19 @@ class WindowsConPtySession:
|
|
|
980
981
|
self._parent_input_draft = draft
|
|
981
982
|
|
|
982
983
|
def _pump_input(self) -> None:
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
984
|
+
frames = TerminalInputFrames(self.write)
|
|
985
|
+
try:
|
|
986
|
+
while not self._stop.is_set():
|
|
987
|
+
try:
|
|
988
|
+
data = self._read_input_bytes()
|
|
989
|
+
if not data:
|
|
990
|
+
return
|
|
991
|
+
self._observe_parent_input(data)
|
|
992
|
+
frames.feed(data)
|
|
993
|
+
except OSError:
|
|
987
994
|
return
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
except OSError:
|
|
991
|
-
return
|
|
995
|
+
finally:
|
|
996
|
+
frames.close()
|
|
992
997
|
|
|
993
998
|
|
|
994
999
|
__all__ = ["WindowsConPtySession", "conpty_enabled"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
title: Attempt remote web backend log capture for Windows paste-marker issue
|
|
2
|
+
date: 2026-09-07
|
|
3
|
+
target: http://10.0.0.238:9683
|
|
4
|
+
authorization: User requested backend log capture and console/mouse diagnostics.
|
|
5
|
+
request: GET /health without credentials
|
|
6
|
+
response:
|
|
7
|
+
timestamp: Mon, 07 Sep 2026 18:02:22 GMT
|
|
8
|
+
status: HTTP/1.0 401 Unauthorized
|
|
9
|
+
challenge: Bearer realm="ciel-runtime"
|
|
10
|
+
message: ciel-runtime router external authentication is required.
|
|
11
|
+
conclusion: Backend reachable; administrative authentication required before logs or TUI diagnostics can be retrieved.
|
|
12
|
+
limitation: Remote runtime version, logs, and console state not retrieved; no assertion that paste issue is diagnosed.
|
|
13
|
+
next_requirement: User-provided administrative external access token for this remote backend.
|
|
14
|
+
changes: No remote writes, input injection, configuration changes, or authentication bypass attempted.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
title: Authenticated remote backend console investigation
|
|
2
|
+
date: 2026-09-07
|
|
3
|
+
target: http://10.0.0.238:9683
|
|
4
|
+
credentials: Omitted; user-provided token used only for authenticated requests.
|
|
5
|
+
captures:
|
|
6
|
+
health:
|
|
7
|
+
ok: true
|
|
8
|
+
version: 0.2.40
|
|
9
|
+
source_fingerprint: 3bc950932f5d1691
|
|
10
|
+
pid: 21824
|
|
11
|
+
workspace: c:\users\1347817
|
|
12
|
+
router_port: 9683
|
|
13
|
+
instance_id: 9683-a2f58d1342e0
|
|
14
|
+
active_client_count: 1
|
|
15
|
+
active_client_pids: [29116]
|
|
16
|
+
provider: codex
|
|
17
|
+
chat_health:
|
|
18
|
+
ok: true
|
|
19
|
+
base: http://10.0.0.238:9683
|
|
20
|
+
tui_status:
|
|
21
|
+
ok: true
|
|
22
|
+
enabled: true
|
|
23
|
+
active_count: 0
|
|
24
|
+
latest_event_id: 0
|
|
25
|
+
capture_scope: routed runtime traffic
|
|
26
|
+
excluded: hidden thinking, tool arguments, native traffic that bypasses this router, terminal pixels
|
|
27
|
+
tui_recent: '{"ok":true,"events":[],"last_id":0,"active":[]}'
|
|
28
|
+
events_recent: '{"ok":true,"events":[]}'
|
|
29
|
+
source_audit:
|
|
30
|
+
router_http: Authenticated GET dispatches TUI/events/config/web/chat/runtime APIs; no raw router.log download handler found.
|
|
31
|
+
tui: Observation API explicitly excludes terminal pixels and native bypass traffic.
|
|
32
|
+
conclusion:
|
|
33
|
+
confirmed: Remote 0.2.40 is running and reachable on LAN with authentication; one active client recorded.
|
|
34
|
+
not_confirmed: Clipboard delimiter corruption location, actual ConPTY/fallback selection, mouse state, live terminal input bytes.
|
|
35
|
+
blocker: Existing backend does not expose the raw log and console input/output evidence required for this native-terminal issue.
|
|
36
|
+
remote_changes: None; no input injection, model request, log-level change, or session restart.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
title: Codex LAN client authentication and Windows paste framing
|
|
2
|
+
date: 2026-09-07
|
|
3
|
+
version: 0.2.41
|
|
4
|
+
authorization: User requested handling paste corruption and internal Codex router authentication failure; continuing requested main release workflow.
|
|
5
|
+
causes:
|
|
6
|
+
auth: Previous patch authenticated health only; native model Authorization cannot double as Ciel administrative token.
|
|
7
|
+
paste_reproduction: Real Windows Codex 0.153.4 rendered [200~1660377[201~ when ESC and remaining delimiter bytes were written 20ms apart.
|
|
8
|
+
changes:
|
|
9
|
+
auth: Model and builtin Ciel MCP receive x-ciel-runtime-token from process environment, loaded from this instance token repository; no user TOML edits.
|
|
10
|
+
scope: Exact router origin only; later URL overrides excluded; native OpenAI Authorization retained.
|
|
11
|
+
upstream: Existing x-ciel-runtime header exclusion verified; router credential not forwarded to OpenAI.
|
|
12
|
+
builtin: Reserved openai provider uses a per-launch custom provider retaining requires_openai_auth, verified with actual Codex config loader.
|
|
13
|
+
paste: Frame CSI/SS3 boundaries across parent reads without stripping or rewriting payload; single Escape flushed after 100ms idle.
|
|
14
|
+
verification:
|
|
15
|
+
actual_codex_paste: Replayed split input through corrected pump; 1660377 visible, neither delimiter visible; no prompt submitted.
|
|
16
|
+
actual_codex_http: Local test server received Ciel token on MCP and model requests; native Authorization also present on model requests. Server intentionally returns 400 without inference or forwarding.
|
|
17
|
+
auth_tests: 5 passed, including real CLI and overridden destination protection.
|
|
18
|
+
conpty: 33 passed.
|
|
19
|
+
framing: 3 passed, covering every split, Korean, literal marker text, standalone Escape, arrows and F9.
|
|
20
|
+
runtime_group: 285 passed with 12 skips before additional URL-override test.
|
|
21
|
+
router_debug: 25 passed.
|
|
22
|
+
static: Ruff and architecture budget passed.
|
|
23
|
+
limitations:
|
|
24
|
+
remote: Original mouse gesture on remote machine not directly replayed; reproduced matching corruption with fragmented native Codex input locally.
|
|
25
|
+
idle: Incomplete sequences exceeding 100ms idle are flushed unchanged to preserve standalone Escape.
|
|
26
|
+
deployment: Main workflow and published package verification follow commit.
|
|
@@ -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.
|
package/package.json
CHANGED