@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 +15 -0
- package/ciel_runtime.py +3 -3
- package/ciel_runtime_support/prelaunch_terminal.py +46 -9
- package/ciel_runtime_support/providers/zai.py +28 -6
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/upstream_retry.py +7 -0
- package/ciel_runtime_support/upstream_retry_context.py +2 -0
- package/ciel_runtime_support/zai_oauth.py +1 -1
- package/ciel_runtime_support/zai_start_plan_captcha.py +409 -0
- package/docs/journal/2026/08/24/zai-start-plan-codex-captcha-runtime.okf +84 -0
- package/package.json +1 -1
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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__ = [
|
|
@@ -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
|
|
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