@oneciel-ai/ciel-runtime 0.2.39 → 0.2.41

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 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.41 — 2026-09-07
9
+
10
+ - Automatically authenticate Codex model and Ciel MCP requests to LAN-bound
11
+ routers using a separate process-local header. Preserve native OpenAI
12
+ authorization and strip Ciel credentials before forwarding upstream.
13
+ - Reassemble fragmented Windows console VT input sequences before forwarding
14
+ them to ConPTY, preventing split paste boundaries from entering the Codex
15
+ draft as text. Preserve literal text and standalone Escape (100ms idle bound).
16
+
17
+ ## 0.2.40 — 2026-09-07
18
+
19
+ - Authenticate internal router health checks when the web backend uses a
20
+ specific LAN address. Preserve the external listener and authentication
21
+ requirements instead of reporting an authenticated running server as down.
22
+
8
23
  ## 0.2.39 — 2026-09-07
9
24
 
10
25
  - Fix native Codex startup failing with `invalid transport` when DuckDuckGo or
package/ciel_runtime.py CHANGED
@@ -3024,6 +3024,7 @@ def router_health() -> dict[str, Any] | None:
3024
3024
  try:
3025
3025
  data = http_json(
3026
3026
  f"{ROUTER_BASE}/health",
3027
+ headers=_ROUTER_ACCESS_POLICY.health_headers(ROUTER_BASE, load_config(), router_external_access_token),
3027
3028
  timeout=router_health_timeout_seconds(),
3028
3029
  )
3029
3030
  return data if isinstance(data, dict) else None
@@ -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:]]
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import secrets
9
9
  import time
10
+ import urllib.parse
10
11
  from collections.abc import Callable, Mapping, MutableMapping
11
12
  from dataclasses import dataclass
12
13
  from pathlib import Path
@@ -63,6 +64,18 @@ class RouterAccessPolicy:
63
64
  parse_env_bool: Callable[[str | None, bool | None], bool | None]
64
65
  load_config: Callable[[], dict[str, Any]]
65
66
 
67
+ def health_headers(
68
+ self, base: str, config: dict[str, Any], token_provider: Callable[[], str],
69
+ ) -> dict[str, str]:
70
+ # Specific LAN binds are also used as the internal client address;
71
+ # those requests must satisfy external authentication just like peers.
72
+ if is_loopback_address(urllib.parse.urlparse(base).hostname):
73
+ return {}
74
+ if not self.administrative_external_access_enabled(config):
75
+ return {}
76
+ token = token_provider()
77
+ return {"Authorization": f"Bearer {token}"} if token else {}
78
+
66
79
  def remote_bridge_enabled(self, config: Mapping[str, Any]) -> bool:
67
80
  override = str(
68
81
  self.environ.get("CIEL_RUNTIME_REMOTE_BRIDGE") or ""
@@ -188,9 +201,11 @@ class RouterAccessPolicy:
188
201
  and hmac.compare_digest(expected, bridge_expected)
189
202
  ):
190
203
  return False
191
- return bool(
192
- expected and supplied and hmac.compare_digest(expected, supplied)
193
- )
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
+ ))
194
209
  return False
195
210
 
196
211
 
@@ -97,7 +97,7 @@ OPENCODE_ENDPOINT_ALIASES = {
97
97
  }
98
98
 
99
99
  APP_NAME = "Ciel Runtime"
100
- VERSION = "0.2.39"
100
+ VERSION = "0.2.41"
101
101
  CREDITS = "Credits: One Ciel LLC"
102
102
  PRELAUNCH_CANCEL = 10
103
103
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -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,78 @@
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 CSI to a child's keyboard-event 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
+ # CSI/SS3 sequences end with a final byte; other ESC keys
36
+ # (including Alt+key) are complete after the next byte.
37
+ complete = (
38
+ len(self.pending) == 2 and byte not in (ord('['), ord('O'))
39
+ ) or (
40
+ len(self.pending) >= 3 and 0x40 <= byte <= 0x7e
41
+ ) or len(self.pending) >= 64
42
+ if complete:
43
+ ready.extend(self.pending)
44
+ self.pending.clear()
45
+ if ready:
46
+ self.write(bytes(ready))
47
+ if self.pending:
48
+ generation = self.generation
49
+ self.timer = threading.Timer(self.idle_seconds, self._expire, args=(generation,))
50
+ self.timer.daemon = True
51
+ self.timer.start()
52
+
53
+ def _cancel(self) -> None:
54
+ self.generation += 1
55
+ if self.timer is not None:
56
+ self.timer.cancel()
57
+ self.timer = None
58
+
59
+ def _expire(self, generation: int) -> None:
60
+ with self.lock:
61
+ if generation != self.generation:
62
+ return
63
+ self.timer = None
64
+ self._flush()
65
+
66
+ def _flush(self) -> None:
67
+ if self.pending:
68
+ data = bytes(self.pending)
69
+ self.pending.clear()
70
+ try:
71
+ self.write(data)
72
+ except OSError:
73
+ pass # Child may have exited while Escape was pending.
74
+
75
+ def close(self) -> None:
76
+ with self.lock:
77
+ self._cancel()
78
+ 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
- while not self._stop.is_set():
984
- try:
985
- data = self._read_input_bytes()
986
- if not data:
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
- self._observe_parent_input(data)
989
- self.write(data)
990
- except OSError:
991
- return
995
+ finally:
996
+ frames.close()
992
997
 
993
998
 
994
999
  __all__ = ["WindowsConPtySession", "conpty_enabled"]
@@ -0,0 +1,18 @@
1
+ title: Remote Windows clipboard paste exposes delimiter-like text
2
+ date: 2026-09-07
3
+ status: investigated; remote cause not established
4
+ observation: User screenshot shows [20~ before a pasted URL and [201~ after it.
5
+ interpretation: Consistent with exposed bracketed-paste delimiters; not evidence of mouse reporting.
6
+ reference: https://github.com/microsoft/terminal/issues/12385
7
+ protocol: Bracketed paste wraps text in ESC[200~ and ESC[201~.
8
+ source_checks:
9
+ parent_input: windows_conpty._read_input_bytes uses ReadConsoleW then UTF-8 encoding.
10
+ pump: _pump_input observes draft state then writes input bytes without textual marker replacement.
11
+ transport: write loops until all bytes have been written to ConPTY.
12
+ verification:
13
+ command: python -m unittest discover -s tests -p test_windows_conpty.py -v
14
+ result: 33 tests passed in 0.361s on local Windows.
15
+ coverage: Includes real ConPTY bracketed-paste/large Korean payload byte integrity and submission tests.
16
+ limitation: Does not reproduce the remote user's clipboard, outer terminal, or Codex TUI input path.
17
+ next_evidence: Remote machine access or exact terminal/runtime versions and relevant transport startup logs.
18
+ changes: No runtime code changes or deployment; do not strip literal marker-like text from user messages.
@@ -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,12 @@
1
+ title: Router startup timeout after remote Windows web backend configuration
2
+ date: 2026-09-07
3
+ status: conditional configuration bug reproduced; remote root cause unconfirmed
4
+ evidence:
5
+ traceback: router_process_lifecycle.start_router_if_needed exhausted 30-second router_up polling.
6
+ remote_initial: http://10.0.0.238:9683/health returned 401 external authentication required.
7
+ remote_later: Connection timed out after 3015 milliseconds.
8
+ source: WebBackendSettings.client_host preserves specific LAN addresses; router_health sends no authentication; RouterAccessPolicy permits unauthenticated loopback only.
9
+ local_policy_reproduction: client_host=10.0.0.238; LAN health without token allowed=False; loopback health allowed=True.
10
+ limitations: No remote router.log or configured bind host available; startup process crash and bind errors remain possible.
11
+ changes: No runtime configuration or code modified.
12
+ required_evidence: Remote router.log or authenticated backend access after service recovery.
@@ -0,0 +1,19 @@
1
+ title: Authenticate LAN-bound managed router health checks
2
+ date: 2026-09-07
3
+ version: 0.2.40
4
+ authorization: User explicitly requested fix and new-version main deployment.
5
+ evidence:
6
+ remote_log: Listener and client base both use 10.0.0.238:9683.
7
+ observed_http: Earlier external health request returned 401.
8
+ source: router_health omitted authentication; non-loopback requests require authentication.
9
+ change:
10
+ health: Read existing administrative token for non-loopback health checks when administrative external access is enabled.
11
+ preserved: Bind address, port, global/workspace configuration, external authentication rules, active sessions.
12
+ secrets: No token generation or token logging in the health probe.
13
+ verification:
14
+ real_http: Isolated HTTP server with production access policy: missing token fails, correct token makes router_up true, wrong token fails.
15
+ limitations: Test socket is loopback with simulated LAN peer policy; remote user's final restart not yet verified.
16
+ targeted: 2 tests passed in 0.513s.
17
+ runtime: 280 tests passed with 16 skips.
18
+ static: ruff and py_compile passed.
19
+ deployment: main push triggers full-test npm latest publishing; registry and downloaded artifact checked after publishing.
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "Universal AI coding-agent runtime and model-routing layer for Claude, Codex, AGY, ZCode, and compatible runtimes.",
5
5
  "license": "MIT",
6
6
  "author": "One Ciel LLC",