@oneciel-ai/ciel-runtime 0.2.26 → 0.2.27

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
@@ -3,6 +3,14 @@
3
3
  This file records stable Ciel Runtime releases. Changes are grouped by user-visible
4
4
  capability, followed by the complete commit ledger merged into each release.
5
5
 
6
+ ## 0.2.27 — 2026-08-24
7
+
8
+ - Repair Codex startup only when an invalid TOML file contains equivalent MCP
9
+ `http_headers` in both a legacy child table and a managed inline table;
10
+ preserve a timestamped backup before the atomic rewrite.
11
+ - Reject a pasted Z.AI authorization-page URL with explicit callback guidance
12
+ and keep the prelaunch API-key panel alive when OAuth validation fails.
13
+
6
14
  ## 0.2.26 — 2026-08-24
7
15
 
8
16
  - Added ZCode as a first-class launch client in the interactive menu,
package/ciel_runtime.py CHANGED
@@ -121,7 +121,7 @@ from ciel_runtime_support.codex_backend_context import CodexBackendChannelPorts,
121
121
  from ciel_runtime_support.codex_reasoning_rejects import RejectedReasoningStore
122
122
  from ciel_runtime_support.codex_cli import codex_passthrough_args_for_launch, codex_passthrough_has_command, codex_resume_picker_requested, codex_resume_with_session_id
123
123
  from ciel_runtime_support.codex_config import codex_alternate_screen_value_from_config_text # noqa: F401
124
- from ciel_runtime_support.codex_config import codex_config_paths_for_launch # noqa: F401 - compatibility export
124
+ from ciel_runtime_support.codex_config import codex_config_paths_for_launch, repair_codex_mcp_header_collisions # noqa: F401 - compatibility export
125
125
  from ciel_runtime_support.codex_config import codex_config_override_keys as _codex_config_override_keys # noqa: F401
126
126
  from ciel_runtime_support.codex_config import toml_scalar_without_comment as _toml_scalar_without_comment # noqa: F401
127
127
  from ciel_runtime_support.codex_config import toml_string
@@ -4890,7 +4890,7 @@ def runtime_launch_context() -> RuntimeLaunchContext:
4890
4890
 
4891
4891
  _RUNTIME_LAUNCH_API = RuntimeLaunchCompatibilityApi(runtime_launch_context)
4892
4892
  launch_claude = SynchronizedLaunch(_RUNTIME_LAUNCH_API.launch_claude, sync_remote_launch_assets, "claude")
4893
- launch_codex = SynchronizedLaunch(_RUNTIME_LAUNCH_API.launch_codex, sync_remote_launch_assets, "codex")
4893
+ launch_codex = SynchronizedLaunch(_RUNTIME_LAUNCH_API.launch_codex, sync_remote_launch_assets, "codex", lambda passthrough=None, **_kwargs: repair_codex_mcp_header_collisions(codex_config_paths_for_launch(list(passthrough or [])), report=lambda message: router_log("WARN", message)))
4894
4894
  launch_codex_app_server = SynchronizedLaunch(_RUNTIME_LAUNCH_API.launch_codex_app_server, sync_remote_launch_assets, "codex-app-server")
4895
4895
  launch_agy = SynchronizedLaunch(_RUNTIME_LAUNCH_API.launch_agy, sync_remote_launch_assets, "agy")
4896
4896
  launch_grok = SynchronizedLaunch(launch_grok, sync_remote_launch_assets, "grok")
@@ -5,7 +5,16 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import re
8
+ import shutil
9
+ import stat
10
+ import time
8
11
  from pathlib import Path
12
+ from typing import Any, Callable, Iterable
13
+
14
+ try:
15
+ import tomllib
16
+ except ModuleNotFoundError: # Python 3.10 remains a supported launcher runtime.
17
+ tomllib = None # type: ignore[assignment]
9
18
 
10
19
 
11
20
  def toml_string(value: str) -> str:
@@ -124,3 +133,87 @@ def codex_config_paths_for_launch(
124
133
  if path not in paths:
125
134
  paths.append(path)
126
135
  return paths
136
+
137
+
138
+ _MCP_PARENT_TABLE = re.compile(
139
+ r'^\s*\[mcp_servers\.(?P<name>"(?:[^"\\]|\\.)+"|[A-Za-z0-9_-]+)\]\s*(?:#.*)?$'
140
+ )
141
+ _MCP_HEADER_TABLE = re.compile(
142
+ r'^\s*\[mcp_servers\.(?P<name>"(?:[^"\\]|\\.)+"|[A-Za-z0-9_-]+)\.http_headers\]\s*(?:#.*)?$'
143
+ )
144
+
145
+
146
+ def _toml_table_blocks(lines: list[str], pattern: re.Pattern[str]) -> list[tuple[int, int, str, dict[str, Any]]]:
147
+ blocks: list[tuple[int, int, str, dict[str, Any]]] = []
148
+ if tomllib is None:
149
+ return blocks
150
+ for start, line in enumerate(lines):
151
+ if pattern.fullmatch(line.rstrip("\r\n")) is None:
152
+ continue
153
+ end = next((index for index in range(start + 1, len(lines)) if lines[index].lstrip().startswith("[")), len(lines))
154
+ try:
155
+ parsed = tomllib.loads("".join(lines[start:end]))
156
+ servers = parsed.get("mcp_servers", {})
157
+ name, server = next(iter(servers.items()))
158
+ except (ValueError, TypeError, AttributeError, StopIteration):
159
+ continue
160
+ if isinstance(server, dict):
161
+ blocks.append((start, end, str(name), server))
162
+ return blocks
163
+
164
+
165
+ def repair_codex_mcp_header_collisions(
166
+ paths: Iterable[Path],
167
+ *,
168
+ report: Callable[[str], None] | None = None,
169
+ ) -> list[Path]:
170
+ """Repair only identical legacy-table/managed-inline MCP header collisions."""
171
+
172
+ repaired: list[Path] = []
173
+ if tomllib is None:
174
+ return repaired
175
+ for path in paths:
176
+ if not path.is_file() or path.is_symlink():
177
+ continue
178
+ try:
179
+ text = path.read_text(encoding="utf-8")
180
+ tomllib.loads(text)
181
+ continue
182
+ except (OSError, UnicodeError):
183
+ continue
184
+ except tomllib.TOMLDecodeError:
185
+ pass
186
+ lines = text.splitlines(keepends=True)
187
+ parents = {
188
+ name: server.get("http_headers")
189
+ for _start, _end, name, server in _toml_table_blocks(lines, _MCP_PARENT_TABLE)
190
+ if isinstance(server.get("http_headers"), dict)
191
+ }
192
+ removals: list[tuple[int, int, str]] = []
193
+ for start, end, name, server in _toml_table_blocks(lines, _MCP_HEADER_TABLE):
194
+ old_headers = server.get("http_headers")
195
+ if isinstance(old_headers, dict) and old_headers == parents.get(name):
196
+ removals.append((start, end, name))
197
+ if not removals:
198
+ continue
199
+ removed_lines = {index for start, end, _name in removals for index in range(start, end)}
200
+ candidate = "".join(line for index, line in enumerate(lines) if index not in removed_lines)
201
+ try:
202
+ tomllib.loads(candidate)
203
+ except tomllib.TOMLDecodeError:
204
+ continue
205
+ backup = path.with_name(f"{path.name}.ciel-mcp-repair-{time.time_ns()}.bak")
206
+ temporary = path.with_name(f".{path.name}.ciel-mcp-repair-{os.getpid()}.tmp")
207
+ try:
208
+ shutil.copy2(path, backup)
209
+ temporary.write_text(candidate, encoding="utf-8")
210
+ os.chmod(temporary, stat.S_IMODE(path.stat().st_mode))
211
+ os.replace(temporary, path)
212
+ except OSError:
213
+ temporary.unlink(missing_ok=True)
214
+ continue
215
+ repaired.append(path)
216
+ if report is not None:
217
+ names = ", ".join(sorted({name for _start, _end, name in removals}))
218
+ report(f"Repaired duplicate Codex MCP headers in {path} ({names}); backup: {backup}")
219
+ return repaired
@@ -180,6 +180,13 @@ class PrelaunchServices:
180
180
  options: PrelaunchOptions
181
181
 
182
182
 
183
+ def guarded_zai_oauth_action(action: str, operation: Callable[[str], list[str]]) -> list[str]:
184
+ try:
185
+ return operation(action)
186
+ except RuntimeError as exc:
187
+ return [f"Z.AI OAuth failed: {exc}"]
188
+
189
+
183
190
  def run_prelaunch_menu(passthrough: list[str] | None = None,
184
191
  *,
185
192
  services: PrelaunchServices,
@@ -570,8 +577,8 @@ def run_prelaunch_menu(passthrough: list[str] | None = None,
570
577
  panel_rows, panel_values = api_key_panel_rows(provider, pcfg)
571
578
  panel_idx = 0
572
579
  elif value.startswith("zai-oauth-"):
573
- messages = zai_oauth_action(
574
- value.removeprefix("zai-oauth-")
580
+ messages = guarded_zai_oauth_action(
581
+ value.removeprefix("zai-oauth-"), zai_oauth_action
575
582
  )
576
583
  refresh_checks()
577
584
  cfg = load_config()
@@ -1082,5 +1089,6 @@ __all__ = [
1082
1089
  "PrelaunchSecrets",
1083
1090
  "PrelaunchServices",
1084
1091
  "PrelaunchTerminal",
1092
+ "guarded_zai_oauth_action",
1085
1093
  "run_prelaunch_menu",
1086
1094
  ]
@@ -206,8 +206,11 @@ class SynchronizedLaunch:
206
206
  delegate: Callable[..., Any]
207
207
  synchronize: Callable[..., Any]
208
208
  runtime: str
209
+ prepare: Callable[..., Any] | None = None
209
210
 
210
211
  def __call__(self, *args: Any, **kwargs: Any) -> Any:
212
+ if self.prepare is not None:
213
+ self.prepare(*args, **kwargs)
211
214
  self.synchronize(self.runtime, reason="launch")
212
215
  return self.delegate(*args, **kwargs)
213
216
 
@@ -87,7 +87,7 @@ OPENCODE_ENDPOINT_ALIASES = {
87
87
  }
88
88
 
89
89
  APP_NAME = "Ciel Runtime"
90
- VERSION = "0.2.26"
90
+ VERSION = "0.2.27"
91
91
  CREDITS = "Credits: One Ciel LLC"
92
92
  PRELAUNCH_CANCEL = 10
93
93
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -147,6 +147,15 @@ class ZaiOAuthClient:
147
147
  raise ZaiOAuthError("Z.AI returned an invalid OAuth callback URL.") from exc
148
148
  path = f"/{parsed.path.strip('/')}"
149
149
  if parsed.scheme != "zcode" or parsed.netloc != "zai-auth" or path != "/callback":
150
+ if (
151
+ parsed.scheme == "https"
152
+ and parsed.hostname == "chat.z.ai"
153
+ and path.endswith("/oauth/authorize")
154
+ ):
155
+ raise ZaiOAuthError(
156
+ "The pasted URL is the authorization page, not the completed callback. "
157
+ "Finish authorization and paste the complete zcode://zai-auth/callback URL."
158
+ )
150
159
  raise ZaiOAuthError("Z.AI returned an unexpected OAuth callback target.")
151
160
  query = urllib.parse.parse_qs(parsed.query)
152
161
  state = str((query.get("state") or [""])[0])
@@ -0,0 +1,69 @@
1
+ okf_version: "1.0"
2
+ knowledge:
3
+ task:
4
+ id: kevin-codex-config-mia-zai-oauth-recovery-20260824
5
+ date: "2026-08-24"
6
+ objective: >-
7
+ Restore Kevin's Codex startup and prevent Mia's Z.AI OAuth callback input
8
+ error from terminating the Ciel prelaunch interface.
9
+ status: validation-in-progress
10
+
11
+ evidence:
12
+ kevin:
13
+ observed_error: /home/kevin-codex/.codex/config.toml:63:1 duplicate key
14
+ conflicting_forms:
15
+ - table: mcp_servers.ai-net.http_headers
16
+ lines_before_repair: 36-38
17
+ - inline_key: mcp_servers.ai-net.http_headers
18
+ line_before_repair: 63
19
+ comparison:
20
+ header_names_equal: true
21
+ header_values_equal: true
22
+ repaired_config:
23
+ toml_valid: true
24
+ codex_mcp_list_exit: 0
25
+ ciel_runtime_codex_version_exit: 0
26
+ backup: /home/kevin-codex/.codex/config.toml.ciel-repair-20260824T0555Z.bak
27
+ attribution: >-
28
+ The config and AI-Net managed artifacts were written in the same second,
29
+ but the writer source was not retained; causal attribution is unconfirmed.
30
+ mia:
31
+ supplied_input_kind: https authorization page
32
+ required_input_kind: zcode callback URL
33
+ source_behavior: >-
34
+ ZaiOAuthClient rejected the target with ZaiOAuthError and the prelaunch
35
+ API-key panel invoked the action without a RuntimeError boundary.
36
+
37
+ implementation:
38
+ codex_preflight:
39
+ scope: all Codex launch paths
40
+ mutation_gate:
41
+ - original TOML must be invalid
42
+ - legacy child-table and managed inline header maps must be identical
43
+ - candidate TOML must parse successfully
44
+ recovery: timestamped backup followed by atomic replacement
45
+ non_matching_invalid_files: unchanged
46
+ zai_oauth:
47
+ authorize_page: rejected with completed-callback guidance
48
+ prelaunch_error: rendered in panel without unwinding the menu
49
+
50
+ verification:
51
+ completed:
52
+ - Kevin remote TOML candidate parse
53
+ - Kevin remote config backup and minimal repair
54
+ - Kevin codex mcp list exit 0
55
+ - Kevin Ciel-routed Codex version exit 0
56
+ - focused Codex repair tests
57
+ - focused Z.AI OAuth tests
58
+ - focused synchronized-launch tests
59
+ - Python compilation
60
+ - focused Ruff validation
61
+ - full regression suite: 2744 passed and 136 skipped
62
+ - documentation metadata: 23 Markdown files passed
63
+ - full Ruff validation
64
+ - npm package dry run for version 0.2.27
65
+ - local install reports ciel-runtime 0.2.27
66
+ - local Ciel-routed Codex version exit 0
67
+ pending:
68
+ - main and nightly publication
69
+ - Kevin and Mia nightly installation verification
@@ -6,7 +6,7 @@ knowledge:
6
6
  objective: >-
7
7
  Register ZCode as a supported Ciel runtime, share Z.AI OAuth credentials
8
8
  across runtime clients, and repair leaked Windows bracketed-paste markers.
9
- status: deployment-in-progress
9
+ status: complete
10
10
 
11
11
  evidence:
12
12
  source_code:
@@ -64,5 +64,20 @@ knowledge:
64
64
  - local install to C:/Users/djlov/.local/share/ciel-runtime
65
65
  - local launcher reports ciel-runtime 0.2.26
66
66
  - local ZCode launch reports zcode-runtime 0.16.3
67
- pending:
68
- - nightly publication
67
+ pending: []
68
+
69
+ deployment:
70
+ implementation_commit: 5e461f468ea78f82ac8b1ce531abec4401ee5a51
71
+ branches: [main, nightly]
72
+ local_version: 0.2.26
73
+ npm_latest: 0.2.26
74
+ npm_nightly: 0.2.26-nightly.20260824-051932.5e461f4
75
+ workflow_results:
76
+ main_ci: success
77
+ main_publish: success
78
+ nightly_ci: success
79
+ nightly_publish_and_tarball_verification: success
80
+ registry_execution:
81
+ ciel_runtime: 0.2.26
82
+ zcode_app_cli: 3.8.1-15
83
+ zcode_runtime: 0.16.3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.26",
3
+ "version": "0.2.27",
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",