@oneciel-ai/ciel-runtime 0.2.29 → 0.2.30

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,21 @@
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.30 — 2026-08-24
7
+
8
+ - Enabled Z.AI Start Plan routed clients by acquiring a fresh request-scoped
9
+ verification value through the official Aliyun CAPTCHA browser SDK before
10
+ every upstream attempt; verification results return only to a state-bound
11
+ loopback receiver and are never persisted.
12
+ - Refresh Start Plan CAPTCHA headers independently for retries, serialize
13
+ concurrent verification requests, and keep every other provider isolated
14
+ from the Start Plan-only headers.
15
+ - Route Codex Responses through the Start Plan gateway's verified Anthropic
16
+ Messages endpoint; the advertised OpenAI base currently exposes no standard
17
+ Responses or Chat Completions route.
18
+ - Accept both native Win32 and ConPTY/SSH VT arrow-key sequences in Windows
19
+ prelaunch menus, including the provider environment menu shown after OAuth.
20
+
6
21
  ## 0.2.29 — 2026-08-24
7
22
 
8
23
  - Split Z.AI general API, Coding Plan, and Start Plan into independent provider
package/ciel_runtime.py CHANGED
@@ -170,6 +170,7 @@ from ciel_runtime_support.credentials import secret_fingerprint as project_secre
170
170
  from ciel_runtime_support.executable_discovery import ExecutableDiscovery
171
171
  from ciel_runtime_support.github_copilot_oauth_runtime import GitHubCopilotOAuthRuntime, GitHubCopilotOAuthRuntimePorts
172
172
  from ciel_runtime_support.zai_oauth import ZaiOAuthClient, ZaiOAuthHttp, ZaiOAuthRuntime, ZaiOAuthRuntimePorts, ZaiOAuthService
173
+ from ciel_runtime_support.zai_start_plan_captcha import ZaiStartPlanRuntimeHeaderPreparer
173
174
  from ciel_runtime_support.headless_config import HeadlessConfigCommands, HeadlessConfigServices, HeadlessEnvFileLoader, apply_headless_config
174
175
  from ciel_runtime_support.http_response import ChannelDeliveryGuard, HttpResponseAdapter
175
176
  from ciel_runtime_support.kimi_runtime_context import KimiConfigurationPorts, KimiIdentityPorts, KimiLifecyclePorts, KimiProcessPorts, KimiRuntimeCompatibilityApi, KimiRuntimeContext
@@ -537,7 +538,6 @@ inject_pending_channel_context = channel_llm_context.inject_pending_channel_cont
537
538
  TERMINAL_INPUT_MODE_RESET = terminal_platform_io.TERMINAL_INPUT_MODE_RESET
538
539
  _terminal_winsize_from_fd = terminal_platform_io.terminal_winsize_from_fd
539
540
  _apply_pty_winsize = terminal_platform_io.apply_pty_winsize
540
-
541
541
  try:
542
542
  sys.stdout.reconfigure(encoding="utf-8")
543
543
  sys.stderr.reconfigure(encoding="utf-8")
@@ -1340,7 +1340,7 @@ provider_requires_streaming = _PROVIDER_REQUEST_ACCESS.requires_streaming
1340
1340
  key_from_request_headers = _PROVIDER_REQUEST_ACCESS.key_from_headers
1341
1341
  provider_headers = _PROVIDER_REQUEST_ACCESS.headers
1342
1342
  get_current_provider = _PROVIDER_REQUEST_ACCESS.current_provider
1343
-
1343
+ prepare_provider_runtime_headers = ZaiStartPlanRuntimeHeaderPreparer(log=router_log)
1344
1344
  def materialize_runtime_command(
1345
1345
  runtime_name: str,
1346
1346
  executable: str,
@@ -2799,7 +2799,7 @@ def upstream_retry_context() -> UpstreamRetryContext:
2799
2799
  errors=UpstreamRetryErrorPorts(project_upstream_http_error_message, project_upstream_retry_message, first_header, parse_retry_after_seconds, format_duration_seconds),
2800
2800
  policy=UpstreamRetryPolicyPorts(project_configured_gateway_retries, retry_after_exceeds_request_timeout, project_retryable_upstream_exception,
2801
2801
  project_upstream_retry_wait_seconds, UPSTREAM_RETRY_HTTP_CODES, lambda: str(load_config().get("language") or "en")),
2802
- credentials=UpstreamRetryCredentialPorts(key_from_request_headers, provider_api_key_count, provider_has_live_api_key, provider_headers, register_api_key_cooldown),
2802
+ credentials=UpstreamRetryCredentialPorts(key_from_request_headers, provider_api_key_count, provider_has_live_api_key, provider_headers, prepare_provider_runtime_headers, register_api_key_cooldown),
2803
2803
  rate_limit=UpstreamRetryRateLimitPorts(learn_router_rate_limit_headers, router_log, register_router_rate_limit_backoff, write_router_activity),
2804
2804
  transport=UpstreamRetryTransportPorts(estimate_tokens, provider_urlopen, set_upstream_stream_read_timeout, provider_stream_idle_timeout_seconds),
2805
2805
  )
@@ -171,6 +171,49 @@ def append_menu_key_debug_log(path: Path, line: str) -> None:
171
171
  pass
172
172
 
173
173
 
174
+ def _windows_menu_key(
175
+ character: str,
176
+ *,
177
+ getwch: Callable[[], str],
178
+ kbhit: Callable[[], bool],
179
+ ) -> str:
180
+ """Decode both Win32 extended keys and ConPTY VT input sequences."""
181
+
182
+ if character in ("\x00", "\xe0"):
183
+ code = getwch()
184
+ return {"H": "up", "P": "down", "K": "left", "M": "right"}.get(code, "")
185
+ if character in ("\r", "\n"):
186
+ return "enter"
187
+ if character != "\x1b":
188
+ return character.lower()
189
+ # SSH/web terminals connected through Windows ConPTY can deliver arrows as
190
+ # ESC [ A instead of Win32's two-character extended-key representation.
191
+ time.sleep(0.01)
192
+ if not kbhit():
193
+ return "esc"
194
+ sequence = ""
195
+ while kbhit() and len(sequence) < 8:
196
+ sequence += getwch()
197
+ if sequence[-1:] in {"A", "B", "C", "D", "H", "F", "~"}:
198
+ break
199
+ return {
200
+ "[A": "up",
201
+ "[B": "down",
202
+ "[C": "right",
203
+ "[D": "left",
204
+ "[H": "home",
205
+ "[F": "end",
206
+ "[1~": "home",
207
+ "[4~": "end",
208
+ "[5~": "pageup",
209
+ "[6~": "pagedown",
210
+ "OA": "up",
211
+ "OB": "down",
212
+ "OC": "right",
213
+ "OD": "left",
214
+ }.get(sequence, "esc")
215
+
216
+
174
217
  def read_menu_key(
175
218
  fd: int | None = None,
176
219
  *,
@@ -179,15 +222,9 @@ def read_menu_key(
179
222
  if os.name == "nt":
180
223
  import msvcrt
181
224
 
182
- character = msvcrt.getwch()
183
- if character in ("\x00", "\xe0"):
184
- code = msvcrt.getwch()
185
- return {"H": "up", "P": "down", "K": "left", "M": "right"}.get(code, "")
186
- if character in ("\r", "\n"):
187
- return "enter"
188
- if character == "\x1b":
189
- return "esc"
190
- return character.lower()
225
+ return _windows_menu_key(
226
+ msvcrt.getwch(), getwch=msvcrt.getwch, kbhit=msvcrt.kbhit
227
+ )
191
228
  descriptor = sys.stdin.fileno() if fd is None or fd < 0 else fd
192
229
  character = os.read(descriptor, 1)
193
230
  log = f"{time.time():.3f} first={character!r}"
@@ -289,6 +289,18 @@ class ZaiStartPlanProviderAdapter(ZaiCodingPlanProviderAdapter):
289
289
  name: str = "zai-start-plan"
290
290
  base_url: str = PROVIDER_DEFAULT_BASE_URLS["zai-start-plan"]
291
291
  include_x_api_key: bool = False
292
+ capabilities_value: ProviderCapabilities = field(
293
+ default_factory=lambda: ProviderCapabilities(
294
+ upstream_protocol="anthropic_messages",
295
+ supports_thinking=True,
296
+ requires_api_key=True,
297
+ )
298
+ )
299
+ request_policy_value: ProviderRequestPolicy = field(
300
+ default_factory=lambda: ProviderRequestPolicy(
301
+ chat_path="/v1/messages", models_path="/v1/models"
302
+ )
303
+ )
292
304
  configuration_defaults_value: dict = field(
293
305
  default_factory=lambda: {
294
306
  **ZaiProviderAdapter().configuration_defaults_value,
@@ -306,6 +318,21 @@ class ZaiStartPlanProviderAdapter(ZaiCodingPlanProviderAdapter):
306
318
  del config
307
319
  return "https://zcode.z.ai/api/v1/zcode-plan/anthropic"
308
320
 
321
+ def supported_protocols(
322
+ self, config: ProviderConfig, model: str | None = None
323
+ ) -> frozenset[MessageProtocol]:
324
+ del config, model
325
+ return frozenset({"anthropic_messages"})
326
+
327
+ def select_protocol(
328
+ self,
329
+ operation: MessageProtocol,
330
+ config: ProviderConfig,
331
+ model: str | None = None,
332
+ ) -> MessageProtocol:
333
+ del operation, config, model
334
+ return "anthropic_messages"
335
+
309
336
  def build_headers(
310
337
  self, config: ProviderConfig, api_key: str | None
311
338
  ) -> Mapping[str, str]:
@@ -330,12 +357,7 @@ class ZaiStartPlanProviderAdapter(ZaiCodingPlanProviderAdapter):
330
357
  def launch_api_key_error(self, config: ProviderConfig) -> str | None:
331
358
  if not config.api_keys:
332
359
  return self.api_key_launch_error_value
333
- return (
334
- "Launch blocked: Z.AI Start Plan requires a fresh Aliyun CAPTCHA "
335
- "runtime header before each model request. The installed ZCode "
336
- "runtime provides that private interactive flow; Ciel Runtime does "
337
- "not bypass or fabricate it for routed clients."
338
- )
360
+ return None
339
361
 
340
362
 
341
363
  __all__ = [
@@ -87,7 +87,7 @@ OPENCODE_ENDPOINT_ALIASES = {
87
87
  }
88
88
 
89
89
  APP_NAME = "Ciel Runtime"
90
- VERSION = "0.2.29"
90
+ VERSION = "0.2.30"
91
91
  CREDITS = "Credits: One Ciel LLC"
92
92
  PRELAUNCH_CANCEL = 10
93
93
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -124,6 +124,7 @@ class UpstreamRetryKeys:
124
124
  provider_api_key_count: Callable[..., Any]
125
125
  provider_has_live_api_key: Callable[..., Any]
126
126
  provider_headers: Callable[..., Any]
127
+ prepare_runtime_headers: Callable[..., dict[str, str]]
127
128
  register_api_key_cooldown: Callable[..., Any]
128
129
 
129
130
 
@@ -180,6 +181,7 @@ def post_json_with_rate_retry(
180
181
  provider_api_key_count = keys.provider_api_key_count
181
182
  provider_has_live_api_key = keys.provider_has_live_api_key
182
183
  provider_headers = keys.provider_headers
184
+ prepare_runtime_headers = keys.prepare_runtime_headers
183
185
  register_api_key_cooldown = keys.register_api_key_cooldown
184
186
  learn_router_rate_limit_headers = rate_limit.learn_headers
185
187
  register_router_rate_limit_backoff = rate_limit.register_backoff
@@ -194,6 +196,7 @@ def post_json_with_rate_retry(
194
196
  byte_estimate = len(json.dumps(req_body, ensure_ascii=False).encode("utf-8"))
195
197
  for attempt in range(rate_limit_max_attempts):
196
198
  try:
199
+ headers = prepare_runtime_headers(provider, pcfg, headers)
197
200
  write_router_activity(
198
201
  "request",
199
202
  provider,
@@ -318,6 +321,7 @@ def open_provider_request_with_key_retry(
318
321
  provider_api_key_count = keys.provider_api_key_count
319
322
  provider_has_live_api_key = keys.provider_has_live_api_key
320
323
  provider_headers = keys.provider_headers
324
+ prepare_runtime_headers = keys.prepare_runtime_headers
321
325
  register_api_key_cooldown = keys.register_api_key_cooldown
322
326
  learn_router_rate_limit_headers = rate_limit.learn_headers
323
327
  register_router_rate_limit_backoff = rate_limit.register_backoff
@@ -334,6 +338,7 @@ def open_provider_request_with_key_retry(
334
338
  data_bytes = json.dumps(req_body).encode("utf-8")
335
339
  for attempt in range(rate_limit_max_attempts):
336
340
  try:
341
+ headers = prepare_runtime_headers(provider, pcfg, headers)
337
342
  write_router_activity(
338
343
  "request",
339
344
  provider,
@@ -460,6 +465,7 @@ def open_openai_stream_with_rate_retry(
460
465
  provider_api_key_count = keys.provider_api_key_count
461
466
  provider_has_live_api_key = keys.provider_has_live_api_key
462
467
  provider_headers = keys.provider_headers
468
+ prepare_runtime_headers = keys.prepare_runtime_headers
463
469
  register_api_key_cooldown = keys.register_api_key_cooldown
464
470
  learn_router_rate_limit_headers = rate_limit.learn_headers
465
471
  register_router_rate_limit_backoff = rate_limit.register_backoff
@@ -477,6 +483,7 @@ def open_openai_stream_with_rate_retry(
477
483
  data_bytes = json.dumps(req_body).encode("utf-8")
478
484
  for attempt in range(rate_limit_max_attempts):
479
485
  try:
486
+ headers = prepare_runtime_headers(provider, pcfg, headers)
480
487
  write_router_activity(
481
488
  "request",
482
489
  provider,
@@ -43,6 +43,7 @@ class UpstreamRetryCredentialPorts:
43
43
  api_key_count: Callable[..., int]
44
44
  has_live_api_key: Callable[..., bool]
45
45
  headers: Callable[..., dict[str, str]]
46
+ prepare_runtime_headers: Callable[..., dict[str, str]]
46
47
  register_cooldown: Callable[..., Any]
47
48
 
48
49
 
@@ -111,6 +112,7 @@ class UpstreamRetryContext:
111
112
  provider_api_key_count=self.credentials.api_key_count,
112
113
  provider_has_live_api_key=self.credentials.has_live_api_key,
113
114
  provider_headers=self.credentials.headers,
115
+ prepare_runtime_headers=self.credentials.prepare_runtime_headers,
114
116
  register_api_key_cooldown=self.credentials.register_cooldown,
115
117
  ),
116
118
  rate_limit=UpstreamRetryRateLimit(
@@ -556,7 +556,7 @@ class ZaiOAuthRuntime:
556
556
  ]
557
557
  if profile == "start-plan":
558
558
  lines.append(
559
- "Start Plan routed launch remains blocked because ZCode requires a fresh interactive CAPTCHA runtime header for model requests."
559
+ "Start Plan model requests will open the official Aliyun CAPTCHA verification page when a fresh runtime header is required."
560
560
  )
561
561
  return lines
562
562
 
@@ -0,0 +1,409 @@
1
+ """Interactive Aliyun CAPTCHA headers for the Z.AI Start Plan gateway.
2
+
3
+ ZCode's Start Plan gateway consumes a fresh Aliyun CAPTCHA verification value
4
+ for each model request. This module hosts the official browser SDK on a
5
+ loopback-only page, receives one state-bound result, and returns only the two
6
+ request-scoped headers used by the gateway.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hmac
12
+ import html
13
+ import json
14
+ import os
15
+ import platform
16
+ import secrets
17
+ import threading
18
+ import urllib.parse
19
+ import urllib.request
20
+ import webbrowser
21
+ from dataclasses import dataclass, field
22
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
23
+ from typing import Any, Callable, Mapping
24
+
25
+
26
+ ZCODE_CLIENT_CONFIG_URL = "https://zcode.z.ai/api/v1/client/configs"
27
+ ALIYUN_CAPTCHA_SDK_URL = (
28
+ "https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js"
29
+ )
30
+ CAPTCHA_PARAM_HEADER = "X-Aliyun-Captcha-Verify-Param"
31
+ CAPTCHA_REGION_HEADER = "X-Aliyun-Captcha-Verify-Region"
32
+ _CAPTCHA_PATH = "/zai-start-plan-captcha"
33
+ _CAPTCHA_RESULT_PATH = f"{_CAPTCHA_PATH}/result"
34
+ _MAX_RESULT_BYTES = 64 * 1024
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class ZaiStartPlanCaptchaConfig:
39
+ enabled: bool
40
+ region: str
41
+ prefix: str
42
+ scene_id: str
43
+
44
+ @classmethod
45
+ def from_envelope(cls, envelope: Any) -> "ZaiStartPlanCaptchaConfig":
46
+ if not isinstance(envelope, Mapping):
47
+ raise RuntimeError("ZCode client config returned an invalid envelope.")
48
+ if envelope.get("code") not in {None, 0, "0"}:
49
+ raise RuntimeError(
50
+ f"ZCode client config request failed (code {envelope.get('code')})."
51
+ )
52
+ data = envelope.get("data")
53
+ configs = data.get("configs") if isinstance(data, Mapping) else None
54
+ captcha = configs.get("captcha") if isinstance(configs, Mapping) else None
55
+ if not isinstance(captcha, Mapping):
56
+ raise RuntimeError("ZCode client config did not include CAPTCHA settings.")
57
+ config = cls(
58
+ enabled=captcha.get("enabled") is not False,
59
+ region=str(captcha.get("region") or "").strip(),
60
+ prefix=str(captcha.get("prefix") or "").strip(),
61
+ scene_id=str(captcha.get("sceneId") or "").strip(),
62
+ )
63
+ if not config.enabled:
64
+ raise RuntimeError("ZCode Start Plan CAPTCHA is disabled by server config.")
65
+ if not config.region or not config.prefix or not config.scene_id:
66
+ raise RuntimeError("ZCode client config returned incomplete CAPTCHA settings.")
67
+ return config
68
+
69
+
70
+ class _LoopbackCaptchaServer(ThreadingHTTPServer):
71
+ daemon_threads = True
72
+ allow_reuse_address = False
73
+
74
+
75
+ @dataclass(slots=True)
76
+ class _CaptchaResultReceiver:
77
+ config: ZaiStartPlanCaptchaConfig
78
+ state: str
79
+ timeout_seconds: float
80
+ host: str = "127.0.0.1"
81
+ port: int = 0
82
+ _server: _LoopbackCaptchaServer | None = field(default=None, init=False)
83
+ _thread: threading.Thread | None = field(default=None, init=False)
84
+ _ready: threading.Event = field(default_factory=threading.Event, init=False)
85
+ _result: str = field(default="", init=False)
86
+ _lock: threading.Lock = field(default_factory=threading.Lock, init=False)
87
+
88
+ @property
89
+ def url(self) -> str:
90
+ if self._server is None:
91
+ raise RuntimeError("Z.AI CAPTCHA receiver was not started.")
92
+ query = urllib.parse.urlencode({"state": self.state})
93
+ return f"http://localhost:{self._server.server_port}{_CAPTCHA_PATH}?{query}"
94
+
95
+ def __enter__(self) -> "_CaptchaResultReceiver":
96
+ receiver = self
97
+
98
+ class Handler(BaseHTTPRequestHandler):
99
+ def do_GET(self) -> None: # noqa: N802
100
+ receiver._handle_get(self)
101
+
102
+ def do_POST(self) -> None: # noqa: N802
103
+ receiver._handle_post(self)
104
+
105
+ def log_message(self, _format: str, *_args: object) -> None:
106
+ return
107
+
108
+ self._server = _LoopbackCaptchaServer((self.host, self.port), Handler)
109
+ self._thread = threading.Thread(
110
+ target=self._server.serve_forever,
111
+ kwargs={"poll_interval": 0.05},
112
+ name="ciel-zai-start-plan-captcha",
113
+ daemon=True,
114
+ )
115
+ self._thread.start()
116
+ return self
117
+
118
+ def __exit__(self, _exc_type: object, _exc: object, _traceback: object) -> None:
119
+ server, thread = self._server, self._thread
120
+ if server is not None:
121
+ server.shutdown()
122
+ server.server_close()
123
+ if thread is not None:
124
+ thread.join(timeout=2.0)
125
+ self._server = None
126
+ self._thread = None
127
+
128
+ def wait(self) -> str:
129
+ if not self._ready.wait(max(0.0, self.timeout_seconds)):
130
+ raise RuntimeError(
131
+ "Z.AI Start Plan CAPTCHA timed out after "
132
+ f"{int(self.timeout_seconds)} seconds."
133
+ )
134
+ with self._lock:
135
+ result = self._result
136
+ if not result:
137
+ raise RuntimeError("Z.AI Start Plan CAPTCHA returned an empty result.")
138
+ return result
139
+
140
+ def _valid_state(self, query: str) -> bool:
141
+ values = urllib.parse.parse_qs(query, keep_blank_values=True)
142
+ supplied = str((values.get("state") or [""])[0])
143
+ return bool(supplied) and hmac.compare_digest(supplied, self.state)
144
+
145
+ def _handle_get(self, handler: BaseHTTPRequestHandler) -> None:
146
+ parsed = urllib.parse.urlsplit(handler.path)
147
+ if parsed.path != _CAPTCHA_PATH:
148
+ self._respond(handler, 404, b"Not found", "text/plain; charset=utf-8")
149
+ return
150
+ if not self._valid_state(parsed.query):
151
+ self._respond(handler, 403, b"Invalid CAPTCHA state", "text/plain; charset=utf-8")
152
+ return
153
+ body = self._page().encode("utf-8")
154
+ self._respond(handler, 200, body, "text/html; charset=utf-8")
155
+
156
+ def _handle_post(self, handler: BaseHTTPRequestHandler) -> None:
157
+ parsed = urllib.parse.urlsplit(handler.path)
158
+ if parsed.path != _CAPTCHA_RESULT_PATH:
159
+ self._respond(handler, 404, b"Not found", "text/plain; charset=utf-8")
160
+ return
161
+ if not self._valid_state(parsed.query):
162
+ self._respond(handler, 403, b"Invalid CAPTCHA state", "text/plain; charset=utf-8")
163
+ return
164
+ try:
165
+ length = int(handler.headers.get("Content-Length") or "0")
166
+ except ValueError:
167
+ length = 0
168
+ if length <= 0 or length > _MAX_RESULT_BYTES:
169
+ self._respond(handler, 413, b"Invalid result size", "text/plain; charset=utf-8")
170
+ return
171
+ value = handler.rfile.read(length).decode("utf-8", errors="strict").strip()
172
+ if not value:
173
+ self._respond(handler, 400, b"Empty result", "text/plain; charset=utf-8")
174
+ return
175
+ with self._lock:
176
+ if self._result:
177
+ self._respond(handler, 409, b"Result already received", "text/plain; charset=utf-8")
178
+ return
179
+ self._result = value
180
+ self._respond(handler, 204, b"", "text/plain; charset=utf-8")
181
+ self._ready.set()
182
+
183
+ @staticmethod
184
+ def _respond(
185
+ handler: BaseHTTPRequestHandler,
186
+ status: int,
187
+ body: bytes,
188
+ content_type: str,
189
+ ) -> None:
190
+ handler.send_response(status)
191
+ handler.send_header("Content-Type", content_type)
192
+ handler.send_header("Content-Length", str(len(body)))
193
+ handler.send_header("Cache-Control", "no-store")
194
+ handler.send_header("Connection", "close")
195
+ handler.end_headers()
196
+ handler.close_connection = True
197
+ if body:
198
+ try:
199
+ handler.wfile.write(body)
200
+ except (BrokenPipeError, ConnectionResetError):
201
+ pass
202
+
203
+ def _page(self) -> str:
204
+ config_json = json.dumps(
205
+ {
206
+ "region": self.config.region,
207
+ "prefix": self.config.prefix,
208
+ "sceneId": self.config.scene_id,
209
+ "state": self.state,
210
+ "resultPath": _CAPTCHA_RESULT_PATH,
211
+ },
212
+ ensure_ascii=True,
213
+ ).replace("<", "\\u003c")
214
+ sdk_url = html.escape(ALIYUN_CAPTCHA_SDK_URL, quote=True)
215
+ return f"""<!doctype html>
216
+ <html lang="en"><head><meta charset="utf-8">
217
+ <meta name="viewport" content="width=device-width,initial-scale=1">
218
+ <title>Ciel Runtime · Z.AI Start Plan verification</title>
219
+ <style>
220
+ body{{font:16px system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 24px;color:#17202a}}
221
+ #captcha-element{{min-height:1px}} button{{font:inherit;padding:10px 16px}}
222
+ #status{{white-space:pre-wrap}} .muted{{color:#5f6b76}}
223
+ </style></head><body>
224
+ <h1>Z.AI Start Plan verification</h1>
225
+ <p id="status">Preparing the official Aliyun CAPTCHA…</p>
226
+ <p class="muted">This page sends the one-time verification result only to the local Ciel Runtime process.</p>
227
+ <div id="captcha-element"></div><button id="captcha-button" type="button">Verify</button>
228
+ <script>const CIEL_CAPTCHA={config_json};</script>
229
+ <script src="{sdk_url}"></script>
230
+ <script>
231
+ (() => {{
232
+ const status = document.getElementById('status');
233
+ const button = document.getElementById('captcha-button');
234
+ let instance = null;
235
+ let interactiveShown = false;
236
+ const setStatus = value => {{ status.textContent = value; }};
237
+ const extractParam = value => value && typeof value === 'object'
238
+ ? String(value.captchaVerifyParam || value.CaptchaVerifyParam || '').trim() : '';
239
+ const submit = async value => {{
240
+ const param = String(value || '').trim();
241
+ if (!param) throw new Error('CAPTCHA returned an empty verification result.');
242
+ const query = new URLSearchParams({{state: CIEL_CAPTCHA.state}});
243
+ const response = await fetch(`${{CIEL_CAPTCHA.resultPath}}?${{query}}`, {{
244
+ method: 'POST', headers: {{'Content-Type': 'text/plain;charset=UTF-8'}}, body: param
245
+ }});
246
+ if (!response.ok) throw new Error(`Ciel Runtime rejected the result (${{response.status}}).`);
247
+ setStatus('Verification complete. Returning to Ciel Runtime…');
248
+ button.hidden = true;
249
+ window.setTimeout(() => window.close(), 700);
250
+ }};
251
+ const showInteractive = () => {{
252
+ interactiveShown = true;
253
+ setStatus('Complete the verification challenge to continue the model request.');
254
+ if (instance && typeof instance.show === 'function') instance.show(); else button.click();
255
+ }};
256
+ button.addEventListener('click', () => {{
257
+ if (instance && typeof instance.show === 'function') instance.show();
258
+ }});
259
+ window.AliyunCaptchaConfig = {{region: CIEL_CAPTCHA.region, prefix: CIEL_CAPTCHA.prefix}};
260
+ if (typeof window.initAliyunCaptcha !== 'function') {{
261
+ setStatus('The official Aliyun CAPTCHA SDK could not be loaded.');
262
+ return;
263
+ }}
264
+ window.initAliyunCaptcha({{
265
+ SceneId: CIEL_CAPTCHA.sceneId, mode: 'popup', language: 'en', showErrorTip: false,
266
+ element: '#captcha-element', button: '#captcha-button',
267
+ getInstance: value => {{
268
+ instance = value;
269
+ window.setTimeout(() => {{
270
+ setStatus('Running security verification…');
271
+ if (typeof value.startTracelessVerification === 'function') value.startTracelessVerification();
272
+ else showInteractive();
273
+ }}, 2000);
274
+ }},
275
+ success: value => submit(value).catch(error => setStatus(error.message)),
276
+ fail: value => {{
277
+ const param = extractParam(value);
278
+ if (param) {{ submit(param).catch(error => setStatus(error.message)); return; }}
279
+ if (!interactiveShown) showInteractive();
280
+ else setStatus('Verification was not accepted. Select Verify to try again.');
281
+ }},
282
+ onError: error => setStatus(`Verification error: ${{error && error.message ? error.message : String(error)}}`)
283
+ }});
284
+ }})();
285
+ </script></body></html>"""
286
+
287
+
288
+ @dataclass(slots=True)
289
+ class ZaiStartPlanCaptchaBroker:
290
+ """Acquire one official Aliyun CAPTCHA result for each upstream attempt."""
291
+
292
+ open_url: Callable[[str], bool] = webbrowser.open
293
+ urlopen: Callable[..., Any] = urllib.request.urlopen
294
+ random_state: Callable[[], str] = lambda: secrets.token_urlsafe(32)
295
+ receiver_factory: Callable[..., Any] = _CaptchaResultReceiver
296
+ log: Callable[[str, str], None] = lambda _level, _message: None
297
+ _request_lock: threading.Lock = field(default_factory=threading.Lock, init=False)
298
+
299
+ def fetch_config(self, app_version: str) -> ZaiStartPlanCaptchaConfig:
300
+ query = urllib.parse.urlencode(
301
+ {
302
+ "app_version": app_version,
303
+ "platform": self._platform_key(),
304
+ }
305
+ )
306
+ request = urllib.request.Request(
307
+ f"{ZCODE_CLIENT_CONFIG_URL}?{query}",
308
+ headers={"Accept": "application/json", "User-Agent": f"ZCode/{app_version}"},
309
+ )
310
+ with self.urlopen(request, timeout=15.0) as response:
311
+ envelope = json.loads(response.read().decode("utf-8"))
312
+ return ZaiStartPlanCaptchaConfig.from_envelope(envelope)
313
+
314
+ def headers(self, options: Mapping[str, Any]) -> dict[str, str]:
315
+ app_version = str(options.get("zcode_app_version") or "3.8.1").strip()
316
+ timeout = self._timeout(options)
317
+ with self._request_lock:
318
+ config = self.fetch_config(app_version)
319
+ state = self.random_state()
320
+ with self.receiver_factory(config, state, timeout) as receiver:
321
+ url = receiver.url
322
+ self.log("INFO", f"zai_start_plan_captcha_waiting url={url}")
323
+ if not self.open_url(url):
324
+ raise RuntimeError(
325
+ "Could not open the Z.AI Start Plan verification page: " + url
326
+ )
327
+ result = receiver.wait()
328
+ self.log(
329
+ "INFO",
330
+ "zai_start_plan_captcha_completed "
331
+ f"region={config.region} result_length={len(result)}",
332
+ )
333
+ return {
334
+ CAPTCHA_PARAM_HEADER: result,
335
+ CAPTCHA_REGION_HEADER: config.region,
336
+ }
337
+
338
+ @staticmethod
339
+ def _timeout(options: Mapping[str, Any]) -> float:
340
+ raw = options.get("zai_captcha_timeout_seconds") or os.environ.get(
341
+ "CIEL_RUNTIME_ZAI_CAPTCHA_TIMEOUT_SECONDS", "120"
342
+ )
343
+ try:
344
+ return max(15.0, min(600.0, float(raw)))
345
+ except (TypeError, ValueError):
346
+ return 120.0
347
+
348
+ @staticmethod
349
+ def _platform_key() -> str:
350
+ system = {"Windows": "win32", "Darwin": "darwin", "Linux": "linux"}.get(
351
+ platform.system(), platform.system().lower()
352
+ )
353
+ machine = platform.machine().lower()
354
+ arch = "arm64" if machine in {"arm64", "aarch64"} else "x64"
355
+ return f"{system}-{arch}"
356
+
357
+
358
+ @dataclass(slots=True)
359
+ class ZaiStartPlanRuntimeHeaderPreparer:
360
+ """Reusable upstream callback backed by one serialized CAPTCHA broker."""
361
+
362
+ log: Callable[[str, str], None] = lambda _level, _message: None
363
+ broker: ZaiStartPlanCaptchaBroker = field(init=False)
364
+
365
+ def __post_init__(self) -> None:
366
+ self.broker = ZaiStartPlanCaptchaBroker(log=self.log)
367
+
368
+ def __call__(
369
+ self,
370
+ provider: str,
371
+ config: Mapping[str, Any],
372
+ headers: Mapping[str, str],
373
+ ) -> dict[str, str]:
374
+ return apply_zai_start_plan_runtime_headers(
375
+ provider, config, headers, broker=self.broker
376
+ )
377
+
378
+
379
+ def apply_zai_start_plan_runtime_headers(
380
+ provider: str,
381
+ config: Mapping[str, Any],
382
+ headers: Mapping[str, str],
383
+ *,
384
+ broker: ZaiStartPlanCaptchaBroker,
385
+ ) -> dict[str, str]:
386
+ """Refresh request-scoped CAPTCHA headers without altering other providers."""
387
+
388
+ projected = {
389
+ name: value
390
+ for name, value in headers.items()
391
+ if name.casefold()
392
+ not in {CAPTCHA_PARAM_HEADER.casefold(), CAPTCHA_REGION_HEADER.casefold()}
393
+ }
394
+ if str(provider or "").casefold() != "zai-start-plan":
395
+ return projected
396
+ projected.update(broker.headers(config))
397
+ return projected
398
+
399
+
400
+ __all__ = [
401
+ "ALIYUN_CAPTCHA_SDK_URL",
402
+ "CAPTCHA_PARAM_HEADER",
403
+ "CAPTCHA_REGION_HEADER",
404
+ "ZCODE_CLIENT_CONFIG_URL",
405
+ "ZaiStartPlanCaptchaBroker",
406
+ "ZaiStartPlanCaptchaConfig",
407
+ "ZaiStartPlanRuntimeHeaderPreparer",
408
+ "apply_zai_start_plan_runtime_headers",
409
+ ]
@@ -0,0 +1,84 @@
1
+ okf_version: "1.0"
2
+ knowledge:
3
+ task:
4
+ id: zai-start-plan-codex-captcha-runtime-20260824
5
+ date: "2026-08-24"
6
+ objective: >-
7
+ Make Z.AI Start Plan launchable through routed Codex while preserving the
8
+ provider's official per-request Aliyun CAPTCHA flow, and restore Windows
9
+ menu arrow input for both native console and ConPTY/SSH terminals.
10
+ status: validation-in-progress
11
+
12
+ evidence:
13
+ installed_zcode:
14
+ version: 3.8.1
15
+ sdk_url: https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js
16
+ request_headers:
17
+ - X-Aliyun-Captcha-Verify-Param
18
+ - X-Aliyun-Captcha-Verify-Region
19
+ refresh_scope: every model request
20
+ official_client_config:
21
+ url: https://zcode.z.ai/api/v1/client/configs
22
+ observed_platform: win32-x64
23
+ captcha_enabled: true
24
+ region: sgp
25
+ prefix: no8xfe
26
+ scene_id: 11xygtvd
27
+ live_endpoint_probe:
28
+ openai_paths:
29
+ /responses: 404
30
+ /v1/responses: 404
31
+ /chat/completions: 404
32
+ /v1/chat/completions: 404
33
+ anthropic_path: /api/v1/zcode-plan/anthropic/v1/messages
34
+ anthropic_invalid_jwt_status_after_captcha: 401
35
+ browser_runtime:
36
+ loopback_host: 127.0.0.1
37
+ official_sdk_loaded: true
38
+ traceless_verification_completed: true
39
+ result_length: 280
40
+ screenshot_captured: true
41
+ remote_credential_audit:
42
+ host: mia@100.95.132.58
43
+ current_provider: zai-start-plan
44
+ persisted_start_plan_secret_count: 0
45
+
46
+ implementation:
47
+ captcha:
48
+ receiver: state-bound loopback-only HTTP server
49
+ persistence: none
50
+ maximum_result_bytes: 65536
51
+ timeout_range_seconds: 15-600
52
+ concurrency: serialized
53
+ retry_behavior: fresh verification for each upstream attempt
54
+ provider_scope: zai-start-plan only
55
+ codex_protocol: anthropic_messages
56
+ terminal_input:
57
+ native_win32_arrows: supported
58
+ conpty_vt_arrows: supported
59
+ standalone_escape: preserved
60
+
61
+ validation:
62
+ targeted:
63
+ architecture_budget: passed
64
+ main_module_line_count: 4980
65
+ captcha_tests: 4_passed
66
+ terminal_primitive_tests: 7_passed
67
+ ruff_changed_files: passed
68
+ full_suite:
69
+ unit:
70
+ passed: 1177
71
+ skipped: 44
72
+ router:
73
+ passed: 951
74
+ channel:
75
+ passed: 381
76
+ skipped: 80
77
+ runtime:
78
+ passed: 255
79
+ skipped: 12
80
+ authenticated_model_request:
81
+ status: blocked
82
+ reason: no Start Plan OAuth secret exists in either inspected workspace
83
+ release:
84
+ status: pending
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oneciel-ai/ciel-runtime",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
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",