From f2b86f6d404dae7698bb9d460b690fdab539c608 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 03:12:38 -0300 Subject: [PATCH 01/43] feat(zcode): add opt-in local app-server agent provider Route native agent turns through the official ZCode runtime over stdio with isolated operator configuration, scoped continuation, cancellation and no replay or external sidecars. Register local discovery and dashboard support, document tool ownership and add transport/routing regressions. --- docs-site/public/images/zcode-provider.png | Bin 0 -> 248857 bytes .../src/content/docs/guides/providers.md | 3 + .../src/content/docs/guides/zcode-agent.md | 126 ++++++++++++ gui/src/provider-icons.ts | 2 + scripts/test-layout/layout.json | 5 +- src/adapters/base.ts | 4 + src/adapters/registry.ts | 8 + src/adapters/zcode/adapter.ts | 179 ++++++++++++++++++ src/adapters/zcode/client.ts | 121 ++++++++++++ src/adapters/zcode/settings.ts | 132 +++++++++++++ src/codex/catalog/provider-fetch.ts | 16 ++ src/providers/registry.ts | 6 + src/server/index.ts | 1 + src/server/management/provider-routes.ts | 11 ++ src/server/models-capabilities.ts | 6 +- src/server/responses/core.ts | 18 +- structure/10_adapter-registry.md | 5 + .../adapter-buffered-tool-conformance.test.ts | 2 + .../adapter-registry-authority.test.ts | 1 + .../adapters/adapter-tool-conformance.test.ts | 4 +- tests/fixtures/test-layout-expected.json | 5 +- .../adapter-conformance/wire-drivers.ts | 5 + tests/providers/zcode-adapter.test.ts | 157 +++++++++++++++ tests/providers/zcode-routing.test.ts | 58 ++++++ tests/providers/zcode-transport.test.ts | 67 +++++++ 25 files changed, 930 insertions(+), 12 deletions(-) create mode 100644 docs-site/public/images/zcode-provider.png create mode 100644 docs-site/src/content/docs/guides/zcode-agent.md create mode 100644 src/adapters/zcode/adapter.ts create mode 100644 src/adapters/zcode/client.ts create mode 100644 src/adapters/zcode/settings.ts create mode 100644 tests/providers/zcode-adapter.test.ts create mode 100644 tests/providers/zcode-routing.test.ts create mode 100644 tests/providers/zcode-transport.test.ts diff --git a/docs-site/public/images/zcode-provider.png b/docs-site/public/images/zcode-provider.png new file mode 100644 index 0000000000000000000000000000000000000000..4947ebfd5e25168950b5d4e50f37c13964f487f0 GIT binary patch literal 248857 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%0D%@Kw}~n6uXC!iaMX*=%2Cy*)?bdkn~mK@ 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 = { "command-code": "commandcode-color.svg", commandcode: "commandcode-color.svg", cursor: "cursor-color.svg", + zcode: "zcode.svg", deepseek: "deepseek-color.svg", firepass: "firepass-color.svg", fireworks: "fireworks-color.svg", @@ -152,6 +153,7 @@ const PROVIDER_DISPLAY_NAMES: Record = { siliconflow: "SiliconFlow", "tencent-coding-plan": "Tencent Cloud Coding Plan", codebuddy: "CodeBuddy", + zcode: "ZCode", "codebuddy-cn": "CodeBuddy CN", qoder: "Qoder", "qoder-cn": "Qoder CN", diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index fad27db650..ad80d1ac98 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1341,7 +1341,10 @@ "zhipu-bigmodel-responses-quota.test.ts": "providers", "zz-ci-api-usage-isolation.test.ts": "ci-workflows", "zz-ci-storage-policy-isolation.test.ts": "ci-workflows", - "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows" + "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", + "zcode-adapter.test.ts": "providers", + "zcode-transport.test.ts": "providers", + "zcode-routing.test.ts": "providers" }, "migrated": [ "adapters", diff --git a/src/adapters/base.ts b/src/adapters/base.ts index f5a22b2969..ae7ebf77f2 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -22,6 +22,10 @@ export interface IncomingMeta { } export interface ProviderAdapter { + /** False for native agents whose accepted turns may have filesystem side effects. */ + replaySafe?: boolean; + /** False when all inference must stay inside the provider's own agent runtime. */ + allowExternalSidecars?: boolean; name: string; /** diff --git a/src/adapters/registry.ts b/src/adapters/registry.ts index d8edbead92..ce23b03bc3 100644 --- a/src/adapters/registry.ts +++ b/src/adapters/registry.ts @@ -1,3 +1,4 @@ +import { createZcodeAdapter } from "./zcode/adapter"; import { createAnthropicAdapter } from "./anthropic"; import { createAzureAdapter } from "./azure"; import type { ProviderAdapter } from "./base"; @@ -22,6 +23,7 @@ export interface AdapterFactoryContext { } export type AdapterWire = + | "zcode" | "codebuddy" | "command-code" | "openai-chat" @@ -33,6 +35,7 @@ export type AdapterWire = | "cursor"; export type AdapterMutationContract = + | "agent-owned-with-explicit-opt-in" | "codex-owned" | "codex-owned-with-gated-native-fallback"; @@ -56,6 +59,11 @@ type InheritedAdapterDefinition = { type AdapterDefinition = DirectAdapterDefinition | InheritedAdapterDefinition; export const ADAPTER_REGISTRY = { + zcode: { + wire: "zcode", + mutation: "agent-owned-with-explicit-opt-in", + create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createZcodeAdapter(provider), + }, codebuddy: { wire: "codebuddy", mutation: "codex-owned", diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts new file mode 100644 index 0000000000..65141259de --- /dev/null +++ b/src/adapters/zcode/adapter.ts @@ -0,0 +1,179 @@ +import { createHash } from "node:crypto"; +import type { OcxParsedRequest, OcxProviderConfig } from "../../types"; +import type { ProviderAdapter } from "../base"; +import { ZcodeClient } from "./client"; +import { loadZcodeSettings, readZcodeModels, record, type JsonObject, type ZcodeSettings } from "./settings"; + +type Client = Pick; +export interface ZcodeAdapterDeps { + settings?: () => ZcodeSettings; + client?: (settings: ZcodeSettings) => Client; + timeoutMs?: number; +} + +// ZCode persists model selection in its settings file: serialize the whole profile, not just a +// session. Each turn owns a child; completing/cancelling it cannot kill another conversation. +const locks = new Map>(); +const sessions = new Map(); +let reservations = 0; +async function lock(key: string, signal?: AbortSignal): Promise<() => void> { + if (signal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + if (reservations >= 32) throw new Error("ZCode profile queue is full."); + reservations++; + const previous = locks.get(key) ?? Promise.resolve(); + let unlock!: () => void; + const current = new Promise(resolve => { unlock = resolve; }); + const tail = previous.then(() => current); + locks.set(key, tail); + let released = false; + const release = () => { + if (released) return; + released = true; reservations--; unlock(); + void tail.then(() => { if (locks.get(key) === tail) locks.delete(key); }); + }; + let onAbort = () => {}; + try { + await Promise.race([previous, new Promise((_, reject) => { + onAbort = () => reject(new Error("ZCode request cancelled before dispatch.")); + signal?.addEventListener("abort", onAbort, { once: true }); + })]); + if (signal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + } catch (error) { release(); throw error; } + finally { signal?.removeEventListener("abort", onAbort); } + return release; +} + +function textInput(parsed: OcxParsedRequest, resumed: boolean): string { + const messages = parsed.context.messages; + const current = resumed ? messages.slice(parsed._continuationConversationMessageIndex ?? -1) : messages; + const lines = current.map(message => { + if (message.role === "toolResult") throw new Error("ZCode executes its own tools; client tool results are unsupported."); + const content = typeof message.content === "string" ? message.content : message.content.map(part => { + if (part.type !== "text") throw new Error("ZCode bridge currently accepts text only."); + return part.text; + }).join("\n"); + return `${message.role}: ${content}`; + }); + const prompt = [...(parsed.context.systemPrompt ?? []), ...lines].join("\n\n"); + if (!prompt.trim() || prompt.length > 200_000) throw new Error("ZCode input is empty or exceeds the bridge limit."); + return prompt; +} + +export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapterDeps = {}): ProviderAdapter { + return { + name: "zcode", + replaySafe: false, + allowExternalSidecars: false, + buildRequest() { throw new Error("ZCode uses its local app-server transport, not HTTP."); }, + async *parseStream() { yield { type: "error", message: "ZCode requires runTurn.", retryable: false }; }, + async runTurn(parsed, incoming, emit) { + let client: Client | undefined; + let release: (() => void) | undefined; + let timer: ReturnType | undefined; + let heartbeat: ReturnType | undefined; + let sent = false; + let sessionId: string | undefined; + let sessionKey: string | undefined; + let stop = () => {}; + const cancelled = () => stop(); + try { + let settings: ZcodeSettings; + try { settings = (deps.settings ?? loadZcodeSettings)(); } + catch { throw new Error("ZCode native execution is unavailable. Configure the isolated launcher, home, workspace and explicit opt-in."); } + if (provider.authMode !== "local") throw new Error("ZCode requires local authentication mode; log in using the isolated ZCode client."); + const model = readZcodeModels(settings).find(item => item.id === parsed.modelId); + if (!model) throw new Error("ZCode model is unavailable in the isolated settings."); + if (parsed.options.toolChoice && parsed.options.toolChoice !== "auto") { + throw new Error("ZCode owns its tools and does not support client tool_choice constraints."); + } + release = await lock(settings.scope, incoming.abortSignal); + const scope = createHash("sha256").update(JSON.stringify([ + settings.scope, provider.baseUrl, parsed._reasoningReplayScope?.current?.providerName, + parsed._cursorIdentityScope, parsed.modelId, + ])).digest("hex"); + sessionKey = parsed._clientThreadId && !parsed._cursorIsolateConversation + ? createHash("sha256").update(scope + parsed._clientThreadId).digest("hex") : undefined; + const previous = record(parsed._providerContinuation?.zcode); + sessionId = previous.scope === scope && typeof previous.sessionId === "string" + && /^sess_[a-zA-Z0-9-]{1,80}$/.test(previous.sessionId) ? previous.sessionId + : sessionKey ? sessions.get(sessionKey) : undefined; + const content = textInput(parsed, Boolean(sessionId)); + client = (deps.client ?? (s => new ZcodeClient(s)))(settings); + const active = client; + const controller = Promise.withResolvers(); + // Attach a handler immediately: failures can happen during session materialization. + void controller.promise.catch(() => {}); + stop = () => { controller.reject(new Error("ZCode turn cancelled or timed out.")); void active.close(); }; + incoming.abortSignal?.addEventListener("abort", cancelled, { once: true }); + if (incoming.abortSignal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + timer = setTimeout(stop, deps.timeoutMs ?? 300_000); + heartbeat = setInterval(() => emit({ type: "heartbeat" }), 5_000); + active.onFailure = error => controller.reject(error); + let textSeen = false; + active.onEvent = message => { + const params = record(message.params); + if (params.sessionId && params.sessionId !== sessionId) return; + if (message.method !== "session/event") return; + const payload = record(params.payload); + if (params.type === "model.streaming") { + if ((payload.kind === "text_delta" || payload.kind === "text_start") && typeof payload.delta === "string") { + textSeen ||= payload.delta.length > 0; + emit({ type: "text_delta", text: payload.delta, phase: "final_answer" }); + } else if ((payload.kind === "reasoning_delta" || payload.kind === "reasoning_start") && typeof payload.delta === "string") { + emit({ type: "thinking_delta", thinking: payload.delta }); + } + } else if (params.type === "tool.updated") { + // Informational only: no tool_call_* event can cause a client to repeat native work. + if (payload.kind === "started" || payload.kind === "result") emit({ type: "text_delta", + text: payload.kind === "started" ? "\n[ZCode: native tool started]\n" : "\n[ZCode: native tool finished]\n", + phase: "commentary" }); + } else if (params.type === "turn.completed") { + if (!textSeen && typeof payload.response === "string") emit({ type: "text_delta", text: payload.response, phase: "final_answer" }); + controller.resolve(); + } else if (params.type === "turn.failed") { + controller.reject(new Error("ZCode agent turn failed. Inspect the isolated client for authentication, quota or model errors.")); + } + }; + if (sessionId) { + await active.request("session/resume", { sessionId, runtimeModel: model.runtimeModel }); + } else { + const result = await active.request("session/create", { + workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace }, + mode: "edit", runtimeModel: model.runtimeModel, titleGenerationEnabled: false, + mcpServers: [], toolDenylist: ["Task", "TaskOutput", "TaskStop"], + }); + const id = record(result.session).sessionId; + if (typeof id !== "string" || !/^sess_[a-zA-Z0-9-]{1,80}$/.test(id)) throw new Error("ZCode returned an invalid session identity."); + sessionId = id; + } + await active.request("session/subscribe", { sessionId, deliveryKind: "desktop-continuous" }); + if (incoming.abortSignal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + // Commit visible output before dispatch, so streaming combo routing cannot replay an + // accepted task that already changed files. Post-send failure is non-retryable incomplete. + emit({ type: "text_delta", text: "[ZCode: running in the configured isolated workspace]\n", phase: "commentary" }); + sent = true; + await active.request("session/send", { sessionId, content, runtimeModel: model.runtimeModel }); + await controller.promise; + if (sessionKey) { + if (sessions.size >= 128) sessions.delete(sessions.keys().next().value!); + sessions.set(sessionKey, sessionId); + } + emit({ type: "done", endTurn: true, providerState: { zcode: { sessionId, scope } } }); + } catch (error) { + if (sessionKey) sessions.delete(sessionKey); + const message = error instanceof Error ? error.message : "ZCode bridge failed."; + if (sent) emit({ type: "incomplete", reason: "zcode_agent_interrupted", message, + retryable: false, endTurn: true }); + else emit({ type: "error", message, retryable: false, status: 400 }); + } finally { + incoming.abortSignal?.removeEventListener("abort", cancelled); + clearTimeout(timer); clearInterval(heartbeat); + if (client) { + if (sent && sessionId) await client.request("session/stop", { sessionId }, 500).catch(() => {}); + await client.close(); + } + release?.(); + } + }, + }; +} diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts new file mode 100644 index 0000000000..36404bab30 --- /dev/null +++ b/src/adapters/zcode/client.ts @@ -0,0 +1,121 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { readJsonLines } from "../coding-agent/protocol"; +import { record, type JsonObject, type ZcodeSettings } from "./settings"; +import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks"; + +export type ZcodeSpawn = typeof spawn; +type Pending = { resolve: (value: JsonObject) => void; reject: (error: Error) => void; timer: ReturnType }; + +/** ZCode 0.16.5 uses request/result NDJSON, without a jsonrpc field. */ +export class ZcodeClient { + private child: ChildProcessWithoutNullStreams; + private pending = new Map(); + private nextId = 0; + private closed = false; + private terminalError?: Error; + private reading: Promise; + private exited: Promise; + private detachShutdown: () => void; + onEvent: (message: JsonObject) => void = () => {}; + onFailure: (error: Error) => void = () => {}; + + constructor(settings: ZcodeSettings, spawnProcess: ZcodeSpawn = spawn) { + const [command, ...args] = settings.command; + this.child = spawnProcess(command!, [...args, "app-server"], { + cwd: settings.home, shell: false, stdio: ["pipe", "pipe", "pipe"], + env: { HOME: settings.home, PATH: process.env.PATH ?? "/usr/bin:/bin", + XDG_CONFIG_HOME: `${settings.home}/.config`, XDG_CACHE_HOME: `${settings.home}/.cache` }, + }) as ChildProcessWithoutNullStreams; + this.exited = new Promise(resolve => { + this.child.once("exit", () => resolve()); + this.child.once("error", () => resolve()); + }); + // Discard vendor diagnostics: stderr may contain account or request data. + this.child.stderr.resume(); + this.child.stdin.on("error", () => this.fail(new Error("ZCode protocol input closed."))); + this.child.once("error", () => this.fail(new Error("ZCode isolated launcher could not start."))); + this.child.once("exit", () => this.fail(new Error("ZCode app server exited before completing the turn."))); + this.reading = this.read(); + this.detachShutdown = registerOptionalShutdownHook(`zcode-${crypto.randomUUID()}`, () => { void this.close(); }); + } + + private fail(error: Error): void { + if (this.closed || this.terminalError) return; + this.terminalError = error; + for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(error); } + this.pending.clear(); + this.onFailure(error); + } + + private write(frame: JsonObject): void { + if (this.closed || this.terminalError) throw this.terminalError ?? new Error("ZCode client is closed."); + this.child.stdin.write(JSON.stringify(frame) + "\n"); + } + + private async read(): Promise { + try { + for await (const message of readJsonLines(this.child.stdout)) { + if (typeof message.method === "string") { + if (message.id !== undefined) { + if (message.method === "session/requestRuntimePreferences") { + this.write({ id: message.id, result: { + nativeSearchEnhancementsEnabled: false, memoryEnabled: false, + askUserQuestionAutoResolutionEnabled: false, modelContextBudgetStrategy: "preflight-v1", + } }); + } else if (message.method === "interaction/requestPermission") { + // Permission decisions cannot be safely represented by all OpenCodex clients. + // Native non-interactive actions still run under ZCode's edit mode and OS sandbox. + this.write({ id: message.id, result: { decision: "deny", reason: "Interactive approval is unavailable through this bridge." } }); + } else if (message.method === "interaction/requestUserInput") { + this.write({ id: message.id, result: { action: "cancel" } }); + this.fail(new Error("ZCode requires user input; continue in the isolated ZCode client.")); + } else { + this.write({ id: message.id, error: { code: -32601, message: "Unsupported ZCode client request." } }); + } + } else this.onEvent(message); + } else if (typeof message.id === "number") { + const item = this.pending.get(message.id); + if (!item) continue; + clearTimeout(item.timer); + this.pending.delete(message.id); + // Never forward raw error messages/stacks: the vendor includes prompts and paths. + if (message.error) item.reject(new Error("ZCode rejected the protocol request. Check the isolated login and model configuration.")); + else item.resolve(record(message.result)); + } + } + this.fail(new Error("ZCode protocol output closed.")); + } catch { + this.fail(new Error("ZCode returned an invalid or oversized protocol stream.")); + } + } + + request(method: string, params: JsonObject, timeoutMs = 30_000): Promise { + if (this.terminalError || this.closed) return Promise.reject(this.terminalError ?? new Error("ZCode client is closed.")); + const id = ++this.nextId; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error("ZCode protocol request timed out.")); + }, timeoutMs); + this.pending.set(id, { resolve, reject, timer }); + try { this.write({ id, method, params }); } + catch { clearTimeout(timer); this.pending.delete(id); reject(new Error("ZCode protocol write failed.")); } + }); + } + + async close(): Promise { + if (this.closed) return; + this.closed = true; + this.detachShutdown(); + for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(new Error("ZCode client closed.")); } + this.pending.clear(); + this.child.stdin.destroy(); + this.child.kill("SIGTERM"); + const killTimer = setTimeout(() => this.child.kill("SIGKILL"), 500); + // Never await an uncooperative descendant that inherited stdout indefinitely. + await Promise.race([this.exited, new Promise(resolve => setTimeout(resolve, 750))]); + clearTimeout(killTimer); + this.child.stdout.destroy(); + this.child.stderr.destroy(); + } +} diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts new file mode 100644 index 0000000000..93d87dc0c0 --- /dev/null +++ b/src/adapters/zcode/settings.ts @@ -0,0 +1,132 @@ +import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from "node:fs"; +import { isAbsolute, join, sep } from "node:path"; +import { createHash } from "node:crypto"; + +export type JsonObject = Record; +export function record(value: unknown): JsonObject { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {}; +} + +export interface ZcodeSettings { + command: string[]; + home: string; + workspace: string; + settingsPath: string; + scope: string; +} + +/** Execution authority is operator environment, never a request header or model configuration. */ +export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env): ZcodeSettings { + if (env.OCX_ZCODE_NATIVE_TOOLS !== "1") { + throw new Error("ZCode native execution is disabled. Configure an isolated launcher and opt in with OCX_ZCODE_NATIVE_TOOLS=1."); + } + let command: unknown; + try { command = JSON.parse(env.OCX_ZCODE_COMMAND ?? "null"); } catch { /* validated below */ } + if (!Array.isArray(command) || !command.length || command.length > 32 + || command.some(v => typeof v !== "string" || v.includes("\0") || v.length > 4096) + || !isAbsolute(command[0])) { + throw new Error("OCX_ZCODE_COMMAND must be a JSON argv array starting with an absolute isolated launcher path."); + } + const home = env.OCX_ZCODE_HOME; + const workspace = env.OCX_ZCODE_WORKSPACE; + if (!home || !workspace || !isAbsolute(home) || !isAbsolute(workspace)) { + throw new Error("ZCode requires explicit absolute OCX_ZCODE_HOME and OCX_ZCODE_WORKSPACE paths."); + } + const realHome = realpathSync(home); + if (realHome === realpathSync(env.HOME ?? home)) throw new Error("ZCode must use a separate home, not the proxy home."); + if (!statSync(realHome).isDirectory()) throw new Error("ZCode home is not a directory."); + const settingsPath = join(realHome, ".zcode", "cli", "config.json"); + return { + command: command as string[], home: realHome, workspace, settingsPath, + scope: createHash("sha256").update(JSON.stringify([command, realHome, workspace])).digest("hex"), + }; +} + +export interface ZcodeModel { + id: string; + providerId: string; + modelId: string; + label: string; + contextWindow?: number; + runtimeModel: JsonObject; +} + +/** Local settings only: no credential import, cloud discovery or writes to ZCode Desktop. */ +export function readZcodeModels(settings: ZcodeSettings): ZcodeModel[] { + try { return readModels(settings); } + catch { throw new Error("ZCode isolated model settings are unavailable, invalid or exceed the size limit."); } +} + +function readSettings(settings: ZcodeSettings): JsonObject { + const home = realpathSync(settings.home); + const resolved = realpathSync(settings.settingsPath); + if (!resolved.startsWith(home + sep)) throw new Error("Settings escaped the isolated home."); + const fd = openSync(settings.settingsPath, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + // Verify the opened object, not just the path checked before open (Linux sandbox host). + if (process.platform === "linux" && !realpathSync(`/proc/self/fd/${fd}`).startsWith(home + sep)) { + throw new Error("Settings escaped the isolated home."); + } + const stat = fstatSync(fd); + if (!stat.isFile() || stat.size > 4 * 1024 * 1024) throw new Error("Invalid settings file."); + const bytes = Buffer.allocUnsafe(4 * 1024 * 1024 + 1); + let length = 0; + while (length < bytes.length) { + const count = readSync(fd, bytes, length, bytes.length - length, null); + if (!count) break; + length += count; + } + if (length === bytes.length) throw new Error("Settings grew beyond the size limit."); + return record(JSON.parse(bytes.subarray(0, length).toString("utf8"))); + } finally { closeSync(fd); } +} + +function readModels(settings: ZcodeSettings): ZcodeModel[] { + const config = readSettings(settings); + const result: ZcodeModel[] = []; + for (const [providerId, value] of Object.entries(record(config.provider))) { + const provider = record(value); + if (provider.enabled === false || providerId === "opencodex") continue; + const options = record(provider.options); + // Never route an agent back into this proxy (including a differently named local entry). + if (typeof options.baseURL === "string") { + let url: URL; + try { url = new URL(options.baseURL); } catch { continue; } + if (url.protocol !== "https:" || url.username || url.password + || /^(localhost|127\.|0\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|\[|.*\.localhost$)/i.test(url.hostname)) continue; + } + const models = Object.entries(record(provider.models)); + for (const [modelId, raw] of models) { + if (!providerId || !modelId || providerId.includes("/") || /[\x00-\x20]/.test(providerId + modelId)) continue; + const model = record(raw); + const limit = record(model.limit); + const contextWindow = typeof limit.context === "number" && limit.context > 0 ? limit.context : undefined; + result.push({ + id: `${providerId}/${modelId}`, providerId, modelId, + label: `${String(provider.name ?? providerId)} / ${String(model.name ?? modelId)}`, + contextWindow, + runtimeModel: { + revision: "opencodex-zcode-v1", generatedAt: Date.now(), model: { providerId, modelId }, + provider: { + providerId, kind: provider.kind ?? "openai-compatible", label: provider.name ?? providerId, + source: provider.source ?? "custom", baseURL: options.baseURL, + ...(provider.apiFormat ? { apiFormat: provider.apiFormat } : {}), + ...(typeof options.apiKey === "string" && options.apiKey + ? { apiKey: { source: "inline", value: options.apiKey } } : {}), + apiKeyRequired: options.apiKeyRequired, + models: models.map(([id, item]) => ({ modelId: id, + contextWindow: record(record(item).limit).context, + maxOutputTokens: record(record(item).limit).output })), + }, + }, + }); + if (result.length > 2000) throw new Error("ZCode model catalog exceeds the size limit."); + } + } + return result; +} + +/** Never include runtimeModel (which may contain a key) in discovery or management output. */ +export function discoverZcodeModels(): Array<{ id: string; label: string; contextWindow?: number }> { + return readZcodeModels(loadZcodeSettings()).map(({ id, label, contextWindow }) => ({ id, label, contextWindow })); +} diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index cf4e4dddef..8305753b3a 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -1,3 +1,4 @@ +import { discoverZcodeModels } from "../../adapters/zcode/settings"; import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases"; import { initialModelSelectionPending } from "../../providers/initial-model-selection"; import { execFileSync } from "node:child_process"; @@ -1655,6 +1656,21 @@ async function fetchProviderModelsWithAuth( ? [...models, vertexDefaultSeed] : models ); + if (prov.adapter === "zcode") { + try { + const models = discoverZcodeModels().map(model => ({ + id: model.id, provider: name, + ...catalogHintsFromProviderConfig(name, prov, model.id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias), + displayName: model.label, + ...(model.contextWindow ? { contextWindow: typeof contextCap === "number" && contextCap > 0 + ? Math.min(model.contextWindow, contextCap) : model.contextWindow } : {}), + inputModalities: ["text"], + } as CatalogModel)); + return observed(withConfiguredRetention(models), "authoritative"); + } catch { + return observed([], "degraded"); + } + } if (prov.adapter === "qoder") { if (!apiKey) return observed(configured, "degraded"); const profile = resolveQoderProfile(prov.baseUrl); diff --git a/src/providers/registry.ts b/src/providers/registry.ts index d4c6c542d0..4ba7010727 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1996,6 +1996,12 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] }, { id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] }, { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" }, + { + id: "zcode", label: "ZCode (local agent)", adapter: "zcode", + baseUrl: "https://zcode.z.ai", authKind: "local", liveModels: true, dashboardPreset: true, + allowBaseUrlOverride: false, + note: "Opt-in local ZCode app-server agent, not the ZCode client integration. Requires an isolated launcher (OCX_ZCODE_COMMAND JSON argv), separate OCX_ZCODE_HOME, OCX_ZCODE_WORKSPACE and OCX_ZCODE_NATIVE_TOOLS=1. Log in using that isolated launcher. ZCode owns its tools; text input only. No automatic credential import or installation.", + }, { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, { id: "lm-studio", label: "LM Studio (local)", adapter: "openai-chat", baseUrl: "http://localhost:1234/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — no key needed" }, diff --git a/src/server/index.ts b/src/server/index.ts index e1aa2be2bd..c1f2f56161 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1847,6 +1847,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server 0, models: models.length, latencyMs: 0, + message: "Local catalog loaded. Account inference is validated only by an explicit agent turn." }); + } catch { + return jsonResponse({ ok: false, latencyMs: 0, + error: "ZCode isolated launcher, opt-in or model settings are unavailable. See the ZCode provider setup guide." }); + } + } if (prov.adapter === "qoder") { const started = Date.now(); const profile = resolveQoderProfile(prov.baseUrl); diff --git a/src/server/models-capabilities.ts b/src/server/models-capabilities.ts index b619d162e8..ab564bce49 100644 --- a/src/server/models-capabilities.ts +++ b/src/server/models-capabilities.ts @@ -105,6 +105,8 @@ export function predictCursorEffort( } export interface ModelCapabilityInput { + /** False for agent-owned transports that cannot execute the caller's tool catalog. */ + supportsToolUse?: boolean; reasoningEfforts?: readonly string[]; contextWindow?: number; /** @@ -126,7 +128,7 @@ export interface ModelCapabilityFields { /** Cursor's extended-row filter REQUIRES this to contain "text"; every route emits text. */ output_modalities: string[]; input_modalities?: string[]; - supports_tool_use: true; + supports_tool_use: boolean; supports_streaming: true; supports_reasoning: boolean; supports_vision?: boolean; @@ -168,7 +170,7 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil // include "text"; omitting the key drops the row from the extended catalog. output_modalities: ["text"], ...(modalities !== undefined && modalities.length > 0 ? { input_modalities: [...modalities] } : {}), - supports_tool_use: true, + supports_tool_use: input.supportsToolUse !== false, supports_streaming: true, supports_reasoning: efforts.length > 0, ...(supportsVision !== undefined ? { supports_vision: supportsVision } : {}), diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 425621814c..8dfa1d76d9 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -4485,8 +4485,11 @@ async function handleResponsesInner( } let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined; - const needsOpenAiVision = shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed); - const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough); + // Opt-in native agents own the entire execution path. Do not turn an unsupported attachment + // into a separate direct-API inference (or resolve helper credentials) before their adapter. + const nativeAgentOwnsExecution = adapter.allowExternalSidecars === false; + const needsOpenAiVision = !nativeAgentOwnsExecution && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed); + const needsOpenAiSearch = !nativeAgentOwnsExecution && shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough); if (needsOpenAiVision || needsOpenAiSearch) { try { // Preserve explicit OpenAI helper auth across route changes without returning it to @@ -4535,7 +4538,7 @@ async function handleResponsesInner( // surface (whose bridge rebuilds headers) or as the raw header for native // Responses callers. Marked + text-only routed model → strip, depth cap 1. const visionDescribeTerminal = options.visionDescribeTerminal === true; - const visionPlan = visionDescribeTerminal + const visionPlan = visionDescribeTerminal || nativeAgentOwnsExecution ? undefined : planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar, { admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, @@ -4550,7 +4553,7 @@ async function handleResponsesInner( recordSidecarOutcome, translatorBudget, ); - } else if (isModelTextOnly(route.provider, route.modelId)) { + } else if (!nativeAgentOwnsExecution && isModelTextOnly(route.provider, route.modelId)) { // Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar // disabled): fail closed — never forward raw images to a text-only upstream. stripImagesInPlace(parsed, translatorBudget); @@ -6346,13 +6349,13 @@ async function handleResponsesInner( // - non-runTurn: web-search wins over image when both eligible (documented priority) // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn // can proceed for web-search-only turns - const wsPlan = !routedCompaction + const wsPlan = !routedCompaction && !nativeAgentOwnsExecution ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, { admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, }) : undefined; - const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined; - const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined; + const imgPlan = !routedCompaction && !nativeAgentOwnsExecution ? await planImageBridge(config, parsed, route.provider) : undefined; + const vidPlan = !routedCompaction && !nativeAgentOwnsExecution ? await planVideoBridge(config, parsed, route.provider) : undefined; const canRunWebSearch = !!wsPlan && !adapter.runTurn; const rotateSidecarProviderOn429 = async ( retryAfter: string | null, @@ -6652,6 +6655,7 @@ async function handleResponsesInner( // intentionally outside this guard and retain their existing one-send wire behavior. const emptyCompletionGuardEnabled = emptyCompletionRetryEnabled(config) + && adapter.replaySafe !== false && !options.comboAttempt && !routedCompaction; diff --git a/structure/10_adapter-registry.md b/structure/10_adapter-registry.md index 2c36481f54..d5aa34b126 100644 --- a/structure/10_adapter-registry.md +++ b/structure/10_adapter-registry.md @@ -22,6 +22,11 @@ The registry records those relationships with `contractParent`. A parent relatio ## Extension policy +`zcode` uses the direct `zcode` wire and `agent-owned-with-explicit-opt-in` mutation contract. +Unlike routed function tools, native ZCode actions are informational output only. Its `runTurn` +sets `replaySafe: false`; accepted failures terminate incomplete rather than becoming automatic +failover candidates. Launcher authority comes only from the operator environment, never requests. + Adding a production adapter requires: 1. one `ADAPTER_REGISTRY` entry with its factory; diff --git a/tests/adapters/adapter-buffered-tool-conformance.test.ts b/tests/adapters/adapter-buffered-tool-conformance.test.ts index fd56fc6e25..6270338ab5 100644 --- a/tests/adapters/adapter-buffered-tool-conformance.test.ts +++ b/tests/adapters/adapter-buffered-tool-conformance.test.ts @@ -24,6 +24,7 @@ const WIRE_MODELS: Record = { "openai-responses": "deepseek-v4-flash", cursor: "cursor/auto", codebuddy: "glm-5.3", + zcode: "test/model", }; function providerFixture(adapterId: string, wire: AdapterWire): OcxProviderConfig { @@ -37,6 +38,7 @@ function providerFixture(adapterId: string, wire: AdapterWire): OcxProviderConfi "openai-responses": "https://api.deepseek.com", cursor: "https://api2.cursor.sh", codebuddy: "https://www.codebuddy.ai", + zcode: "https://zcode.z.ai", }; const baseUrl = adapterId === "mimo-free" ? "https://api.xiaomimimo.com/api/free-ai/openai/chat" diff --git a/tests/adapters/adapter-registry-authority.test.ts b/tests/adapters/adapter-registry-authority.test.ts index d7bac03afe..7093c6197d 100644 --- a/tests/adapters/adapter-registry-authority.test.ts +++ b/tests/adapters/adapter-registry-authority.test.ts @@ -10,6 +10,7 @@ import type { OcxParsedRequest, OcxProviderConfig } from "../../src/types"; import { withTestTranslatorBudget } from "../helpers/translator-budget"; const EXPECTED_ADAPTER_NAMES = { + zcode: "zcode", codebuddy: "codebuddy", "command-code": "command-code", "openai-chat": "openai-chat", diff --git a/tests/adapters/adapter-tool-conformance.test.ts b/tests/adapters/adapter-tool-conformance.test.ts index 91fd5a399b..f932b440e0 100644 --- a/tests/adapters/adapter-tool-conformance.test.ts +++ b/tests/adapters/adapter-tool-conformance.test.ts @@ -35,6 +35,7 @@ const WIRE_MODELS: Record = { "openai-responses": "deepseek-v4-flash", cursor: "cursor/auto", codebuddy: "glm-5.3", + zcode: "test/model", }; function providerFixture(adapterId: string, wire: AdapterWire): OcxProviderConfig { @@ -48,6 +49,7 @@ function providerFixture(adapterId: string, wire: AdapterWire): OcxProviderConfi "openai-responses": "https://api.deepseek.com", cursor: "https://api2.cursor.sh", codebuddy: "https://www.codebuddy.ai", + zcode: "https://zcode.z.ai", }; // Semantic wrappers with provider-specific URL shapes must override the wire-family default here. const baseUrl = adapterId === "mimo-free" @@ -419,7 +421,7 @@ describe("registry-derived routed tool conformance", () => { } }); - const TOOL_LESS_ADAPTERS = new Set(["codebuddy", "qoder"]); + const TOOL_LESS_ADAPTERS = new Set(["codebuddy", "qoder", "zcode"]); test("every registered adapter keeps the nested apply_patch helper in its final request", async () => { for (const [adapterId] of adapterDefinitions()) { diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index e8e712c839..392f217154 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1176,5 +1176,8 @@ "zhipu-bigmodel-responses-quota.test.ts": "providers", "zz-ci-api-usage-isolation.test.ts": "ci-workflows", "zz-ci-storage-policy-isolation.test.ts": "ci-workflows", - "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows" + "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", + "zcode-adapter.test.ts": "providers", + "zcode-transport.test.ts": "providers", + "zcode-routing.test.ts": "providers" } diff --git a/tests/helpers/adapter-conformance/wire-drivers.ts b/tests/helpers/adapter-conformance/wire-drivers.ts index d99ea81a6d..0a179c6536 100644 --- a/tests/helpers/adapter-conformance/wire-drivers.ts +++ b/tests/helpers/adapter-conformance/wire-drivers.ts @@ -318,6 +318,11 @@ export const TOOL_WIRE_DRIVERS = { } }, }, + zcode: { + async observeOutbound(): Promise { + throw new Error("ZCode owns native tools and does not forward client tool catalogs."); + }, + }, codebuddy: { // CodeBuddy v1 runs the vendor CLI with `--tools ""` so Codex keeps tool ownership; it forwards // no client tool catalog and is exempt from routed-tool conformance, so this driver is never diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts new file mode 100644 index 0000000000..dd912206ff --- /dev/null +++ b/tests/providers/zcode-adapter.test.ts @@ -0,0 +1,157 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createZcodeAdapter } from "../../src/adapters/zcode/adapter"; +import { loadZcodeSettings, readZcodeModels, type JsonObject, type ZcodeSettings } from "../../src/adapters/zcode/settings"; +import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../src/types"; +import { createTestTranslatorBudget } from "../helpers/translator-budget"; +import { modelCapabilityFields } from "../../src/server/models-capabilities"; +import { deriveProviderPresets } from "../../src/providers/derive"; + +const roots: string[] = []; +afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); +function fixture(): ZcodeSettings { + const home = mkdtempSync(join(tmpdir(), "ocx-zcode-test-")); roots.push(home); + const dir = join(home, ".zcode", "cli"); mkdirSync(dir, { recursive: true }); + const settingsPath = join(dir, "config.json"); + writeFileSync(settingsPath, JSON.stringify({ provider: { + test: { name: "Test", kind: "anthropic", options: { baseURL: "https://example.invalid", apiKey: "never-log-this" }, + models: { model: { limit: { context: 1000 } } } }, + opencodex: { models: { recursive: {} } }, + alias: { options: { baseURL: "https://127.0.0.1/v1" }, models: { recursive: {} } }, + disabled: { enabled: false, models: { hidden: {} } }, + } })); + return { command: ["/isolated-launcher"], home, workspace: "/workspace", settingsPath, scope: home }; +} +class FakeClient { + onEvent: (event: JsonObject) => void = () => {}; + onFailure: (error: Error) => void = () => {}; + calls: Array<{ method: string; params: JsonObject }> = []; + closed = false; + constructor(private outcome: "ok" | "failed" | "hang" = "ok") {} + async request(method: string, params: JsonObject): Promise { + this.calls.push({ method, params }); + if (method === "session/create") return { session: { sessionId: "sess_test-1" } }; + if (method === "session/send") { + queueMicrotask(() => { + if (this.outcome === "hang") return; + this.event("model.streaming", { kind: "reasoning_delta", delta: "Thinking" }); + this.event("tool.updated", { kind: "started", toolCallId: "tool-1" }); + this.event("tool.updated", { kind: "result", toolCallId: "tool-1" }); + if (this.outcome === "failed") return this.event("turn.failed", { error: { message: "secret" } }); + this.event("model.streaming", { kind: "text_delta", delta: "Hello" }); + this.event("turn.completed", { response: "Hello" }); + }); + } + return {}; + } + event(type: string, payload: JsonObject) { + this.onEvent({ method: "session/event", params: { sessionId: "sess_test-1", type, payload } }); + } + async close() { this.closed = true; } +} +const provider: OcxProviderConfig = { adapter: "zcode", baseUrl: "https://zcode.z.ai", authMode: "local" }; +const request = (): OcxParsedRequest => ({ modelId: "test/model", stream: true, options: {}, + context: { messages: [{ role: "user", content: "Say hello", timestamp: 0 }] } }); +async function run(settings: ZcodeSettings, client: FakeClient, parsed = request(), signal?: AbortSignal) { + const events: AdapterEvent[] = []; + const adapter = createZcodeAdapter(provider, { settings: () => settings, client: () => client, timeoutMs: 40 }); + await adapter.runTurn!(parsed, { headers: new Headers(), abortSignal: signal, + translatorBudget: createTestTranslatorBudget() }, event => events.push(event)); + return events; +} + +describe("ZCode local agent", () => { + test("has no direct HTTP inference path or client-tool capability", () => { + const adapter = createZcodeAdapter(provider); + expect(adapter.fetchResponse).toBeUndefined(); + expect(adapter.replaySafe).toBe(false); + expect(adapter.allowExternalSidecars).toBe(false); + expect(() => adapter.buildRequest(request(), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() })) + .toThrow("not HTTP"); + expect(modelCapabilityFields({ supportsToolUse: false }).capabilities.supports_tool_use).toBe(false); + expect(deriveProviderPresets().find(p => p.id === "zcode")).toMatchObject({ auth: "local", adapter: "zcode" }); + }); + test("requires explicit operator opt-in and an argv launcher, never a shell string", () => { + expect(() => loadZcodeSettings({})).toThrow("disabled"); + expect(() => loadZcodeSettings({ OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: "echo unsafe" })).toThrow("JSON argv"); + }); + test("catalog excludes disabled/recursive entries and preserves canonical model identity", () => { + const models = readZcodeModels(fixture()); + expect(models.map(m => m.id)).toEqual(["test/model"]); + expect(models[0]?.contextWindow).toBe(1000); + }); + test("settings parse errors and escaping symlinks disclose no file content", () => { + const settings = fixture(); + writeFileSync(settings.settingsPath, "{secret-value"); + expect(() => readZcodeModels(settings)).toThrow("unavailable, invalid"); + if (process.platform !== "win32") { + const outside = fixture(); unlinkSync(settings.settingsPath); symlinkSync(outside.settingsPath, settings.settingsPath); + expect(() => readZcodeModels(settings)).toThrow("unavailable, invalid"); + } + }); + test("native execution streams progress without asking the client to execute tools", async () => { + const settings = fixture(); const client = new FakeClient(); + const events = await run(settings, client); + expect(events.some(e => e.type.startsWith("tool_call"))).toBe(false); + expect(events.filter(e => e.type === "text_delta" && e.text === "Hello")).toHaveLength(1); + expect(events.at(-1)?.type).toBe("done"); + expect(JSON.stringify(events)).not.toContain("never-log-this"); + expect(client.closed).toBe(true); + expect(client.calls.map(c => c.method)).toEqual(["session/create", "session/subscribe", "session/send", "session/stop"]); + }); + test("continuation resumes only owner-fenced state and sends the current delta", async () => { + const settings = fixture(); + const first = await run(settings, new FakeClient()); + const done = first.find(e => e.type === "done"); + const next = request(); next._providerContinuation = done?.providerState; + next.context.messages.push({ role: "user", content: "Follow up", timestamp: 1 }); + const client = new FakeClient(); await run(settings, client, next); + expect(client.calls[0]?.method).toBe("session/resume"); + const sent = String(client.calls.find(c => c.method === "session/send")?.params.content); + expect(sent).toContain("Follow up"); expect(sent).not.toContain("Say hello"); + const other = new FakeClient(); await run(fixture(), other, next); + expect(other.calls[0]?.method).toBe("session/create"); + }); + test("post-send failures are non-retryable incomplete, not failover candidates", async () => { + const client = new FakeClient("failed"); const events = await run(fixture(), client); + expect(events.at(-1)).toMatchObject({ type: "incomplete", reason: "zcode_agent_interrupted", retryable: false }); + expect(JSON.stringify(events)).not.toContain("secret"); + expect(client.calls.filter(c => c.method === "session/send")).toHaveLength(1); + }); + test("timeout closes the owned app-server and does not resend", async () => { + const client = new FakeClient("hang"); const events = await run(fixture(), client); + expect(events.at(-1)?.type).toBe("incomplete"); expect(client.closed).toBe(true); + expect(client.calls.filter(c => c.method === "session/send")).toHaveLength(1); + }); + test("a running cancellation closes only its child and never resends", async () => { + const controller = new AbortController(); const client = new FakeClient("hang"); + const pending = run(fixture(), client, request(), controller.signal); + await new Promise(resolve => setTimeout(resolve, 5)); controller.abort(); + expect((await pending).at(-1)).toMatchObject({ type: "incomplete", retryable: false }); + expect(client.closed).toBe(true); + expect(client.calls.filter(c => c.method === "session/send")).toHaveLength(1); + }); + test("profile turns serialize and cancelled waiters never start a child", async () => { + const settings = fixture(); const first = new FakeClient("hang"); const second = new FakeClient(); + const pending = run(settings, first); + await new Promise(resolve => setTimeout(resolve, 5)); + const controller = new AbortController(); const waiting = run(settings, second, request(), controller.signal); + controller.abort(); + expect((await waiting).at(-1)?.type).toBe("error"); + expect(second.calls).toHaveLength(0); + await pending; + expect((await run(settings, new FakeClient())).at(-1)?.type).toBe("done"); + }); + test("pre-aborted requests never spawn or send", async () => { + const client = new FakeClient(); const events = await run(fixture(), client, request(), AbortSignal.abort()); + expect(events.at(-1)?.type).toBe("error"); expect(client.calls).toHaveLength(0); + }); + test("rejects images and client tool results instead of silently losing them", async () => { + const parsed = request(); parsed.context.messages = [{ role: "user", timestamp: 0, + content: [{ type: "image", url: "https://example.invalid/image.png" }] }]; + const client = new FakeClient(); expect((await run(fixture(), client, parsed)).at(-1)?.type).toBe("error"); + expect(client.calls).toHaveLength(0); + }); +}); diff --git a/tests/providers/zcode-routing.test.ts b/tests/providers/zcode-routing.test.ts new file mode 100644 index 0000000000..001609b8a3 --- /dev/null +++ b/tests/providers/zcode-routing.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import type { AdapterEvent, OcxConfig } from "../../src/types"; + +const vision = await import("../../src/vision"); +const search = await import("../../src/web-search"); +const images = await import("../../src/images"); +const resolver = await import("../../src/server/adapter-resolve"); +const forbidden = mock(() => { throw new Error("A native ZCode turn must not plan direct-API sidecars."); }); +mock.module("../../src/vision", () => ({ ...vision, + shouldResolveOpenAiVisionSidecar: forbidden, planVisionSidecar: forbidden, stripImagesInPlace: forbidden })); +mock.module("../../src/web-search", () => ({ ...search, + shouldResolveOpenAiWebSearchSidecar: forbidden, planWebSearch: forbidden })); +mock.module("../../src/images", () => ({ ...images, planImageBridge: forbidden, planVideoBridge: forbidden })); +let sends = 0; +let terminal: AdapterEvent = { type: "done" }; +mock.module("../../src/server/adapter-resolve", () => ({ ...resolver, + resolveAdapter: () => ({ name: "zcode", replaySafe: false, allowExternalSidecars: false, + buildRequest: forbidden, async *parseStream() { throw new Error("Unexpected HTTP transport"); }, + async runTurn(_parsed: unknown, _incoming: unknown, emit: (event: AdapterEvent) => void) { + sends++; emit(terminal); + }, + }), +})); +const { handleResponses } = await import("../../src/server/responses"); +beforeEach(() => { sends = 0; forbidden.mockClear(); terminal = { type: "done" }; }); + +function config(): OcxConfig { + return { port: 0, emptyCompletionRetry: true, defaultProvider: "zcode", providers: { + zcode: { adapter: "zcode", baseUrl: "https://zcode.z.ai", authMode: "local", noVisionModels: ["test/model"] }, + } } as OcxConfig; +} +async function request(stream: boolean) { + return handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "zcode/test/model", stream, input: "hello", + tools: [{ type: "web_search" }, { type: "image_generation" }] }), + }), config(), { model: "", provider: "" }); +} + +describe("ZCode routing keeps inference inside the official agent transport", () => { + for (const stream of [false, true]) { + test(`no helper inference or empty-completion resend (stream=${stream})`, async () => { + const response = await request(stream); + expect(response.status).toBe(200); + await response.text(); + expect(sends).toBe(1); + expect(forbidden).not.toHaveBeenCalled(); + }); + } + test("accepted failure remains incomplete rather than becoming an HTTP retry error", async () => { + terminal = { type: "incomplete", reason: "zcode_agent_interrupted", retryable: false }; + const response = await request(false); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ status: "incomplete", incomplete_details: { reason: "zcode_agent_interrupted" } }); + expect(sends).toBe(1); + expect(forbidden).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/providers/zcode-transport.test.ts b/tests/providers/zcode-transport.test.ts new file mode 100644 index 0000000000..7ca0d5206d --- /dev/null +++ b/tests/providers/zcode-transport.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from "bun:test"; +import { EventEmitter } from "node:events"; +import { PassThrough, Writable } from "node:stream"; +import { ZcodeClient, type ZcodeSpawn } from "../../src/adapters/zcode/client"; +import type { JsonObject, ZcodeSettings } from "../../src/adapters/zcode/settings"; + +const settings: ZcodeSettings = { command: ["/isolated/launcher", "argument with spaces"], home: "/isolated/home", + workspace: "/workspace", settingsPath: "/isolated/config.json", scope: "test" }; +function fixture() { + const child = new EventEmitter() as EventEmitter & { stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: () => boolean }; + const writes: JsonObject[] = []; + child.stdin = new Writable({ write(chunk, _encoding, callback) { writes.push(JSON.parse(chunk.toString())); callback(); } }); + child.stdout = new PassThrough(); child.stderr = new PassThrough(); + child.kill = () => { child.stdout.end(); child.stderr.end(); queueMicrotask(() => child.emit("exit", 0)); return true; }; + let invocation: unknown[] = []; + const spawn = ((...args: unknown[]) => { invocation = args; return child; }) as ZcodeSpawn; + const client = new ZcodeClient(settings, spawn); + return { child, client, writes, invocation: () => invocation }; +} +const tick = () => new Promise(resolve => setTimeout(resolve, 5)); + +describe("ZCode NDJSON transport", () => { + test("uses argv without shell and does not inherit provider secrets", async () => { + const f = fixture(); const invocation = f.invocation(); + expect(invocation[0]).toBe("/isolated/launcher"); + expect(invocation[1]).toEqual(["argument with spaces", "app-server"]); + const options = invocation[2] as { shell: boolean; env: Record }; + expect(options.shell).toBe(false); + expect(Object.keys(options.env).sort()).toEqual(["HOME", "PATH", "XDG_CACHE_HOME", "XDG_CONFIG_HOME"]); + await f.client.close(); + }); + test("correlates fragmented Unicode responses and services runtime preferences", async () => { + const f = fixture(); const pending = f.client.request("session/create", {}); + const bytes = Buffer.from(JSON.stringify({ id: 1, result: { text: "世界" } }) + "\n"); + for (const byte of bytes) f.child.stdout.write(Buffer.from([byte])); + expect(await pending).toEqual({ text: "世界" }); + f.child.stdout.write(JSON.stringify({ id: "server-1", method: "session/requestRuntimePreferences", params: {} }) + "\n"); + await tick(); + expect(f.writes.at(-1)).toMatchObject({ id: "server-1", result: { askUserQuestionAutoResolutionEnabled: false } }); + await f.client.close(); + }); + test("denies interactive permissions rather than silently auto-approving", async () => { + const f = fixture(); + f.child.stdout.write(JSON.stringify({ id: "server-2", method: "interaction/requestPermission", params: {} }) + "\n"); + await tick(); expect(f.writes.at(-1)).toMatchObject({ result: { decision: "deny" } }); + await f.client.close(); + }); + test("malformed protocol rejects pending requests without exposing payload", async () => { + const f = fixture(); const pending = f.client.request("session/create", {}); + f.child.stdout.write("secret-value-not-json\n"); + await expect(pending).rejects.toThrow("invalid or oversized"); + await expect(f.client.request("session/create", {})).rejects.not.toThrow("secret-value"); + await f.client.close(); + }); + test("vendor errors never leak raw messages or stack traces", async () => { + const f = fixture(); const pending = f.client.request("session/create", {}); + f.child.stdout.write(JSON.stringify({ id: 1, error: { message: "secret", data: { stack: "private" } } }) + "\n"); + await expect(pending).rejects.toThrow("ZCode rejected"); + await f.client.close(); + }); + test("EOF and request deadlines cannot leave promises pending", async () => { + const f = fixture(); const pending = f.client.request("session/create", {}); + f.child.stdout.end(); await expect(pending).rejects.toThrow("closed"); await f.client.close(); + const timed = fixture(); await expect(timed.client.request("session/create", {}, 10)).rejects.toThrow("timed out"); + await timed.client.close(); + }); +}); From 63d1348c4c65ffdbacdd0c92f756511b0b5e50bf Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 04:22:14 -0300 Subject: [PATCH 02/43] feat(zcode): connect Desktop and show local usage windows --- .../public/images/zcode-desktop-connected.png | Bin 0 -> 133141 bytes docs-site/public/images/zcode-usage-flash.png | Bin 0 -> 300226 bytes .../src/content/docs/guides/zcode-agent.md | 96 +++++++- gui/.eslint/i18n-allowlist.ts | 1 + gui/src/components/AddProviderModal.tsx | 5 +- gui/src/components/ZcodeDesktopPane.tsx | 116 ++++++++++ gui/src/components/ZcodeUsageNotices.tsx | 39 ++++ .../ProviderCurrentQuota.tsx | 7 +- .../provider-workspace/ProviderOverview.tsx | 2 +- .../ProviderOverviewDashboard.tsx | 3 + .../provider-workspace/ProviderSettings.tsx | 2 + .../provider-workspace/ProviderUsage.tsx | 2 +- gui/src/i18n/de.ts | 40 ++++ gui/src/i18n/en.ts | 40 ++++ gui/src/i18n/fr.ts | 40 ++++ gui/src/i18n/ja.ts | 40 ++++ gui/src/i18n/ko.ts | 40 ++++ gui/src/i18n/ru.ts | 40 ++++ gui/src/i18n/tr.ts | 40 ++++ gui/src/i18n/zh-TW.ts | 40 ++++ gui/src/i18n/zh.ts | 40 ++++ gui/src/provider-payload.ts | 4 +- gui/src/zcode-usage-schedule.ts | 48 ++++ gui/tests/zcode-desktop-pane.test.tsx | 95 ++++++++ gui/tests/zcode-usage-schedule.test.ts | 66 ++++++ scripts/test-layout/layout.json | 10 +- src/adapters/zcode/adapter.ts | 15 +- src/adapters/zcode/client.ts | 11 + src/adapters/zcode/desktop-bootstrap.cjs | 94 ++++++++ src/adapters/zcode/desktop.ts | 212 ++++++++++++++++++ src/adapters/zcode/settings.ts | 10 +- src/providers/registry.ts | 2 +- src/server/management/provider-routes.ts | 4 + src/server/management/zcode-desktop-routes.ts | 63 ++++++ structure/10_adapter-registry.md | 5 +- tests/fixtures/test-layout-expected.json | 10 +- tests/providers/zcode-adapter.test.ts | 18 ++ tests/providers/zcode-desktop-routes.test.ts | 38 ++++ tests/providers/zcode-desktop.test.ts | 74 ++++++ tests/providers/zcode-transport.test.ts | 17 +- 40 files changed, 1403 insertions(+), 26 deletions(-) create mode 100644 docs-site/public/images/zcode-desktop-connected.png create mode 100644 docs-site/public/images/zcode-usage-flash.png create mode 100644 gui/src/components/ZcodeDesktopPane.tsx create mode 100644 gui/src/components/ZcodeUsageNotices.tsx create mode 100644 gui/src/zcode-usage-schedule.ts create mode 100644 gui/tests/zcode-desktop-pane.test.tsx create mode 100644 gui/tests/zcode-usage-schedule.test.ts create mode 100644 src/adapters/zcode/desktop-bootstrap.cjs create mode 100644 src/adapters/zcode/desktop.ts create mode 100644 src/server/management/zcode-desktop-routes.ts create mode 100644 tests/providers/zcode-desktop-routes.test.ts create mode 100644 tests/providers/zcode-desktop.test.ts diff --git a/docs-site/public/images/zcode-desktop-connected.png b/docs-site/public/images/zcode-desktop-connected.png new file mode 100644 index 0000000000000000000000000000000000000000..ff53e1cba114084011edbfa362b09b8740e6b286 GIT binary patch literal 133141 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

dw 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�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,Uh{uhHD}X>W+?}vh86sO1h%= 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 literal 0 HcmV?d00001 diff --git a/docs-site/public/images/zcode-usage-flash.png b/docs-site/public/images/zcode-usage-flash.png new file mode 100644 index 0000000000000000000000000000000000000000..d3a13e611ac61a7e362efdcf122607a775619d0a GIT binary patch literal 300226 zcmXt-wf@UwWxW^4 z%$_~Zo`fqYNC8lYP@tfo0Nj%Yia`*L)$ACt}C$w<@57#r^X%~fDEzQY<# zOhDde0|O5jq@!eClQC6B=EEQks?688ds-gnfEayJbBX$Ay_iY$i;s{1$4a zGKlRxu4ziDBmb^MDg7B`WY4w9Z3HVP2jLW`$7jH_!gEr7W@({q*NiHlrs9=rqbjU# z|3lG^XKWI)T}l{H(NTh}!i4_5zJErY#md&eKO+__gS?IPadTpSOe#v^BOpGnuk?)` z=zmO+mo@aO*^D*UBPUaVM&3f88H$;YCkzy?G89$ZjMOQK5&!&abyBsbm{S=as{Po% zN%A`3BpO!BS8?XM(_sCk*~|o|imOQ?ii_*4QTXPqy(mp~+zwgMh`w;w;CyRA9;*1a zEK4#f@qa?njknxTCi#H)9c+x-paMj~b9_@4Js6F#65qpR07L5fge)+dv7Wd?lKjeQ zaVc6ym({;pxTfwl!)Q+&u3dyM3&AZ&NuCMCC%4S(wr>qbK{7TykU1_z>tz`L(9f~7 zxbp=aD3^=VCRNR=1~%!(=5e7os9i^4WTdh8igya{ZbFM150s!9K#NwYn7)_%G6qpH zhCv&)Djx+cBplU!HlEsCt`JGXyC10`tLaHir4S_7973l4D;MFK(2R-B@^6ndLwv)GFs|JSg=w@K13l|!cqznG@tIcF0I!{Tmj zZ~rFueQ^xd)c$OYHfM@u^HbmI>eoSXMDp(JJ9BK^BE~7pG9t8>_XgaC%2)akx5r1Q zap5n&p=xuaC(5wIp;XHI2T!FmMbXi*;LCBj zk87?tfyP|2oF?+XpwyD+%b=L|=pooT;Qh70Xe*@-R>CqAkTC4t;0Ot}U1SC(Ny#}EUJa?a-WqY;vx6MA~zN+BE*ot2A18LCiZ)WiEC{8%3?sU9ffouyq1EQkzPm;nkC zK4pxI*hu1Ix8}!ByH5OO)?Xl!If9yB&J(7kDxrrd;FOj;2j&R+id7eH4ITBvYF?{H zc=8r((O?uD>dXL|W>OC!p5a9E_s&-|3&1Uvt+$!Q@V> zi85(bwuG7-I7C50B>fX1vw%YCNVMA!yIeU)vDO1sD}#sUK=TB;a*2E@KBFn2V1px| zt(fiC+Wz=~Dj}pLKH2&$mfAtqN3PvVwCN7AiA#4C1x|a<=uTu6H+^zQp%F4D8 z;J@cH4&?-1&M2dH^q^v-J*OkkBa5fPPbdl5x6$&07N^gYAu1&(wGx&_gPD5djz ziTVC0?v?&+;T#dcwB_clIgjWWmP#)JKYxuo@kyEfa1A7F9-DIx%!s;F77+o1<~Oa1 z#abIVJvo89Qb`=)yxU!IA%RA_WKQ;;`#(d3E^;ar$;%~QR#_QUXHpZfv>YYHMNF2) z$;%TA7bl&j&3ol|8Opdx$3Lg5+h~QN9UEum6QHf!@9ViRZIS$v%G(sMP$~l zF;x&W<|{UA2eqiq_eLhkD2Zj>*4{HB!>|NU8VwHy8_ZT3k7hF8MQj>a(FR0CcNtXu zzCOWu6{utwh~e$G^I!&9yTQ7ghK}GMog(1FNFZ;aKryUDU5o4*Vf1+3d}oQu^Wj8I5auJ;c=l7LyH^$JvsO&$h*YO!U-}%j?(Q5@U`bY zu6X(*X`}njW9xsVXzi0ngpARMn%;AsB(@3T3G#6kesR11i_H8&r?Ry4lNKgWbz{=v z=F3U5=-)Y?t6vecO16nm334JJ1cbS9hvY9LdT1O|<4A7(wE2_rN;m@zwHPU2C^dyE zhR6u8^c(2vZY|h=u0#-nnECD?u=u?LxlWTiY|qoUw-}FbkG6(>m`p-uO#Kh6=&;>o z>?uOX#1bHDrZ?{=K7v(D$o(N8-^_$%a!LfRvYyC{0gU%lLPWnvi-2xkuHE00(H zD7YZ!TvU+B35`v+#44|U@w)%3@rfb)SiKPe zwtef#9?C7KPK0a7*21md{wn@76x^F9Z2$K!X(xr{cF6Gw1@Fx|OXA568=sB=KFz5SsMOFvijF&xel>C>_R`IEYUs+|a6+aTq39=~y!=~#P*^PE# zuT4ZZMY=3aI(zv#f;)tj`ud1d893~pYFg`+Sa7@Zt=%Lw_emq?pJJ;2h?gfaM&gc^_%0{X{Pm=8`DmH%T&s1Sb&$ z@$!ibnjEQFVCj$KneY?FUgej0gnSuSW085eF?dxNmiAr-x1hwt!5@g?>PrCqulOI1 zL4*m|@Ma*Bv({UEh(BP_cWy&c`Z0nGhMJeMKa{snNLKXWfe-Uqu~W#MqRb`2-bENa zv{8m{-6=Jsq}dP=A@y0H>>1gEK*72CtfSBvbP%2mlm)vy>1lt_rnCm{ST zsW%Y8u5mCjG0D8ENvGW#1*qAw$6}VYWo@rVSUqgUk;5UR?XaJwM$FdKBj zwA#b{v9Y6W{k`jiL&y2oj(nwFf@NRUDxUTxE{-sT@F+O|Y+&gIJ5R)J&+57}&mI_r z>{<@}i$9q4>vK4Lo#YP zgx2drfD&0J#g_{3K28K7awktn+d*tly~Nw>yz&~axsvT5ef|GfzyrW^Id{U9^sBle z|5L2M1K{QzCjo8*iqP~orm-xH02|t%_wK@)7;Ir@u3QEU2~L1qT>P>rI$?>H-#-F2 zIfY)F)|6|+B`5e?-a^C~E+Um;ljBnZEv2MbEiE}k33mCR%^DBUxk9XeP6*{xa_T{c zJE~SESp6;zVi|TDC^H%F*K^k98Pbhmcu4vOU zaW>c_4=*i2%EGb?xZBX!q{R9S8+X4;Zj4GOKuK{UYx9i7Ni<9C0l>9r@Nk9mjTTq- z*wX*bg${==pN;4mvDIB6yq1RYS0{6_dIAx%i;MbgOwHS$B+|KJjg-5lG}DD(6w)}2 z*hcD6B(*9VjW~EYa>7CdnD|#2y1#YkIj7!kpKy$1^mFpACMSt1wm8Y~Qq2Bgi{pE} z7k|b3Qu2bJt3>|*m{iHx$k;}mGsVd$ke?=z_cQ6UOW~F5!v0VC;1PST>E=u%(4n-g z`oH!#ku>Hq$j~sX_Pc~44cWz-Ihmb?X0`{gv_s$a<~5y$ZXE)u==*$&aCuQ%ac5gZ zw^UQMRX7wXik_j8(pBgk>YhbWvJvXMGEk_gwE_=+bp3?J+YF%UE1cUPBI|3hU!rh+ zh?bN7U7UaOO}Le2PD-=zV*onqIBu}xe!3yYY$13}5Mt`OyM%*~j_L0^(1APpQlKoDZu z3C|(9dm4(JhTPt(=9Wm(Zs=Z9(kB#nt$$f=;C_oh@QTwC8})f9 zGPC;nEqH0JH;~czvv?52uNXvB%!JABbI8&_!9Y@=VJF-Rca2Uu+RBbZ=Lg^i2I_HF zZeA?R6-TM`pp4o|Oz4cVq^1kAWSCkVknq_KsWjD=<@)!g0SfBk?B;X?BXJ;)okil# zzF}+yGeh`64rWE}?Ozw`y(jyjQv{O$u=GmC))K&L>ql;G;>htKfb_k+M*N`>oME)p zfAOVRJq>q4hFsPw<8OKf@R%?Pdd6HLqcH zA(G;;NvdWV{MLam(B8^l-TX^&{4vabe4b=;O(7asM;w$%7`ru%;BQk732*h?7S0vL z_^bj48y){V3ZP;wotd@vr62zOVabWpo(s3I3R?r5=nUez(XQVKjiekwFR}4RH<_UV zlQh%{)6A&=$G*W+jM6jv_22}xB!f4<=b%pBhFhl#zuFGk`3tm*B;jt>GmHgEFf=9z z&F%}_(Buex=mRGiVSyXCBE2@qWqJ5;8o@Jx9PCm&~yH zTfoSKZ@-bjCjUUx;HOmYWbv(RqiCHZTq?;f3elTYn;8O5TXrSFp$S$~yEs>~faNZ~ zKg(jK7$2~DOpzlv&yqFL@k*a>yp%&u%E*+SFmN;pDoX~6O5^@m;n75>!YJc&D$7O$ zLB)}}g$CToQxbv{m36Y&|Hw@gBoaq~rQbo1_20)y|G%vj-=0+Vr73d0Ks8pCX@UTU zyuZtO+6L@mx9kR^P7%VP?x~4xqIzRP3Um0Xn077*QuZU=YAjUaFjWx4 zzc5e*UjZsMd9qPHzp=U!KSDxP@z(N3{2fc_x#?uH24Wtdh811?$aNiIISxx>H`1hX zr*%Y7!7xkjbAvKUGRzHG;H@jiLB-MG3!Bn$k9*Q$lxvG>8T?WtnEzdT8_MV}rEby| zJW&CQXzEoL+yo;o33C;rqxLjX?0xZ4+)ueHeU1P(bNQN2#qJwWYSq_%JcFTD2LHM% z29`+MD01D9KE*FWaaG4f#;GgX3e#J`qN(%V6nvNX)<+JiG>k@C0;p*q4cnDi!jPWl z*Q~v<72gDAe4gmdOBB|yzZAkB;Oz#_0CgM(U^T4!_8LU($|o7{{uJIifI&WHte2td=X08QlyL$xKu4sbVgj#S2FJeTUoJy?3>E?J)zk91g+v!3$ zZcy8X9RLW41sbMMn%Xw{;9=JI5_%G{kGx48Pm=JCV4c#_mD}|!$exGYl5?8M;2y*n7|>{OA&p!?e*(w4jFa^C^83}1lGme@C2Gy1 z2}{|=Ch??{0RCQumT{z}fk%{sG^zO8dmGOZl21c7JSni<`la}wN=UxFmY z*4(OHEuHCPM(tmYPBIYth%}|E)HyZL({mR)bQ63Em?nmguC%r6U(NmvkTjjJ^q{w- zB@~Hkh4YT%0i!~stdatC-+CX3jhn4JKRB}%F;4z0g*7pRhN>Yz?lFe3&21AeD;qJ< z3cv4W?=JC)s{ic(RN|*)g1*LGM5HMXKXKMmFwoFTChVSM`vB0#(QL6V!M)aB!2ofe zN-9zF3s{9^RbUfKe$W5s0YH+qRf@vRT*NW%ddf2K{7P1QO3CL&MTpYyx2?wxYHO*7 zOQuZy9eN%yl6C~9N_`dn2nF-`5TF$=l@`l+JjqaivU@qiA)u|wUN9@)i~H1aEF8DcoGwogZ{(XyN~WTJ#Jl)46HHph3=bN%y%`xSMZ z8Q7vdJ@DGN@X3aYxl^)JI24C z$e>UIG?@${?xZL^GX|AVw)`+xPSZz)`eO`VoEB0RGR;jljYsn$MwzG*V1QY|ubr62 zCyMa^sGLi7OY_g6bP^MJvo@%TXK|nIkgTqN*RJ#7; z=9<&dqQ&Cwe_1)5C?txyfB|T=rskZ+I!!X2@B2BY9LLIeU2fLbhnolfC^p#GIve|x z5uqa(Q4j;S{6o|-R%U_{tEvHS@Qp#Vg?Qrj2Rut!l-Y`j`Y8hHq}}|h&{%cJ=b4n- zvOxi%xvg2ss$Ynsly&m9C2~NS6rq1V^eRQTc0EZz#_9-!{)*KUQ~Yq{%XwwdiFci=m?c2iNfLOe5R}o5eIxkI>cS%hW>L=0)v8VS@S63M$u4K&}Ep`{X!xl1fK?)UPdlG{R8$c{%Ml>*ZwY`T=oJ1jD$q_bQqXWvCk)oZ`sP|> z1PYc^FOmOtb5yi?oG0WJbUeaeKgl4TV&xpc2B}-dk~L6?nF=Q=T)!#Nud`^Ya!jx| zfmV{8LE5S6Xa?JelBmDCLD(+4(LDTa@|+Z@f<8`I0hrm$FCfne-WoS=O*l>{7#lA8 zwV@BRjWa1i;zZ@X6?`ZXPQX-w6cp^(<>)A&YD^*!gsmNyyDR>+$YXe5TFe6Y(HltIac)$;HQ1Gb<;&tkF zFn`^FbJZ6uuF%f%sEb2@4BAmmkBV`58`3ZE-z>ylHBXge$B-#ex2i`$SwOWMoJ_Zf zJP=yK^ieyAUR*bi{8tpxc2{ncnhB^{)6vemh2ipV3_UtArbsSt9ORopxQ?_~qKpGL z|A@3nio0peS)Amf+FfAgMlYT>G|-NoC@)XDY!7XiE}KSLo86Cq3HUS>W#SiMnpnHO zHiet41%CwZ;1^3Fg~IqxGF?~!CjuP%r-AaT+>P~l21}|cec`{dKTO4PscWSw2bIm| zi(WR z&;JJo!2}*OwGgo93AYFW>G4I7%&i(s{L%LHTyeB2W3RCpm2rUT;*t`alWV*=THhoJ z=`Ka^b}hBy0(sk&eNp6S4MZyNIrw*=sS9CbKSH5?ig6LT+)!Eh$tW%KSY3rJPZi6R zs>xbdJpjt<`2*3KxzB+vW_M?hW!#c2>qpNGlWGCWH>P>-UNE1o%2d&{JnH^58xzy2 zced}rR4S+0gp{=H?dfv4cMPd-^Gcb1_xpy=i);En+cvkee>ozs#Z8!GJ-O!igDWCYL2Gvb>8pGboi^b1!C3M z^g_Byi74e-X|DT5&TX*~?y~I~sA6~l1KnhQ+JRfLivR-6LN>9roH^{|Fr&$>d7h$r zf}Gy2C}uObF%~ALF)(8pHrq|3P$r<{)MBwqi_r2sI8yX&IM(RFP4KF|BAh}fM69U$ zhW)N2D^F5%)G)y1%;R$N!W9T2r=0Ul75c4(qU0v+YL*6u`n6e;{mK~Z{X0|cFl1h` zet**Y;r!;GwtdIL`6`n}MNdj-G!cNf#o2PMs8YLe-8V)66%}={T7R)j%{mMNhQvHB zRL(}sHrNQHDIG^qYd49q8zV!+J=Q`f_CFR7oRLEwke8Q6+UR++-&n5k#X764wTye3 zq#d*H0^wma5oOz8&@R@Sp5v?L{L6SH%Ait7Cvp^otQ^JPVoc3<#Kb19XXLqTD9z9Z z2Fp$3{b9fwtlTw1;0n*-!;F~SfV(1<_&PWqJf{r$3`hS3r$tT4WNip#vX+eQ#`!4dS|OTP zxbnLCB%(k;4#r`O^s!W=s?9f+%bsIC?msdRo_&m*`Oi1Vg)OhHya%Qhcy7k47B0c7 zCR`nsfu(V=;qj(M%^?}}6~>QMr)9HILtn4it&+2td9}rH`yhKu!qM?6Iib7dFi&=+ z(fZ^4<-%^I;ix44_o&PI(V}E_s9dVAmGSj1TMPj^G22iSNjKPS^~6iYOxP{ous;;y z2ffK_15Jk88ky3$t^XjNBG4}ZVmQwKR9+w)E&&>|yxjV;b+3bzh%Vm|3QP2=XibG7 zNEZ@n$S^9=i&Uc3FJD=~!( zg~jQQoN?x4bcZTYDJ8YNlt`6&YHFpPKUE88KEcdK>@(+Gl5Y}a%LKiSR8T*2F!y1K z$tA^xkYXiDqhzf`{6^W2$*DKAbdGTPP?iOe*Sv(vJN^(ImTeRtvGaG2l(8UPQnUD1 zk-FjgaK^Q$7b7=(I724*vBqhwUTs!d&y#`r+T79Mb3c(P9isDdP|f?I{(=Pru9;JS z^}auar!eTZ_FoAS&hAO)U~Yt_9gem!P+9gWAh3!AJNOrBXlU54G*EPE3`*c|O;5P@ z_cLRp4_}kgwJAxXVyFt55_4F_Ep3zu%Y> z-{A?PjPC_eTZ43MNAlq&)3iZqg0V8K@yR?X*HSFK94Eym|etfZC?$G^p@BN9S?g9Gc{&AP!>$ICiJC~Un8suN6Ulj&8OcUHa4<1_tWj|R-0J#h<$Ho zDt|P3y+0k8Pi1)S{n6mI{x^~Rn4l>51gTy370cqzhxJw9Z)(&}cqNL;aR(DKrzr%4 z7jn>5ucVBogP5nG!!{2|3T3-quyYTA@G-7;0JgxGWZwD9cK}JTk1>h||=JE)f;W-q@#vjKW%B!Z@C6EGo&NrSH6k z2Y_c%&DAZ`8dyE%A6VDHQG znJa?Wjh&5Zp||U&V(jydt$%g`ZQ0Hv2RG^Ovun! zB%khes{9Lm$?ZfJ@e{$*0Via-pVIO;e01_P+21Xj?_~Slmu?}Rb?bg~Exw*rmuTmG zvwAO5csfqWm$U&l4=eE5INxg(e>{(~DGaGGX}H-eT5Wz>W!0(K9N39jZc~gAa05O* zSXcE$wDb-8^4{0UvaWa^=Dcg)@op{U>v`XqN*&jkj!y|aWPsh59n&klvwgN5t2=J< zn(qXO1djdR9@d-uHkequ=chjzQN8XwTO*>cRJu=By`1sg_I|8iSa=^Z(79PGS`9<5 zem$K=7m~YJ>f#Dr`^fgbi&oWAw(Gb6a@%};z3iVBd^s&uDQ-KBNA2K}9?wjNnkA;U z{hK2~-58)*Z#G%o{qD7Fnmn1p-p__|qpog)7ewv^_expeeT2+K!b2fVF6S%MzNMXw z4=>|Iv1wwVbpEiu#sGJNE^!x@Fexa?q_tEVX()TA2m^nuf zN~8+qOv$A&wpk?}TM)U4Wh zcU->Kd!O@^n2xI-bId~2|8;vQKSmfY$GX{0n!NL6YL(3I%k%PqJ`smW$@rL#&&*RK zKAYRD(9?i%VPWCo>)Uf$M?!)ppUa_tgzx3MceXr%(0e@_uiNo1wI0MZ_adIRQ#vvp zLncbL$LoxI`q=sVgWu&ryL~s;^|X1bm0Zf_pBKIl{lGx95$G2{|8^R;?MkKhgW~Yd zKtI`J)~E5oWYNHsuO}08Bvae1=Gjk|K@Wv#Z%0uo!#_C?*qWBzRtbe({UM)aemaN- zZ8#nXcmvi+1}XC$7dEP%UKTVA6YPWxdY}MyIqHlk+8TgGc2*3T&3i5_|T*ID)B-dFi?ORpZCo99b zcsN$Y31Kj%ZG#OwgUEBXw<;E_Lv!aqWm^U1R(lI+xFY9`Sk!y?BSbT%2f&E^Lv+Qd z;tb#EK$xfQC|2mw%_B-oxSRru`F{`_RQDdULR{q9Wk-Y z{EGWmk|1S?s*UT1&XcEtLDaYk<&VrS8j{`$pA9&Citv zhW`8YVy*i9;J=CElA7x5Wk|y4SF<=k;{6e;XcxJ->~M_%5N(h`{2>sCEuUQjKqJe z?w1lalASqTDA#U`_bi0yu3EXe!0lhL>aJ(Tl$xJ!UcUe$NhXtNT;G|kzK858{M*Nc zUIu_j`O_&mOhJbi$w)GgD9Ho__kpyKgl*r~AizeOw+s-{6;;GtdF7_CZRZ>Q01Zp}DAkb5tu7O(gQ9!Nsn#!=tE*qP!e|@Q%Y>fuOu$FgFDI3Zu zF-^_64!EwP*J?StgDOvoZcLCx!V!>4eT3hs92ndH1I(yn>WkMTuBGzU{|pz#)QX8+ zeJ1R7(b$A;UU}%Uy1e!nKdpa!&vvlRERq{i^-tb}+VL)f2s?<4ibR zR+P}eWM;d;yN^cCUqf$nLJpUc%9m*V3z*mN{pafL*AAa$4mvigyN#C%vE$xl{F{^N zZvWy=g|A!GCRWe4XWqMUQeu-@6sYXWuTR~RuXh@+PX}=aXI~u5I|yp}f7@aetNPq8 zd%Qku2L0lb>$NQY7>(n!+&*}`0B!h0e~`M~viFCcfm@vqKH6Vp?B}A%9vI$NAyX+v zh{$&u?Edpk&8d2FB9+TDn*YIvT5D`DYNj0zcDz-E%ydEt}K)Q!t~lF!%ab{`gFWSb&?D$%9Y)_nKT} zq!Zl&WO27ZCszYQbi9R?!y^uCrFA4*a!}#zB7;BIRFJJgoHxRqn3XNysyFZKR_7*C z&VHJIery25cMe=;W|fS3hJN<7ee$V^b=<=Ghg%bcEv})ekjp--YcdQq^~YW03~3|? zhSPhte!vUkMP;6}VSS+9e2UlUV@uzU3)g5DLtpOkH3jxlI;V3IqsZUi>Cb4Dwq5i> z4KpRmRA0iAnRRFV(rs6pteXvbeF_kWV_;RS&sG{0ICL)A7#J7~;Llc?^_%TTSYH-x z;v8_@JOL=(Z};iWGkFrIbf)8puzkl|xB4X2Ss`!>bI~WdN z?uK-86d`Bq2zkh6<$xcb>v6-1GaPdG?CkfBqu5JC$TAs?6PbqdbU!W*^J$q^?F$g1 z-)d#)<~?@wGLH10S}ph>fwi~Q?6-pNElMcOSkf`-i9$$^XV$J^)?Mv$Z~v6H>~JqZ=p%9F6@ddGm|lNIgm;&b1htjX_^uh zQ)PUF$}efNq%tc!azI1()ejY)*`I)C_yQGg-R({O!}oc{D4Qn5Ww7Qv_g>1r<%nuE zFYs`=93Km%don$e%MtAq0YKE!Xq5zmVAQMI<_312?v+hqUUyvt^SU3!&(hyX1r~b3 zxq}AAa0F%wo$*0ib*8eJp(U--Ij(^C{f8U2t~!e&nTL!17{M#NctM*!*i;iXsWolD{uTM9Q;yvCC2E8yJ2zU&R z)q0&~(^=8s2NS75i{Ckzk?sPR$&ELnS4=?h=1yUTIPHdS%ZX5kkzQ&Tk$5O54z~yr zk`5v0!EAE1JRuPjpkn{5l*<`J7ehuq{e%2m{;fdZAudHNeBEIeCQRyh*c$`ntl{7i z$l0XJ?Dp?}EMVH_-!MXwp^pM)IEP_y@F;d8Ja8s+sc)UTQ4cKqNj~*~@Q-Yo_Lwec z<9%|znnIFgt6p&k>~kkX?hI!tPRSnxJ5yqqpC2PorI_6%ZRK)2Pl$#3IF)Qfb6PiU zhp+2nIsBWXdBy4kQ;K*wrzIe3QTjcX2i)#{5gh(L8#%3^p{Aj(+Vwh%s^9_ei<9Ao z$kcc;OAwKHv9{w71Mdm?``UYs|D%1oe$E8Iw4-p z6bQOLK4wtqa%##(BWVJ?zd%+Y%EkbmEZ=>GS1nc52HkC>7`;xfo$h==j|~dJEWdj~ zdlASSNBodbX6uMuBWQxCw&IY_sSZST+sz^FD1lbquKn%P>(hLKqR;y>+~?ey66mKM z`_8+6D#Z$eepg=G1L4+-RU}+i^Zn>T{GLveT`734 z;T(G68#)Fqt4QxjEF;usn7 zg}tqLoppn8UGxpohNr>2VSr##61cjUCW~E{Zy5`EGukkkN&S@xX7`T^NvkPX(zg6J zO9>eH>Q`g+Y;un#8K41K^(8dg(l6?|{Q@Tml^ZEXQmlW&I;A!s{YTYPi)G zGNRvcJ9ZL;fF3YlZdB~|(V4jS!PBa(6WK*VzOwmesX)cduxUy^uA0G{R1m;vv0=1|%E?dVIj^T2;f*dGm=PUCU@ zcnh^%X()xLnr^%6i{lVlRNMKppPt9epvTn~;zjmT@f^Ra{h3dm>BQ1i36#vs4UhRx zh=(T#8~~`p71}nx-RDdCUH7BUp$j^6Lx!K<>&?&&Tgze8oFbvXaX+L`qDdY+EiD-= zYD;Yd(wfRHvUbtZLH3fe&B{C9!-&c?SgmLC^67dY_d9YA{01OujzFecI$avbt@lel zC`^Eh6jApakQn&-!ueG>!mL8(pK_&-%r~gLWR*2pCAD6rZxWeGQE0j(B1B6(z78@1 zGI^kYNpIoG^dDraqm0ujn_)w~R7ky;d{H(>($m?&zc+!~qH&q&vJx|VJGp;}bYLYCUjm-%uukyebzoN1 zQ=NBPP3*ijjZvZsIxSZ{&2PMahTde4C)3@+Q7e)g@7mp*b`38o4^c`EyakZ!Rwhm& z^iRobB41b}WV;!DEIxogHa!n?J#N{D0l+53bZ%Q94#;A@MtM4`ll6I=rX!6CdA8si zb~xBy91!m(&d%jWm*p}h7Q??@?e{SV$u^Rc(MfnuJJBWiPD3Miehc+FRxBC*A|ZRc z5Yy0eZKQIT775z$I`EA;TPuY4qN&66&f%_?vyFTy^nIq`>Uj}FlBh2zPGr&*o2Wbf zu;{$6VN~p8iW^-eQ13d;c3}IPeTX633_=&!t^tOLg_3!loVkC^qDr}rZe9ns=?iJt z7#eOtgwu0!qq_eGvv!+{iq*>aLtoJ;n1l@k^}i(G z&ese^zCRkDwKW#X%ka6JhA};D_&uEU!J`r-Cf;6j1Bi~T|5Y%su$=v?Y^T${?tYK) zdc@-ufgKLonWma^B|tX0wcE~4jAW+c@zuHFX@4ffAApN5K6HQnd6?_oS_T9#k^7=N#t%liFa5MW z7=GY1YqDOqrO4wO*^t=?e->x0|c)45fVSoVvuC1#nYqt6W}Q4 zSAv2pyYl*cW=$BjXPHQbsgb*|@-8mE zh_a2)eHj@EqLL9py3+g1zBU^abV?o9I^kc?x}?o9N1Ra_DHO}pVYd6ZuKDv319=)a zpYDbhKCl04t`+(xM_yX~l+C8{>OFXYeou!-(rXfc_jk8#`fAyijXP@{{5W6+by z_SQkPwr1_L7D!4>her%Kk69qASO)M2Ix^p!YHP4AUHtcGlVAF z#q+fr*3tO!YYrUV|=j^>JA`q{c3jUIxE<91%Ey9Z5wux$`{ zR>u-F*XXwx^tn1MG^BZ>TFi7v+?Y+qt@JGIjaz@ZJDn|L;$RO62^l+T%(kf^metXl zPs|EWjk0{*6JA#VvO=<((z1$@k|iDGfL$>O^}NElVVSK z%oMHiiaW&Xvz?nY46q9|`dk`!byX5WZtQZt_O$hjEbFsj-$nO_T{dd;8jth7@HSJz zt`rMwE(9pt>`w*_FkT13qjo)RM*{6T93e&`7W(Jw)o`+&7qZiDMX4Ab2oi(6lN+{2 zvRMQAWcIFLs1f3X`+4@QUGw!V5OgAI|7T5d=9p5y>dMS_go3^y>A~{POM*oWmD%5) z`cqH3C|?&ZY7QlZe+Fm{H$(F4bjxWwa$eVSr{z5jo>T?iclrVzhu5^n<>1Pu2k=2KgV-#a+359` z(dU2Z)gK4z?aHVAByRjBkA{6yY>Md}@5fAvf>(!YJoOMH7iD=bbeS_=-BMOiFu-s! z1g!GgeE0T#IJz0!t={mv02gO_`CKRKXEKdn|H_h2Z0=iP~_*V(FrSD1lbdqbZ|W`4wcn*sKMiak0Me2)qZDvy6#MKNV#w2|P{7I@qo zbf5UYNW#_M`zQQeQV7HB$D842Z2ah1n3NL4s=SescG!))k9)a`NgdNVDC<8fL$JQPE=QLLElJjHWRTAa1w z_whyn>yrevOS~cfLM!4h-*X>_KBK0hg_1ZkOD60!9rjYpCOmfjOaHfEXCh`;B3U$H zv&GR^x?O8DPU&n~Y6o{qrrr~5bQu?a{3`K!lL_>4^bapGhOjc>E+BKy6pj^JD3#$0 zG~d^5e3s|_@}7oL{Y=m2Z;FENBM|&KHa?!tW&Jy#8SK>GdXRCou*(L?g*qTEkgYq# z1YO3TdU$nzpIxPRoSBGyYgk=W=n7v(bS|it`}oxIKi( z3n{E+8l04KcRT>l+tG6YQ?ut%X{7m0iqWL1n96WDzEDZ_>rp{MtRMlWS(LlC>|Bw2 zw$JsbL?j+0NZ6TIQRE7hP2sZs2g$E|AHeS2x3jYC=WR?1_K+<-_~xmydHp#Ww60le z1Yf1zWWVtaflAsf4qNUUzV6ck4-GcU*SiUdL&zWM;nO z^#22?uUls&Uurs8-{-v_eH~Ie`y)cxSMMdY!^G~}(H|Y3mSbngy7XNi{IG)A72`=p z5VPfCyrdP^yv6}jt`HJ{&g=2W*8TeBK7pMmUKwn%7BX zafZ{F(EAm-oZsWtFTeku>T?4qg8a=1AHN$iHar}`u8WPBQ_ z4NEsFEi54|B}gbKNb}IJba!_NNF$&k-QC?GAl(Q^cgJ^l=Y9Y99A}(yMqGA(=RVhc z6)r=aFx`na>tPIyu1Al@UAPQ%>-mfJeN^#VZh%(xxn4s>3+sL_znBdt65t=b5a_jR zgB@X=3ip13ABWUL##hq8e}+$JpV~VeW3FJ>8grO|i=hTS=o|5WI`2vXpZvU<*D%X2?pZ9p# zKDPtS4UQ+dpYJYdym{bL4VoYCrhUElCqjQuufm>u6TYv&CQBY}FGACBiz!=&+7C!M zw77s3Cq>^qiScrzZUq7)E=OQlY5<(QTvR@g`gXkAuXW;qjc^=fb zi2x@5C-}~ibu8Z$$DfKDb;_4vq`*88>emI_Pd70+o5!(bEjKz)X}TjnyDmAew)@K! z3(P;>tvvM}XS&S)12WgM6cNALJ}1n>*6-@pV^rGeeV6679v;&vFxIx2pm1yyD{?7r zQLuoE4y3rXAVMV=S`0W?&)hX|D*3HmGpxQ}OC1r5L`bA)twive<%`SGUgc{a?a5dA z0O2!pWh-EepZ62msfVvVLb3Vf(8D76MHG24c}aXY9ePzN{2LKTboXlcNu@~Rl*Rbb zV|wMY3*yNTrQ~Q(xBw z(Q*{VCIqm8T+J(;+I^sF?o3o|uavf4?&QTfSC+%c+OblJ@SO6<*(g;QLMh)i3L<~) zczVClY6$G83k`O&VhMD^J`v|9FAWF?7HB{kjPq5t(q5>gK#O?O#=%7O<`fI?KbsVKIoT^b!J2MiOS=ht+F zx9#(Ts+~Cglxhb)9oDV*9|${XRl1$r9kn@bjMPQn9(%J~e%(-l+l@m1(&nY@kZ1@@ zZ;QwLHioA2%Qn$pD07Uh{>D}*yZK2(uP~aw7Qtz|Kl`W$CYnK~* zdW=tR7dFDCN=speVN3kwEvNx7j+7LkCpx_x7)dq~EI3Z?h6Y=VnlG>h%fIel^!lN31>*B^UI%g+`JnCi-D-^qQI~2-CawxNPZ>c)M=vEiAO&L@X%v& zs7YP~G4th+xM3FzO@;i(etT3OFT0PTMb!xZX$9Ls2g7eyO?BIHe6>Z-^URw~MREk4 zBC8+-TY-YlQcXi6Waecu=h5$)mtp5`Q#d}KxJpQL*!N|hxBEV}z&ZkGe7-(T?BLRM zv{8SmdJ2BM;l5BiFahfsB(_}zj?k9ycLeQpkTSFTH%&kvQaY%YeCiC4RMwfwkx%S> z^1Pm~0-)XVsQF=Vh|}lk!L`b45|s$G_S^vnsa;twb!1jYN+j<32^35Q#g< zqhgR@r+bM@gGp|8yXv&yVk33*0X-VnfZXQ+<-=K?1tu9;7*ZdYq#lE8KUdrOsTQ3k z-`gUnyQwIqy|y@9Y?#q?#CG5|x7YRf_r3o;ZIiz$P;Ykuj9LcTAOKzO2ZQJ7`|PLH zH4We83oh~sFe_(8(an5zrJ`V-{HeSG?(mt?&ymvN|G{ST9T7C*UhAe(k*_`wZi>SR z>iOHcD0ue})b}=*wKb_yvev4gRoEihLqI%yE_xo2-Es`?T)Nmk^x2Xj`jezSE)RkO zvys10Zur1roRIwwJP&1#_V?%m3@Iz#+a+Z`rv*BxoX_ohdZfR6l!`{k6n3@u@wx8s zMDMI;J|5!C=MFon+_N2>Mm=gvAGZFax!s!PayWGZ2!iYD#FHkL)@@*`BG|shRkl1J z&sEC-iIWJ#*TAeVfp*0vmHcfXBiBlt>0w;fi0cr%ME`LOqNLdec=Ay;ira^`@hE4m0amH44!l-8TAbh0p2=pwA^_F`|v zZ3BCgNA!9%5NHDe=xS?pe)m0fD?X7@RVtHIwFT+s4M^c%02HYhy5{N#U zPyPEzi7SsAb0z0u+;xlX?eWJg4S1Z{wZ1sz!3bK<{-iUE{UW!5HP_!B&A9F@Z~S7R z&gAl;4B4-}qqb|lkN_EjAH`j`!scMd{F0n3BSX{inNasG=mq2W# zItF$EtU1T+@8MNSo)^0!_vQH4wVvx0vXpl5fYU#@P8`b_epa9d<@+Q$Yc|h$M;8I$ zeW#GFl>PXSz!X`xovV08i_LeGyv_k@uG(pr8GLh%7bO z!g1mc9%2oS;=8T);@#YAO`M;a4hugi%Kv9*AwLYkai~GQ_^KI}!;`4~o*!vCi(&-5 zL#kvlI(|Esi>Pdqs~C1v346JWvZ?1TW~Bvd;_J(MmiQUl5Gw(v)p@jlTdT#GF47ri~a1v8$=vu z$92#|eP9^yniEkcxQlX>hE^_Y&@>*kxh{J_OhVSz*I$BNsT922f&xD>@(nwv-qlaX zv9)AJm0kC59|y-G5t-e;78!mFt2|b_q?O!6Ws@^XB8k%$Ifchk)Puw+bv&rTwF=~P zSd~$Ia1=4?Xk)2#|--~G%1l;QL3*xu9Ok6Fv2lB1?4 zx|F5%IO7-*cyyf5rJWUDNXpXjKk>`pS}xj! zN2PXp5VHlTb+FkCuuyl2X%2tBT1@#(qjoz6aQnUId3v(Mdrp<~_t~&fl97p@h6m}$=YK#k^8w0pm>! z)yprlC5ZN`-%!nzV636^`{-c>bK&W z=>AKQxGX7r_>F(}e295qztBip!_R6Kn=U3ny+|!i3zvQo69?Z5=TFGecq?auk!kS9 z{gA*Rj;FmQnkd1w?vEaq`_sUn7>Gmdb4PU*MfHUIh%}~65K80jBrhMH_%|p~37#-U z;B=>Md% z66So0j}gd4mCm2vaMhN@O4|DaKeT(XYk{!Yzgh;r5~5qlMTgfU5q?3;Dc{f01W(ii zFK7V$JvS{f+;4kb_Lf*}$zRk%>Op6~)^D^FYO6%Y@xb(g2hn8mkm8fmN@|*tfW|94&x6 z)l`~I(GMydEH*Sv@j=%^{uf#_)e6X$3+jCXRX)UT6k`4Vp`tY)G9`|%85lvsUNHu^ z$pn?(urERb(89?qGb`1NqCf(pyWhB5R;im-RZY#Qi>gjM>P>snj6Pz6lmmYOt$^Gg zx+$atQyELGTPyL0b>^jzTw`w&zm$2}S!~S$2kn;RIczyLBNJ#negB?I2eOM4T}b7c zR5`@Kp!LR9;O1ZH7h6h(CW*$Nxc!iT#4~{*KP4TerZx1R+5`b-}K+8*`B zOTM8WGnMm9)GRLvCDCWVr@fsJV9w@#b@%QrXjBN6>YJP6AU9pUWZ~AE?Xg)-~+= zbroyUZwn)CisxOyRw;+TJ>LKL%*ekVvN;U@_?FG)Ywtu?--KUr;YZBhlXR?2@N_ZD z<59}mAeYgi#4hh6jJLAOHiv2yyu7UH%l(kS zKw6#s5MNSI!DhJ5Obgfajrq3#o%W2t3;2u(M9*0CX`5-&cr{u)2S*IecPja~t7`a` zju^4@@?=I#ih|VtJJ-OS@E~cTKzbnT;^uwc6c~czPbS!XD238;{s>{$sS7CYFGrmv zpp=neko<^MVDT{Luj8dJfw+hoywLsUvYQ2pXOt zfZ!51CBZrY!VOD`aOSA^o+)uAMoQHUoS7eZ#-q1FKH9$I-x7f`;UWF!>KM_3Zt(`s zK1VZrlpy?0hNK|`?pz6cr1TQ^uxG+WG{>|kS-JIeQQj;AgWDm%SV?g0L^I1~qL68d z8Z97D)ps?I7lE7NGCyQOvW%wuJHL2&Mz|Jqbgy0_z{=PWfy7HE;=`9dYQhXORtCZy zLJmZ5Q)Ml|efQ72`ISo@FIr&~z7?D0)|5zzB#IuC-did zz{IwlE{G);caQrh5soen2OZwd49*lIJOVoCodvKo<3@ zPc-Xfi%0>770wKQaJ3G$Qa)bEUmxPuO8R^@I#gqOM;oOaXB_hE(2o?1sX`Cx#`^T5 z+Wuzw_WX2si(~y8a@cr~c|wCtq0!XjpU~oG!&>B5{vf)TvtH9z$IA6l*s7TyZpY3f zU%IIMENmX`i2$0kTyR>LG?Z4mZ*Btp-TUj$A3h%(lGl5if`(8ebhPcT2Wch}PeynOs@}h%1&1U~zRI-VqSk!qz#+Py!-PS?JJ0hW> zw3lPA8+RP(y@2mVSHv}?6dY$z|snngw6-DKk|J}uWAugPVl7}5{ zU8>6I=K0sTe8%5Qr=`kxl*wGl`BSvU=*|?wCbUzUmWXd&%&I_=#RKxdDkNKYc#fL8 z+P;sXiFS2^F-7PShC10^EWW+PI87W&c>k^f>q<7BIbF51R8CciId}JU zhS_^(UirZ8_pEMXNa$9c`FU@!=h{1x$<9ECtQl!IP z3HfjbdR>2>nReT{bqbP?{-S3vg~EwT@K2D-M;CYnAf-FYCR(3p;i7*}{;UI*2d%G$|Y73AW-fF;?K`ovA)qqSRtUp#0CmwBlR!lkJSkcTPe~KC_xJuMhvqV^f~~ylK;=y=5`4G zYCnpl+B&g&*s@B_f)@0~ydY!Dq5f2Ms*~N=+{Eix_q{)IDhy9g2C>#!rYm?qb5l7^ z-I76UwSPea^!PyZD?-lI5*}hk(PAY8eJ)Ea0ZG`U%f}3( zfXb|31;wR{KuERF|II&eck(nGD9AEvVkInDYdB8{IRyStS%AvF2HA)A_z3^-lz$jc zX?A-z>Cv~A2%_+fg;Qt~>-8B-lL*#r~B5AWZGTts(0a2yxU}IHuU8{>(TE(w`$fF`n6x5nQ1sYu0 zSerW9?OGG|QD(~_|GLSoF%i0C4JB+GCST*3i;%uf|0s?wa%WH_7(9%sS28?i#g)rq zHP#lKi-(NxZXo>xIIe|)XNNB(`C_KNX+(r}iWm4k_$v71+UT0fYn8!Em}^xc;GFI- zpL$Vz2q;`v(9ezk!2<`SphJy@sK9geM(AqznR|ta>{81FS&Uj&$F>Z2ujAon zio8^*HCh(Uil_bD)E$vl`p{r$!|lO3{|47q+KqU>(PekSpf^BmOhHxABna;84nq!| zh{X3HkDvBB>&p(c;+P9T&#w0=^k>(Sd2g{OYEwhA^<*y}U#+RCp?n~-`H@5vnV_sQ z0&zN&^K)ChdnC|#`4wdfLh~ytD}hS(C7Iu5v0k3rWu;S63P&j=a#POK^zV94WE$`q z)Y}{P6THea2T(Fz@Rk0+AG<^Lf!r8XF^NmV1@WH51UdAZsAeWE9-0qw^lafmqYIng zZZ{uJcT-DTgJwyj@wzBVheC*J495Wf{*#|JY(u6SWIT0{OdRqHx^1nC3t6o|z6Jz; zidf}x3JMDH^GyJW)-OB-!~W!Ae71FZc_7RP8XR?qFxlk+-O3D@ss!+jwnf| z7f>7E=@-I*%`l5ymWOJLQ5Nllk;j0uiLP}_2?hk#YQK$(&Y1I(PAtBeCyIR-Oz}HZ8nk+^7cepqv zmfIug!mgv%p_M(zT|jogA$n*uFFyeH$X342B0+G3Tu5Tt23 zJqYN{^+Y@Wp5*6KL7Zca%00L5ktx{+J9BeUq_HC- zA|sa=s*N@Y%v*%a;-+(Q3mbD0C`f|@;K!#{zq<>#SpAa_VjNSm{zu<>pT$2gEsm69OkZ# zes|Zccx+q>A2NRs|11PT)g$a-s(6eq@=@q(;6;a67QaE{1y8X{Pt#==gtJzzhD5{>0G`XgmV85&;TYEOBcB)Or?9mCj`aY#yx)&1~rx+_ao1sg%^x zY%|c6nBkm@IR`!Dr0KtV{2iJ8A*YRQ zj^Qp^RkbXWd`gl5qA?dLX31GmcBt23H8*q zRe$G@fm=dz)j&s2?+{lu0C5J1AcZ9T<11Qpi8U=GB-wU(r`LmKVJ91X&-NqWB6kNQ z{Cu6=k@E5FURk#LR;t?xa1l@g9jyGXVNV3HES2*Vn2j_ZJKuoWh16)1>a^c$1eNT) zhb`1JQdL=Ead~m^q_A?NIHJyGf&cN7z7Ik;dbOfv$)}zOzZ4{0c=n=KtH>pLJtNNZ zhJOpyk%OCsRD_Rr$khkJ8CD>5AXZ_P?RXe3OS4e>x#?u1SM+||x2ULyj#e$vSRvGA z8>X_=hl zfY;dqo*wA{V~~5mV+e`r->OtSP#A-+Ea^>_(VjrOcQ`ttV==e>~P`f;1kw|^OQQr7Xd>^I^) zU`{*Z_8`^`4*kKD<>D%UNrm^z zKR(&idfyK8{ZvYAUO4o$EuFo8Y_c26;&TM4t+O>1yW?5W;suCnxV z(?QFakJ2*motkC)7>#DkfpG7Fb<@)E8bij9PSY`Pbi8|UC%HEcJ+XvV+XFKtfyKnL zxCf|!z-1%oR`yMY#j*mh)>mFkk0*DXs84sWJ^O8<(}&;f8SyI&-ck`8CD z{IJncOfHt8I-iFFqz@BI3?%%kHFYEWO@$F9gUs73zcN>y#2G4jS>zt?Uw?A=p~77*(PE$fmm9K-JZ!&w zvK^S&n)FpAM2%*b`IHLp_M3cFRq;cFO*fQHUeXHOUM&0oA5i~C+K!2wdWntR_ew%H z)w%1Sb;I)(v>G(5b}E2a%+HE!uQ#WQnDeeX$6e<~I<=O6O)><}X4XPu1TL=$#i)e; z>%6vqxU*ZxGvh1}^tVI&O+u>gabwpu;`uLl`%|@f#h$I_aK_Eh*_fzzcf9OT`G(SS zq#Gzz!Vk)37e{s2$+P+4D(k_dpAo_lHK~2i={99ADQcmNG577h+`^}{gksq$AH6;c zTOmpw$XA4a=Y=_c2}Vh^Q82`?43r1pa!Q+ZjKq86VunTWtHE&FjGiZz<8TEYet@X5 zhsy>qBynV165`YadVMzSL4E@mwy*L7P9#$))@y|3^GuWh-j zqxaQ-b6w3#2&(Ao>)ZxlK z>bKO7e2Ib9J1W>^_L#8Q=FK&9{WvcTm>SpEKUH^IvEUiUubP9ncaS7#oPj1K3C^HW zy=it#DLq(l6>l{88$rxh@!m?Sf}HoqMjCYjEx4WRKhUpK%#61{_$;G#*TzhTk{c0Z zDSb>VEG9uRuGRRb(ux-^HcNrN_l@)wwaDe7BWyz^`fARst=48iDf2X`s%EV{(DL0r zFuTMEGb=dxy%`<<=6SV0VKqVH`%1gntvieXc*^U%uNHh`MNlWESKS-wkUJ0M(Zv*2 zKQXz{x&+4AKCx?c+6ST*kclc4fzefN(9!6;M=KN6><{+#F+ILFc6l<Wp4&*#SaA{&*_W^rpgcvD{A88+ruNNppp!U7D&)CcaE)y^>8bE?A;}vJ~ zacAq@;R0}FmDJAf<|isWH*aJfgF|ST3_L|1KMNOZ>Zni-o6knwyq_Mfgwu^$L3)gZ zgyvH0>1OP!Y077p2Jj(5_!+mL&y5V=cOZsAB}43+-%Tyj-x*^VK`NJbce28`@7cTb z<)e~B=D_a8{$(%3Zqac}1o$d~gmQTFeQv&+I^%(0qu||sRR#7!on71AVH1P*26gPk zw5Gn{ps4q)K|i>KLK#UPo5A4DBoPn(oJy(-gAZAK@rMQkN!d3T+iM*N6|>&BcZcE6 z&I1K~*{{+Fl7DR3I-SEMtN)*9OXd#Qu`Wd=ts&;@On)t!9#HNJ`C+@ z+4Cqy_%Mv+I zU8LER7&oab_Tc+;N3)5Y;letr^)IBLm-X<`7#dS?PKU1n`z`ygLsd^t6oA80RMg)X zAlMv9m<|4^H^1RxLU%Kv!ohUY7{CW4q@ip71Ac3)parR^?2p%Zs*$7*6%K#jy&4q^ zC7BCt)AgF0tuQ{kpD9_et~xLbrI~vz;a+W?b$($vTaN!?b#x6NJwiIZ8IWl!KH**~ zx4d`}N^Vb4sb2I4LYR%NmBwlxuaz69lqtkAty4$xWHVR!{cO@y>vz9q3ih~%jh}_` zyF)4{CH4UN&7mZ~m-mo=j3w5|!66-Pc!xC0`^A z!~$J~zh0HA>&{F5Fpv(2a{1E@^j@LYGfPNEEPfgNe}R1rSRcL5Mxe8_=5a?Aebi)k zU@VVii_vg`v^i3xiq~*v7c&=WCE~-~RSJPyq)w9yJF`@!F|gGNpiZ-fJHe40bPF}v z9XmUM*Qn5WBJHyCEi|`wbWESn?cri(1-LW6Nrh4r1zNvi)`>UT^?hdmd?Q?Anu zqV!|JM{hDXq}KITop!-t?A(hLz3u;n(25~;y^7d83p{1+~-J5by5x9^z_Jl;?}`TD_xV$l=>G;N^oFMZRv^um*RD0 zG6C&ca2;9RG4Xw@?>*u+H6|&?qVvpW?!WM;Q^i-l;H?#q?z@OeNFcy^Hi~3k#IE>0 zQEl_qm;2knWT(Dz8*`<^uXSSBvMy+SR>uxj&E`s2oJ-KHtCjdd;dM7s4T7zkN~Wz@ z{LcHcu_p+KC-dmVg{2f{Cp$;24=4c}VE(leSGgQm#+3O!)TDqbAk*VE6}og<29j;v z@_vbHqnS+^9`o584{a~E$DR%j>Xzk#Cr4DlFOAxYt%$D18Y?RPd@%j_tTdVe&m)pv zeyX+(M*t73X~to;3;1Fyx8RG3b`K;aXP2K+krL#~Wxx!%#q>Y(Qx-Jy(<+39@6KV{ z8bRNDLGoVza#o6*ySiCt;QlElYl$mO3*eM4J}pWj*u_N%#P)hf|1n^y7ue5sPN|Zw z!G~l|N5S;`o(UYurjhK59Pn*0+9D1*T;Uc#Xg0xq?6@u2@g zDukX*T9puiR=Pa@xufPYB`mL#9@5V=c&PohtMjxZ{*-uy+s728!+;E-Bljom&$#95 zhHmLU-D+kKtPWqdTrEp&tpiNo1*80X{5K2F|EfSPEw^zX(cId7B%#VHQhInz z5YtCfYeS<5RCtQUga1ryIZ(w&%Jnkd4)VA7eBgHz-7CvI4UpF+=6acj)u(L2Jt}%< zox$O;t=X^r+HTnkc>U{A#n%fS!M#Q69CO6Rn5jMxhtcPYicRs_pdAh^(rYjITBFmI z@KgeiDgp!Z+q-??tWQ_EcMi>~DA zrD7UoMH;4#3X_5L+JkvevLR^(9jsljdFrES6JB#WK#F;DC8@X(v|#Yi=v?*RV}vh2 zg2$(kV|G;|wZcc0FGK7W%i*(k0qyi9#ZGd)OFruD#&BvIz1akKj%1=CR>LpqI#%58 zM!;_CbF~Boz)zv=alxPqd)EHv%GSu;X=Hdhe^N9J)Mf7XcVNDkPnrFOII7~Cae~NJ z3t*D@L^SmUyeL0-z#s0bPWZmLE8CcRG3Tx%8clT-1i9;A&@so0J_NZje(zkO<@f=G zA3%GJFcBi5hbS8j!VkF*?#C1I+L9DKY=E+lLJ3evdA)G#=Vo0pf7b|A|W{l*OJ|MUJYJa8#JE~r$|by zu)0s~FJ|;pUQ19s0NlU(`}+`($#t_uu`|wURVXyhvOEtvqT-J8fZQ%}yOWpcO2Rn0 z+c`~PvON){-{vF2foPBvpSEK0Gm{b*XE@Rcw{;&l5koH=DC1#kNGq~hgJ9r20%S#A zBl#mo<>7q;!d^ON`1iCHf_4lcI?k5!-=MD~QoVipBb2-pl@usw=G!5*z^5bz8vA^JtYewGctp0x&t+FUY)vrSOPmssl@! zvJi*h>2_q2SnCQ*5CPckZu?WBb9FfBy4gVLq~x8lIgT$I^{}~N_2`1kgX6i+y#tcE zhO0j+D^v8mY^983!nQ2bgDA;#K$KuB%3A}4e@BO^rxL4Z$HGr>t(uk#djriwjCIPII z8*hcCJ~z^fH>O8`OwhA-w~a4GIju~9b@1u`%*NY~ry)3`0CSob4luW0RYkXaX@c!!dq zRY3u;vD)LoxBI}>x(wUjEJ%S(=qo6zCs zd)pS3M-bu3{WS{cGyDC3ZWQtiV)q`QMSBqp(J2VP7RJ^K|NL`B==D;KJci(Z7IghNO&2HkK@THLi{qN9V0p zux_=?XLEaxGda1DDs@9id1|8tg+E83*UnKU{;AZWyK;qvFJ5rdsG*}@wS(ZulJD+h zJi0jEYKG03Ud zV1K!h{qioHdRRa;LM`uTUAz}6p`|V}H05Ppk(+8)90`@9E%LhX#zK zH`9zxi=z4SXVyd1iyhv|!{lUtuD+d11p|{PVNFKYYvAvPFi$6`r^ki$Y)ETg7T@CA zl+vZrmPZ*nkwCFbXQfkF`fZ!KmB05CLe@0*U6Bts0%&hhkWz6%_vNG{s z?z#~tIM`@@T{OGV0;zBmD~rygkwuM^trfT6Yod6F(v+kdTVa0@qJ-vjX_YNuCg zK!yVkz{?8l>OZ3-HLpmUGB#ql0Dj8YldI^*al0Zm=2RcBUwjx>k);FVq6G8kKv*ol zD5nXDrc@o81um~B<|y)~(+|$gU;OhK5mKhOFqO2AxjPJz*d+6DKz=66yJ!}(cam)VPZ2@Rirx?d-6%}0iyirArvRfzUPhNI>`yQfYH5RD zU&`t}wqP2_`1k_AcNkAun%9?%DBL9Xbwk?wUg8|#9cv@R!xEM*yZ4Z)K+boxh#SYr;ha2C8Z&QPe~PeoPMK7)n8cpSK3lZ-vz) z@`6(Bq@|C)Y`2gfvKSL9$NK@zE33uBm)l}J4_9nQaf^gQm&>HgQ0Y29JiA26+NE%7 z`Z=rOZLaTGF?_~cMQ-`kV~*w?Lc4!uK(_F^CEi_ed&b5RQT(D4?Qi{>8T5m7*u^IL zub|V@)At}tqjn1_`X;K&#n{OOF)4qvfgUPSeJ}iI!cEdfmlAsoNg;B5f*3^Vm@QC! znreJ6tWaZqlgGF2GXZx{2Qr&E@8VY3RB(Q8CYaXrQF{u660O#UGY0DudhH3k{4Efb zs~y1kB6L-J&3sm1iZMBiDA3p~hgpl28S=)Bi-KK?gJ(r9&!C}Rrls?&f=<^_d=RmO zw7K^at>Icw(e4gAK7B4_!oN|tX}h$lVR@*h%-2z359gD3NCK$N zXgJnhaw^m>a1iDQw-GdNU|we|?W$;OFN%Yuz8LTtrF5Y)DfMJHGg?DkvCCWiwl9e2 zI1Q|GS?9T!7<8@}imn8$I_1-7v38ZA+XezAZ6wFpPxrEIk>q_|uGi}D^KG#K+W%(nbE`z%yhT@Rx@R&l+h5A6YUP zas1OYAi56ah+!13v|%ePlA{p~fwml)C<33kE-=iN_a@L7An&KM{xi@s5E+H?NacAS zK5C|YweTd~?1FH2K4%F~-T7W!v>wsqe47(V^1XuD#;V9w1SZ6%yIT*IEj{vTxmJaA zkw|1_k(@BLCpVsqj(JrUa_;t+DA!a)! z<)0-hN+c>V7daKd(hcI$7oP6=Q7;s{n#l$K+xB{^DN`O^ zi3Fm>$$Ae+n9cwS=yUR|h7%UHwHH(p6D`Oupz<&OItSs}yZ#Ii)Er6s>*a-WcMouS z2J3eD|19GZA*2Z`C8UQxMdCc#y9LM-lx`%`e*Z#q*$2%Q>KYpK@XwtGVyFEKIVc1q z^^H9C>u~{Luh1Dv75p5H7Jvz&!v=%X zR{cUsLP>Fq1G>=imB_-ceolSf6cb4k88^!|tCOhY`RHg+;!ffmR(n6M@kK^L^;<9J zSR}Z)mgY{tkB>Zc_a|i3g?0*l)kAC5A3uFsSa}R_3g2j+`}QB=Bej(XZ7h$|9bzi= z4Ni4vd`e%#BDgs-i_<)6j-JD{fFI1rYkj!ml*_?Cw8eMUoS=;96u*R> z8FUTWKhc^RZ?vj99u?T-G1EkJA`GQt{^~NKlfW`e1SBAl{$9PG{kR0GyuP8M-?Rh@ z(zaaz|0Sv~!tg@rXWEhlYLL=ohu2yy{8p+)g0|iQ=_I1c=?~g!8C?T9*#qBh*bvIKm!agi zfFa(OB?Ad6D8ZDJ5zUY6snhJLp~P4mBm-zYaT7c`Y2~xHb+$d$r%K(I^Nc8N6kapJ z@a&Z?sycuw2{Vy+L6Lm`)N1;XeM;|Y@FNq}7CN0RVH_~DE__tkB7jWB;xCIIrj1>xekeHqv|NjE|5@U-;hq zO_`qN>r-r^`KmIlitt-z)Xa4PsS~9y1$F{Nn=hv4@sDY{DM^Ph zfU0G!50vSKY!WRy9rbDOeqa?h=kQBJ)Qy4NtJy30JzRp*=z2*f+>YX;Oe}KJO?c5K zz19`;M|iM<4Y@RMZBtRG9bqOo%jedj4-=69RXM|D-r~1>0Rb1bk|N+agEBBji^lh5 zBf4)wijXpHR~cYHoELB`8_g6x0!OX?q2aLpcGp0no;mQgeqk>zrvra@$s^DIB_WMo zOA&>GxN&ckeXMp*B`i1G(Fp3VU|26(-3W=YZ-o$2-e7&V(f7o4OU5$|?i|FZIN?%m z&p=kB9}jH)ongkU+ZU$2&>n*y_OuS$6;XErWwZ5fVi_K}nYsipdB4cG?u$fZb}{Q>A5YPxU)nbGLTP=z5l3l(e6h z;+U=`qBVrcg>0|T0~PhvlE&|Zs7MsZ1PU;$YSBw;snn7&MMeX|z6#b%KHB1M*$2om zI|i2Q<9C&B^@mugpa#B=|n&uc8Mb;Gc3c6Tcp0cThXAcc;;a6 zrI{hy=K^lu!q+pr{JXom!wKW<#a&4H*ckJlTWqR#?cdka!&6=#vj;E}etzBLb1Q*DQl;q;SyO3C(zMDhUKk@{**%f{8!#|7beL zuu9vu4QIPnO`7Z`+qSLAwr$(ZWMgH!rpcO&$;QOVP5$oZ+qUVQ73EX42IWc<=!EV4jHO@*A~vaUi^Xzu^UI6Re4(%pc=Z5>=4&BqzKs zLW=k%QEgjGgmZaf+OXV@}m7rN>N&g<}g0c@E%s^1w2jfne{_jFhIq$8CTQXl}{i?1U%+go< zSN%>pWdB*Mhbr)Ey57Tiha(%1SG28@WAbnb0^3n8A z<#jT1aVvqU3*J+B0VqTdOuGjVD`*+0_{`4q%JJIoj04b?tuK_QO!8M_6ZAl?T>vrd zE7b2-HS;<1S(=BVCWR~#j3e4L9mHLR3DSn&aLM&r9vbwJTYDJ{-#G24RyF8t2eGL@ zCcR&%i26hR0TiPY743isGW1;Rmcxa>D6b}*rX)*G#gc}2l5mb*MGO0uE$&SemCaWdJY{|nDT`>&WD3<;gccM1l+3r?Pn3r0bOKskG7O^B&)9}CmR zSt>v>AA<-80vH(mz$Cy+`uDV&GC!i3ZkU+OWAULmcL0jE#ks);oOCpO$e5 zueUw|+85G3VfZj!8&}PQF^H(0U#Qfu<=4_kD5TJSijPpKFOm;qG!wF(LN>VuL(RMk z3~5W$=EIoOIHT!NwvX9HbexW%*-FD;!c&6evJS{$xw!pD={?|s9DZE;3ks-obCDL~jf;Bmb@wObR9><<>^`3QI zqP1C;xG#(U6i5U_4_hyPH2U7#YGvPLV=CodZ@-NUcXoCH)No=JW06F>1g%=bubZWr zSzJcomK$C3wuy?hTzSj9TKFixNjCRc8TmBh-Ac$+x8>lj+HcbY?I z{79A|QCKlcd#V^KTk~~kL>;3+;nE)_8H#}^cp?;ds~xyniN??VE?m~erH3iv`b?sza24;YRGy~jEz$<4V# zg*dK{(m&nf-v31%;Ov94n8GBd=YV2WMRV7!1)-;nQ&->}L}`psH=(ykQ(ugRGZ#rL z5E$Zc(N`KT0a*;^AAEQe!m5CR%zrr66}*SKXrzDZ6%rYd<9&GJ>+Y84NxKhPLQT6U z^y8|{M|8|%OGxh>*{p#LXO^@ykgB4*7{P|E6t%1nwW|K;fm^ErhVRAWjUb`F&2isuWwKrh_Zrdo;7a&MOhD4MYPlC zaX*=FYGR8{3HtfcWdC$i6$#r@I!oOv63fY0#b|QBf5dm8qB>x^)_)VCt)4~d3>_nR zNg9?PwGVcArJeAnopr%j?q|ecAUg<$*zs^BiA9e2Jxeo2+{471trJAxF{7$V7@xXd z#Ffe1c0~URxY92d7B4Tj*bRnII5mz_3r+8TpFJJoGqFv77w78Eq>$Ba8xW2yO_S5POp%a~_Xy8^*&Fgq2K@veG@h7F3;lk6JMB9UOL>Q zaZLzaePtMHPhwUN*@?ujKXt+eY#aG56;-vQ9rDT?Mb%JlBmE{Z>RG;ZjBT#zO>D9Z{ZMwo4oXB9%ubONX{pARx_{dxzMXr-wj{dkKt&cOdqac5&Sx)&{ z*1^Z)q!%WQ;|PV^RA={`N7nI-_Z7=0*WkzuDxfX8{~ci-7gw`+Y^qjmu3yYP8CRV* zX`yderDX5KLQk@^YeGXo52s?mg^&4>1n!q?V>sgi^TOu0HCk&cCKe3*jc?OJ8F3l^ zn*~5Ll(WA!&6A6z5E#C6UY0nj69#&UgANl^)d}Afn4$>AeTtLKxiuE0*yL5iB8RCG z=2F7}k`dxqH>cw~;8lxsl{kg829Ol8vV*{MOs#XUnGi^H+HQ0-wi~=n3RoKeRpO>t zgwV#0I+#;Gg;6!fQ23=A{SsWhEz~>acM{~CvW;HZ&ZMM|JmRCU=*GvmmjvQ2UazQVo zr{)36-Lh1>f>vhEe78+DM<^k(HILtWBfa=I1R!;JS7{LR#B%i^wH$)5gop^<%R6< z48_YOhT&pijzSU;OeT>xLSbO7nsF2crV^mVFGe2TejARQQz0y@g;?e)SycD?jx@%z{?gg@K)wb9#@YmqD2WR(L+>b6)NZJI^eR#UFr-K zc6Cd^>_Uw-<&Rta+0x_07igd<>OsKT$el1;E#ijfPcB+ngc=`M`P>1p^YlV~{e%wh za=CZp0NP4w5|-c0Pi*9)r?`@mqbMY==+!+VY2?xvyftXeui-3_F2CA5knuhuh^52O zFVIq!@TaUgXmlrMx>i{eE@9X5Z_ibs`EoPZ`G)FKdi^7cXhh}MOx}1kF4E1;i9SEU zQLE17(a5^6axtV6`d)npTF6C2;(I6mf@_ThUA)6H-?^cV-5P@om7ATCs_^R5?;jNM zQ!=hiNGYr=a;~K~0c_vD6b!A{3tmAj_q)jw%a^0%j@HQUfW&u{-!-%qaS4Q1epMc3 z_KwBiYh{=ibqpr%?q@gV9&46&2dsp(_jM-RB4&ejtEgk~oB#EcnG;+J6)AslcBin~ zW^G0{+7__wDn zWR*ll)KdQxE|OC&)A;&paa7}ad4}M()$zNCTk}zulZ#>iC+guD^$~s^%es{qT0K5# zge{4|CQ2INCEDteW>T0?{3o=NnBIE$IK)5JOo(%K>Gm7}B+HjWT(0=Zta*?^jl1EC zm@*j)E!Ga?Fs-{5+%lA4RNOrlRpM4rp)ayw>VuB7VAQK7LQ@>hN!fD+a{|x7v}OHR zVUDMqCmD|__Zg9DGj1N*9~Xx&7RKDA%`*P5bZ}CaBtHd(JJ?3dNAw#An?&>+BHWXR z0UH6wG=^~)xdht7dYwh89_Lha`VXuN{I(8@?n<0+we?{kZ4hnknnA*w=t z2ZRzd3q)Z3L6s;M-iTA;VURG-{kkEl`ezFJwW!Gun=)AjRWfWw`57#GE%yP5n;kYq zZiV~s3GyNRoxD%tI4tctmyrUSvO)v}#51o3!wHze;g!r4=5lNuWFegg!9-EXdi>DL zGRIdRvc?#Yd64?JCA-18-S&$PwqccJZ2BpNmD$TGe2!q?@LNOg57lqEa%NaT-6?dj zRcRs|GN&5%g)!&;6bx82;L3x45a!c<&fH8!#^N-QP)J-sWsgHIG~avGDB%W`)5cBmj3OLox;_vp{m^1ajL@y?wXNBkkJ$HcoO)?(_x~26T++VsvyrE%{|C zN)t>N+@TDX>ba^RXd$^4kTEsXFVsSvXEmjZbpHg?wyGP)wys(UvR}zeETctR%=L(A zgy9PPy&syjYH>abMDST1KfjHQnWrD=69IpnBITjG`#HHw3~;GCX`Kw49}k_Qhq)QF@b>a3W9KMI*TgJR{Ud#s!L45}=laaq+&`ofnmgG z3Hv;#al7PN(*>4QhaP)1LJVi+*h$AvyM&vZG>(6WxM4iS<8LNiZHGD5)xe^RUy&ZO z?%&0^c_2Ivq-~w|PSJfU{FfGULmA@ERugdaVE}}P47ThRX#}-v?Hd|(lQVmFzk)$) z!rw?Y^`CJ4Nn039;ynb=7HlySNKK4Ffc*=qT6)+K&9TV;#(I5E@m1?~mB55Qh z>@XP!p3g+n1 zS>%|a7Ct)&g@!H!EC|)+8dK)lA`sAwGV94&@SSqX&w=yoI3H!ynk%$N?GtR+2w9tI z^H|MZ6V}(VsBZWky|KNL0qB|0ZZ|e^&COVMrWw`wfoz{%QPewPjU4oHi&c|RH!8MXPBMpzntt>5)o`m7^cz3_cDB9@6VV)Qq(9H`8A1kmTVj9X z{n9y+%hRzJ{lstz^Wt<^fV*I>tD|zAdp7b-%p)O`!FNb^b)y)iGHZks>FXi2{_$<8 z#6j#L^Pu8Ao(^TiUuqH+im^C@wbCa*B4>s=UcbkB4!{ZmHKQe9!2yK10SAW7nBgFR z6ot8m<>Q)9pEtA|QsKjiZZvdOhvJTewS%ln;0(6wDXr;2p|uS&<#w;Bd>2r);iM)m z^WL{c+f-4LQ*bGzwb7vIq&(KbmH`278md z*^r!dFYEdF`)7(s2(-P1+J7m-VrLNKbM2NYsPC#_Ln=f(-yz9Pv3ZcuwYO`?gXIha zRoNqG3>Q>2b7_+p%GLA#oC_I4meQnRO--Np^#{AO61mAfj9*yLrD4o^uvC8r?NnkO zUYCydi>)x$*H4=LM5q)eW^X0MSpt9LC3MCc_y#;QcgV3k!z8TNx46ur zV?-@?AjL|Uzv50k0XbL(EO1?2WLJD>Xecml$HBw<4nP`c7b}*>K5uwBs;$9@nM~SP zRn*4D#$|K3tkcIOxrvzD#>yZ_D2MN`F`nlIud*`=POkQTOZk0-`D+Y^IGIdXtQyZl zo65nMMg@#sTl9{C2@=PaB(8~u3z#o;sI~k&g-*dO$e9|gVQPt6AKcVII-e?rAkVvG zBl%-QtK~R_l&>!d?KTyC&9W4PVTW16$=mPeO%b!m5mKC{qfAm8v}@7#m3Hs{P6&45cM8ne6;oc2O#$)lfMbi-_~e z(xHIeq1@4hDuo}&q+29nZlbayrRT29slI5@UvlYVxtzuj#jAY!*Imu=qz2&bc6>MABl?hZE)^~GUtBSboxkB7W`_;jluKvzd|eOx?jQLR=~?+1TxvA zPMiJ7_Pg&>O;(?nrE#S&p1{48LM2`fUFu*47vwFax(#E0j+%PG4oyr~lP={F-q5LV z2Gl?s&Ri4fpkwe=Yr4A~>kOHR!7>@2CxtG>^lkPgM%5lNSJnov8Fx(pcQDbgY)UN4 zx+$sCYC;dxS{5L;0}60ITP^%M7oqvFtAj!Pqvg5v|C_z%wf+@BPY-1L$xc!tE^#i1v$Ra4z_f!*!NWAirVKIqS zoJw2~WtK%J?_tWQ>NbA6$@|b>bqE|t>Je~`QX`AY(M|-J`IP!#@o5%l!5*$O5i`?* z>AUdEbc$5sJsx9KiZzkhZSrcJ&OUYd<_*H?O2HBoS*7-vx|7r#6%IP@21gYbOh}ve z??%2qtm+M4NSu?YS3}^5n~dLIwT*nPnZLglOdS|6*JK$fW&Z=pBKuHc71Md}Xd4!f zf)C;6(E67^CKm`jFsHQIY;?E)g1<-pou9y}DTNNRAb=~dee;plMx_{QtfcJ}bum&X zD>iVJMuy6*jTokki}VIprWdYxr=vf8KGaFxB-VkhP}n`J(jz!J5eHVVyNFi}r{z3y zw&zFY>-8pt)dVCLI1N5d$`YNYA)l;lze%FLFfV3V3xJIj{UW|!y`hy+8o02jF9mCeb;Wm zxIMN)>i`uo9*Yfnl~nhK-ANU@!>8b`R9Bu9rPROm>1n~$9_is#l*z_jB@73}<~}hW zzH#!^gPOyYOETGW@mfjCl#Jm7v|)b)ka#pz0+zMc9QQLM(X1T>Zu8T3)kPa;BG1vg zl+GOgLCt*?N051w(-oh1!N`Iioe_TP0LDK6+kKc0yU2D`1RE=l;E$9N^cWGb(@Dr=3EC1}S0hpBluV4{SJHu8Icv{(4vR2cJ{cSEc zG}4stxA#Tqsob^qz#!O1Zcl@v!5SFBRXX>b+cyv*!pG|YxW$FvY6p-)kRu|aj$n5g z8TMwv4d5xESjw&p~+O^MDH&Tc(HDWuWCDi@}hj`v)RzP*;I zL(QzgYkoGuYgYdZ3yE=Yj)+Q84IMmfPcohRfd{)TouAj|F^0xbGc=)Kmss*H4S+u*(lIPgRJu{ zcjsPhB?U`JP}r=Iv7slzA|E-mzXg|1W$)et@lwSczC*xOBKK^~VYlVKe$iGS{Lu~| zW|Ww3Yq<^20BHaqRiuponl4FC-gmzj%J-Ty`vTt{0o%}W?YhW7+`$yL;fu-hqhdA( zKHweRx+_Q|W`BG6>m1tj7EBDFYOPUTfxUN&?dEe$v$W~@f7W%k7(uqgMxbJ#%-C*2v?KV0@B=STvt_LJBfZ2eG)xy;9AEhfJUK8%Tf@|c|B-subK zdmx#@N=UZz_CXDRQYl?Tfhq#ZgLaFN6dIBa5YdZ~05XcJy2t#0RHS%1SMUamLVZn05cfeKz4 znmC;y+H}>U@QYqw{k(zzY!j^aY^rA`e`oK?wkBqO$(+DE11l20fyZQN5RL>xRdX0* zG^DO3Ev60iG+T00)mM3S5D^f%RLhJEFfX6-p6jJGFMZKjPW9=j140M1u|=tPTQ?V~ z79wGJF}QSdt3ovUac3yJ^>Zdtf1pC9fN0P(KOlDlL;BjSk0;lmAPTR4>^n*f&t&w&f4+1^ zGiJTT=62ob_6|zB)}Rwu0ReUMRg!yvbl>-CI^Q^&cNO&gqZNQFhkR^wb2ZN218vE- z*M+2)%k~eUmrLQ>-$p$r6-rM?J_*PluH%3EnEn55wx{ItoE)eF!c>Mx3jJ%KK#}i1 zVKe4rGFiUbkScHhWr zBhSura0x}Ns(M~#ryQoz90khH0irZ2s-{OqR8zN~KWwtC-BCX_bu+jykx(`yHbiCE zk;h3KGzdlSDPE_scBH+C$7aeswkI&b!Z)%<^>Yf&C;c3561BU(_SBJPm0GDtY6QVB zhH;)j#@va_O2L@3vFVvZUkUiaTnWx_;a@K*ucc?Ww+SDY{NqBpQ&>VTbBO)@w;Gg)4A!M88P+gV>K;zgueV*4%;#D{k`)vsWTzEdd>1JVxrlH) z6NTc>SH_J@>n-I-+{oi{wj|AAM(3&Ifp3~i1B&iG0F)3Q=n+frGWo6c8>3+jUWelw z-|P1S$zt7XXS7j09AQ+0zv`QN>dt9avy+=f9Ofcetz?)=c9e-94z-Syf6@b7v9J{drB zHU-qS!n4sKUmJmA5(tXG$R_*gQGk?7RKbP%d4OvVlWyk!)NMOC0f#_Oyvmj(=N|g0x!{Pnw(aaGjbZVA~$1vn$`y&cLPx*Yo4?zQW&6VV1S|z(U1*zSC=b92mO$3 z63+YAr>g>c5AXN{?VruHFwN~737AlC2FFy$WS}yWBl3^z79re3Oh-SrTIX}@!=8j3F{k^>UJHK#sk>Uff<&gAAR+}##b zR|aVkw~azFzW@Q>#gZfej+>3!yvJwg$L-_t^TkSRbLeM78~-}Zp)jEknzH6J(q!)| zq0h-?U^Hxx5?W6RUM|{PYKhoLbqmacvex}b8Y#0xJtuicc~5aC$+K0|b+ zl0(A3V^Fj#Q)s$&VC!mRoE0sKnW2QQHsNda_@ah5j-nuBn~s1-S?TFR$&O5-Ox@j! zEqbk<3%hMbB_wf@Sc+k>0v>3A^*JSfA6;i8diB%H#jz1QjCIkYa;ck{f05%Us!`EU zBcQXbg42=N+EnxP$x&OTYhq^UMXsBTjA3p*VFLDbkVrKSR?N^Zo0a{>#rQ%IX11jM% zKVHYqY=g<5iQnI5ppYYfA?SM-1*Wll*$I10hGHQ{9%{Yq+7V+efsMO~DN<)y`9cp!nKM19#&n?GcV7k5;%Sc}g( zg6ccJ$=^s4(8h@dasjLX?3h^q zB4=dmhHUefaA+VD5V6K;RjdbI%96`qSl-52M#dFJ;c(65Y(uvt`xpjrmWJ+PC`;Zv z%3(2wIRf#LX#k%WM!(z{Q!t9k9ko;*ewGQvIvo90VPXYz)>m7@u>2D~OsPicP@-ut zg{mgPL#DIUsqLKXMt50NL`|4RY?ZqT9+;0Ref?HxVp#?M?#EM~a{9(Se)GShDh^y5 zCv5Hd(pUkdy`z+jvP;Y~AtL0a$VAz_AX=Q!M5wQ_q}FH+CA>%OtoCH+DIV>ix>2zuE zsGr%KE;oohH!3yHmdET~-(Ln;&4dW}J%0gGPP^4A&^&9B(5tY9$K&QB;}-`#RaI8s z#V9~p)jg?LW7PMVE)_s6{O2S&ik*@s^5M9l{PgCjKE^YXMg#aB0DGO>pM^Gn zeT9t2D6(s$`0o1stCzzfm{kG_&c-cAztp9_l7pTS-GU`uioDI_;CbH7#+x=yNhO}kjLVtjbo`eLsS}VQJ}F{m3kbmuXZ4a}B3yil^U_j_Zd0Y58Pa%9SNPqDdlJjeDSKh(dm+Q80EnbrJ%`F6(1fn9B;bSQF1QD#xg8k_e(cB>dkjfNm+A;* zYVU_s87CGc&&QJBPGKUphRp96x+b+&nKw`7&c2MYeA!6-Maj8?cptySV!kaboVO60 zu4G}4DPaSbB%Dpe*QCy5>i%n84fZdoV8$^Uwbb!XszP6#`FL4vL)d<@Ui0KBX*lgu z5ANq`QWiflK3BckaK71BcXMvN)UgNLU{kYYcuw2 zL&DZ%Ehob!WJqqBg3f>-Z&hW`=`_`y8Jd6)_3BWK{gm*Q z%i+bF+)hc^U8F@q8h0-B^?q}VteXtD4|zflU=Z;U|8D{Yg&8{$^ogNc5}0rWPpyzG z#Ve$cI$FW6-~fs+nRn-H=DkTBHu{!rHjPBO$}&k)o$DGtB=s|QQL%=OKgYxk0qWGE z(G{PYk~M5kSrDFypqlu`&r>ZWz6`N+uELfb=sh`x*yYh)A{X_x_h)fqM3-BVA{x&J zn4Fv!ABj;^d|cl%Nwo<;bSCFI`Z%eYPgclVrQhsu(QMvGpv(U96j2c(OUpf=x-(xJ zf!7NX)vSDbyI|DnS7*3lPGTT2$(Rg2i!{AF7V-2DWn2)dHKlc77Qy%>!i|f0^*~h2 zCvry;1nLz}zKz485=L-$I2k4TqOH#VRD;^o(H?iOYfC-yiID%fA5iTM-~I+*+ZM#|6xmlK|Xt|B50JX~GeFDce#iMTwuW zNA$ImE(_W5d%QyZ@2x}5Q=s01FN7=NwFj51>L`UF1ap=qE5XC|X5z|3C)NIi>d+t7 z*pj=0R^8?x3K$U=Dm4@SFmGdS5isYrMVxCQ1dwiIR|4Ms!w}Bgc(lnq$_5-J)+KRt z%CuL=EfVA&vyJ>ns2x7BG}H(yMI|R6p*VF2GcsHyk1-+`&{|lsaLxf@;yigOz4F7e zWhlcen_KutNTj8up*8t9+;LjU^%&)R`DswiMOzXPhdRm~&NR>uy^CmQk{;l@9|7Dq z=wSqojJJ7Q+0V@Cd0(pp;|EV%353y_@BKv@rR<4}1T_hmKHJ=AIE|Joo z@TmGjR?KGldwm~8$@jN{wwKE6egoJqU<~5${<@uf^Dkg`pl-A4E8s)|%w+5nbK7sT zKAKR?oH>mLoImB)bW2k(($|dfL8-NENJujawdGvF(A>bP(rCaY3uhA|oju~V;M`tb zRrjTw7f*2oz2j9>hQ{kT!qtJ9$@dPof%R?X>^|oyq?&!DJm2%B<_}4~ z&jxFSWJ4@P{&Qy*eS%t@57J;J2HQq?*(eHvdmcPf7zq!L`+wK_eSjVd82xhEE`p_# zK8r`8T`W|fFTy)pHVzgXd+zm*4dP^eq(<8OI>IM5D`HH8O+&XK)dUxtjr`{xD9lh5-t8 zq6dr2qW<(eGj-n3?#TLQy5hH^=wbHKBOaJ9vWa1I)5?kw>f$KlE*NEmRe94EgD$0? zF=Y+9`a-_5LY-)-vDSrW2Spcw?r4%i^N*Z)G5azI4skQwPjdeoQ~@)Mr;B!V*XE-!!+(Uo;N$it}m=p$g~$3^?L$)ARRSJmCN zG7&I^WXM4nPsVSOw4`&Xy{JP4HkR&PYr_G+@@=Dwd1`o|akw z`kV+H-{SBWdSyjIuT?)~r@E^2f%d<%4jW@ZT#WRQ=Oveps&qNG1^2>+RIuON`Px>0 zTHRbFTx=FYr3bdIOeYDy{{tRu{-XAeUH~>pscdLl=>9e^<@?-&NV?^g(H09`Um^W1m4{dsPyuN5 zErFlg`8*gJqCt_Q0e<^Rf4WtCLSKACs<*l*5xrsX^Q~Ls)0;b(PmotJbxn6}2{7Pj z1Ycry(Fc1YTSvi88Rlry33NptX@_7X8%=>WThVtGo9M)qFbcZt#=y7s ze&MeK9_bdp%`iR*XfWwFs8m^r@dk6YstWgT!2s*-=()`t)%d+qg_~a4PUzgAT`FKg z{_pv$vcBha08n?kCrux2?dc@bBQ%OV8GsIDp+_hrD|zZ@Q^w?nFc4{%0Jr?l)XnG# zuP_OKi{9ECmiNng6_ptPleVn~zBJG9TD`wMB?-J(ual^sL7=;EAL@ z!;7!2bPy9`5Dfqudafi43=9CJ<8R-;?qQ&}7X5>evzb&f=ynv&bx-Au8o5GWr#&gGZ4ZS|^K}vCVu*~-Ji0NI z8(X)FJFM7qT)*sEO;%eS`5Kb63T1+fbo1>X)7>KJ1uIZF*RNmVFsyxH|kn< z${yE-NvJo{*I=d|2 z7A$Rm+5`|)z5~YKfTf9yi?#Lwp6ll8g1{zVdvVc5=)A}}nvtIV`h3P*BJ3Y@y(bAs zNl9G(spj!{0jQs)YUVy3r>%|4O+RC~bhiDdllmVwd-oqborS>wBlB`3$k6Mz;A4#D z?a7H9(4C^8>K2tsHZ3vd7V!VUrzrJr%V<3IXTD8hhkMat)+X2dZ z&gm@B+MrgqIg*m+l%YGM!|&e-^6we}ug5p^KkPpdJ73?dK4-tO8v4JzY-H}t7W&WS z$XM-2x}2GOdH9FHOwh7wb-^7p5U1EDNPtcBkjZ8d>*jI=SaSi8iT5>t98t*SX{@TU z1)!qvM9vTg@NC5_ccvi>G!*H?I5ODtKheTfK*w21z|Vz#g*4!YPcO;5f7F7t@(7wJ zB`!CGws(iLs)I*5Y;Hh|hG^`BKzYeIa8?OthvhfcW{@n<*id%@NldO5{a^LI6_MxYp!KqhD$KO&dMX_R4dRR!DsLMDomHsj-Kj7x`;cpl>4s&Wuj z)tiN@58xRjUATZTDSXmED*Z}$usd+vk)ou2OF{z-mjgwzOFVM8s(;tu-_C*4lmy?h z?LKsWbiS6%ai+ZhsJnOo6h_HRz-7x}GEk9u{r0$339jjQov4OK$M5oAWm5p?VC%bm zeXp++NV!}da_yI!y|FhfRq=H~$8v!qQ(%^5MAwj)8>Ev6lK7Amim~Tu9A(=h0SGQ& zO!7|?H6(sD;ZHxP2w*Pg0>C%`1hAJ>-FF~`iiC`0$ba}@;0fKRnR7rLQBHM@PsFED z^E%&YJ$p%dTwFSca9rEu^W+_Ek^tP9c=6EI|MUX7b=*bda0*$$PN?|IER?!7ePm51 z7TyrmP_>YsAqHsf6)aWfMH~nHsqisKMqW80jtYn^dPvUIEe6$^9hSQT8C8baXcHh& z0g=J-fljIXN8$A3yK{n{{$UykW!c@4_-^2}QdD?lausVPX-FM4jN&%H;vp${_p)~( zcehFH#Aw0lLKlJ|ejnCCM7E-8v@A_@#hWFMl=w4ja3$Q+YgQ|}Je^|@Mi*LsX|!BR zqkw}F+51KbC5pU#-i ze%A+Gy_eofK3)o5Z3ER(|Bub~{}7(0!#BXK-gM-tZ;JoVj<`P6Y^A!DOpmD7mPI>o zMT}G!1F~{_pez2y}Q3E*?04-(FkYT*j99MNv4bCV_u_grYB17!n~~dP_Bv5mZmA zcAbUf6lo-^R*lZCL5st>R+mn@Bk=))cW`I8rru{$*MTLw=v+fT_n_)j+lht{E8&hFYRy8XIald!S+%J_(0^NtPQId>r)14_#TnJe4qrV z!}l~7jkA^OpPeWOsjw!aF0hivvO9x9*RA}X}z$h_RO`>q#c5Ei^t5JAwmalu6HpY zc1O20bUR5oOUVKV+P zEeyd4<7&drd{b;e(DM_}LFaopcLQ9_sI8^q?>m<>TZH^C@I5Q-kDg)OPN=Ml;5wPL zt(t~K)EY9*-K?0qb3;Y~--`Y;=f*-1_0k)y`UsgH-J|YL)pBnrlhq&dU%d zrVDUAUk^L2KMHgaeDm5yk)3jrw)MaXdAxG2GcyADPk?l8cqFci!L57K)*DCX>1It| zy~ktTB;u#v6CT#c$e#$ku?0i7OTH=c{qHv>g`fbADA$%Ndx={%Ib&%z41JBvy(IeV ze+z@O>=U;B;tVKER%p)ZW%`5ZR#4G^Nx}dX zt1Oianq^P6n>okBme&VfzF)Ss_T%M}6##l(==(r318hs-8j=>0l7Pgd@BNMS02pYv z%AGCujsnZEs_N>X%=foY*jr*7fUJHt+qnNnmE`X1cb`ki2=2L*9!Z|)B@N2_dE}?l zBzPG2|IGqI+RFt{x*@p^48j2i%}hmyiBm)!>k=Kujs1U@i&a72!C~O5*&iTcC2y_( z;8?dp@%;U{dZEtu{qNVs;tRM*#BM-^FBFunQdC({ScP&!*q{H`9%`FdE&AmT0Mrl@ zB74m=ekeaG|8z?{={~mCPw5u$89AX-C|(}PnJ(AAFAZ%s_S^WKOa^Jat0M)Aihz%2 zr7(U;oA-)4$Pwvkn_ku)j8>>b%GLALB0x+b284ye;A}u6v7HR59U1-=#XV-rQ>}6; zeJ|@TuY3eFr~HHEk&vn*iYoV>tkq4noo~DeB7m-+TN-0vJrevIynraeh83Qk=$KtD zLj_&ls`1u4rf#Q%^aVlD{F7e}m3(oOWkc0 z^g|`HDKw`D3I6YCmMg3tLK)_@BflleFD10ruKU~ znCIar_a?EcmK`gMpqHi`(A!ra$6$vC)%uTN2ZP(Nc8O@L3T|;JvayJbZgN7+i|XD zd6%AiB!~gJEw$W7!-GZ4VMiCHN<+&vFd(;6ikQpy>%372QCi8YLT@ccD&%Zo#OQa;F`(W&Q43d6g5EeWa2l z;7&b~DQBI+?MEH$JU=Bk?`j^{9DGm1V{|rDWx%D`%3~qU9HU=aiEW&%3jHc!d6UXJ zbsXnx23nx-VR1Ek_xRnL>`chO$mrf)b`D(K{vCt-hEzvohJgiu>wyp6KfAe3H@w_7 z$L==>guXAZ^XG6`oF(Gi5aj=eYS@pgMjWH(?UV3k*7N8*P2GkZDV+h@a21aKhZJ@b?vo5Mcr8EL-QJea^QWR zMDlUs*!@>mN2jpuB4J%k1c(y8T7PLE{Bc3H@g`jvBixIOd#<7CAOONe>^70J&Y*YI z3V}}yhRCSG^R}O$LF;Z5AH6jDcumUOY>)`!8qAHmDJx0Ci`#bP({hR1YJJ~k?n2q9Hf0nB87D1GH>bG?qYBN&1DHO3G(^p9QvCh0AfrmadmVgXWutZ=` zQ+^{*f@{%j4+(;&0$b8m9KUt>3)90hG~FjBEQy^V6KxLSx)y|GMa`f+5mM0qIPG8dl`QE$F&z=mgtGcT7hWPuB)Of}wCg{h@qkg&kD^;C|NS4=? zCQA%+iC-#s-Fw`JFgj} z*qT<^?FR$4|T}hsoe=_9E+EL zOYLr4OAJG`!J7~y30sIix#f)Nv7InYjNXk6!zzTy-qeSMi&X4?WmQ*Sb1f_LK2B>6 z&5MYvg(NL@fmqLj*H%FNa>LA48;;3LbkZ6`1M7Xs6A>ezS6=)^ZZVEu&<(jP9>rX5 z!h~sHOo^aENG9F;SxI6;-~sixIQtg<$0w@ccaIfJ*8U4FQO?*d{2_<7#ZjY@gTzGdbc%F$H%NE4Al=;^lG4)BDInbq(jeU}-Oab}cYMz;_yY{C zGxl0@%{7n3Kt!`uRd%MIy(^3aSI^A^&AhEb0wru^Dv9I`y2{r*D~Tsa6*CB(H2s_L zJMR>Uu0jg%?%r8%H+84fszeU&XRv(Fwxxp@5aYs0Xgr2{ijm}=er>%gjo;2ZxyI>3h%g27-T-JctL4VZtD*&f zW#8-#Kl?Qys5H8$a@Igl&@$Mm?uLV^1@0`_BOl zfdK6@_^HJ?f`IhXQgUg5LrAeXp3;Ccg|yR*!u#Lc&qCgskX8~?p%asu> zt3V~C`m1)6_@!e^q40!W?c814UDBc7sv0&$uM$0gJMF4 zOc1~NaOL)9gZA^uwqB7ECMRM|+c%k&0}k-)LhwYCe1EbA$Tw@@Dl^W9Ae4|~sQ@BC z%4Y^Pt6$Q}E)*Qqs}4<(>b!ilbRl*yW!-YYZg($GSUAvR0|{Eyn0QC{2`mTy&yxDmueC5TF{W3oRnugf}xf?dUe zi*QwsPH%(75^wMnX@(JVxs5c=6RX%^+=`I>VHw1zk)0hWF+psHp*DKVHhj*i%IL(h zAbQ_x*l+_1>D%6l?>e46K!`+vU6GG(a^(ESt%mHL1~UeMyZ4TqxM=PlHz}*_H^2Ac zW~TVK7+sL|&EXi`>(O#NeI8`%=Z5RB?>CSgXq~(NqmlgJ^*z#ysDR_38hyxQ#@5{r zlFO$c40$jiMQ@tzLmp?qHIV_cRUqy}+viXJL~R?$#LwaEGx;0^x2j}>k!8m0;?^5U zWKcoMWp@zPk@>`Pgh$>2SxUUhJg=!#TWV~HL>W$qD6162D2uek4=*oQ`XVikgS;B} z{;ee2ew$*&9Oa$|ZhpPsB8vr15>jvCGdfE^O!I;%0pt|HxEM5YTFh0A8Ej9IFS%9G z>P6wl&INiI>B>v~(uWACKzu|4$os#@`&QAp0oXDj+3)4uvclg#KT%*l2h&Ccv549u z|COae2pD-48iGh4xZ-+x-Yeju_y_lu1#J#DXC$)zsU(VW0Bc*i<+7LChkl`#Nw`d~ zNEsTTEyv#m7J?49BGFcP-3g+Go@0*Zaw_jcPAj!>P-@B)?nl%p{Iz}`a8BWxA@@a z8@*aL26giVhyE?WzsI`gA(Tz;lV06f zF=uYr^kj5;;W*U*oBr6vZBeY*mPb@2f^nRw)Hjn>sLbcNNjQin0Wt~8T=4|1{Q+!5 zq&V@%A5EB$J*BJ~33PLiAY(@T8{F3pjGakTOlndcm);cnV7vSHPc6nnlJOSH(FNjM z3~ATo+p%_qvzMqsuG>|TL0HK;bXqx|m6$It)V~@SEw`2{vpMv|_;D)1A;y!z%9)#d zO?Df;5e$)mxIvxCgJW&&3B|q$ZF2p*I!dNWJsXBtk*tQNTO)xv^1aks>q)4OV}?y6 z2$dM4hx>YwP7^WkfQ%25eo1NxZQxLT6S~|lflO$MtCFUzqo*t5w%FM9{Ed!8}xy2;)x=Dsw)O zGSdpgK7kAh0!u)>gIK8Def_`tJRs>xUwDq0aj6?FPx7Awkdd27t# z?WuMGjgkECSXRI)p-H>R|D_1DWIId@K3o9{ZS!ExFPn~KY#yH1lSn7!8?yCCvhe%!iM4G_V zi7|rl%cU{#9-It=TTv)-AUxMjRAUGaX&tMCi^_FxMT`p!1b1R1F2LOIjQ{?h7LZ+5 zgAyKtR^u;fsI-mM&q11@P6P)RXJuB8zyjhlb($l@s~qG(g&OIE7UX%eYN<|<*eX}2pY4yzWE>-NBx5&v$|8jrL}N=?9DY}?s^@Ak8h zv_(W!4SYNYJ^4lyB0(P0^1a(sGvR**ZR*f|4}5mQD#qf}cod&xk?^!~N}P&{+V%nbh~WwD)Nl$_7X28+1ItYF>S zp#l*AZTM>je{kwPVeRbZvvdq;sGduQ3?nDGvIvpdXqR%|GQ8a?JnlP3j86J_F(QcC zV;LEpOrr(|F?DVn`TfW;6CTcAIbUCOkOC$eaS{S$54*P}QAstz?GS=f%ff$taG|iX z;d;jJj#EJf{8HJ;BI(HQB%h`{&S9ARdOzCe2jGc)(o&yr(Z(u{;Z>=Y%-z@_e+dp+ z`l;jW=(nkkI)3u-|Ma^&0R7_6 zw;-l55i-IwvF!M0w7Q_}tTz$FVX4}C(%alHtiMem5#meL9X>K&iBw>$dW)PB<|QIN z>pLq|^n}AlxP6ZLW1J81Gn9z6Pnh6ZIr6~!LZ@LM-d zJX{U3*-6-E6`NW|DX#jRVwI6EiMimW*vhAsL_gUUHACW*l=?hNGxQqTsJtJrr)7+a zG^ih^VhTA0^Vln{hK=U7IWp+-{6lG$b&M;EK6@fy1pl}$g@gwCM1#l8O>1ZzW z?@xc(DfjV+u>Fj5q2DA@!0ia#^~SHR;@HUTSi0yJYlNO z!)1fj6cnSp*J<;<^<0@N09*0>24J}n5a;Q##{gJyn?0d-E6(VB@&M0Ps^7|)MUsD< ztFX;3GT?V9@~hk(12!*V z4a+%w1Q|U_@oUJ~7qT6yVNy7g4ev-^QpDK}y*W5ckH*I%==BDf)IatzePakhi?fC6 zpS91VOd0!0RqG5p{@acPU9sh(_K<2i#TE@JvweYgWzO^Xorxp_22H?R2y7(Xr|V2#6P)| zB_o2cadt_IFp@ALxw4hrr3Djni_JvXNg=Hdq^ls(`_Y$}IllfbPc$l0K-IP)_9!Dh zrKlYlG~F3l;Yk;uThcllrpYH7Db!=&*iqyt-Cdzen@f_aQ+l<~>k6rn_H^tiWg^PK zmdoQmv9y`WBYG$Al>j-3$~Qa=Nhi+XXQ*StwFDL+z}^Z#m;sPt4R98MfSFLaZ_~w} zzX{%gXt){}QsDM69rOd)(pT^flz2QRsG{e8Kt$dTucK0MAEbWNtTO5jx)oJ%MNe5# zBQ$Hlz0iYB8g^EKNz=E>7P6KiEFStxpG|R=DlI}R+!aFKIY|Ih5|z&lMQil^)B(%a zNi%4kc2!nw0GHz+_v?gTSE`6^K{Kyj7fe!ZX-e#>GK=*4 zMNRCSiqichqNaOh_x#F1?lB!I>;aM9I9wWxugqu=A6#Zq4VoaHS>nz!NOP#x_{T6I zRk%advXJDVxeLQ&wu(%)t|7u*iyQKqx(JK}ny7_Qv3Vv_IO;J!3|ylSd-^hm=A!=L z??GFd%-p2ZTpDMN$oI4olo)_jJ@q>X!>5-~l2CyS)`^4&x5H2tGUODb31sWq zz}Owf*%~#&AaHk~n2#)wH0g?XO`RMDkHhupYAe}w3^AYEF<4!g9JHIS&<1!T z76DsnPvf^3$FE zZ(z8mR;5RvB9P7Fya&KEPn+Q+V9s+wdp6w@iVS{y0#J`k9=PRD;ywuQeqE>m98PrP zGGG`!$lC4)NRV4F{{s&h&*vZfian~uiZ5WY2jB;b0M`zj+d9APPDaEP876$6ZnL=^ z^8sd6yUq;L>^rUU7D#AbYIdd(g#dxS1mDRFj21<$Xeff9fNPu4pcD8cK$x9ggEcNS zF#m3*Xc{2mp^>=-+kOSV93Y2uYE43a(VBt>zCh}mpu+~lyMBSY71cb^P}hI+V99zbd)HkZl7_Pry+FU;&ArbQ_86n#k;EAq+vUM3qnRn=l*I_r;yL0wu`_O+V-3_th*9Y4?s?&h76-RJ528awnB`i% zC5L{cjP^*IXupA&?#KvDL;E{63>a2B)2hHOQ5*1QdjR2?|@eOv)&t$eoYw0GSWgxEDsJ<2c)a|uZvjK zQ#t(L!h>zYzz_F)XKle}%TvEAz)F7-wL(n>Aak-^m)Ojf1PW3Eac9#31(ysi%d)80!OmjUbgh%N4xd>zx);2{^{l+bOjV#14&`S#aA`;CGs zo7`v|>CLPIVu^F$$d?PrfPj)Pc-)1y)^71BG07K?dBk^VZeT@r-H-bzvMq6Wq z9)sj9q@WH6K2LuZv_1OB)amkc;=VJKu9~{D5ePk`TIi6VVY`%O{+M`+i9p@+n`E`> zFyo{9DTzNZ1Grk+?~7$^=Boy|Tpd^vkZtj`LiRCk_KlmGV~%YmrLb?hV5ni$4xo{3 z112!L_d3geA&eCKI%)a` z5?Ea{*f)rr+|%JiiH5>D_y1&Sf*la#a(>JukL_44C0j1D!|5V`zwri{@7c@1bo8BG z9SN0$-6|e?i+dC75_PGK6Gel`c7;|g6ccg1$xuAmoM@kg3t&9?fv9aQEAX!63Gs2# z{z=6{l>_cDu(ncA<_OCT(*9ye5ZO=}FhX2whRFt$n;eW$7Jpfy&-Z`|3BztQjisPB z40TQg}(O5!66KBJA(YAZa-H`cs)h++=&`W5bPUv5sE95`tG+SW=0&nG4TT zwyv?=dqH7g_SM10-b=2>ULzkysu(dkLu!(HYI2b-?yiev^*^#Rv$WFvwD=zxbs+8T zk!a#8;TkrH!~M$9EbAs;UB2c&{6HsAmg=3jfU2$_pB+!)YWj-uCz}m^Kgie6=gU-2 z=wh9Dd5Nb#hxcs-kMpYw1e!CQ-c%c%V%a%4I6TzL)&H}RWQM*&S^iR71OpI}pg1`$mfJ2!wx68a<_uiN61E0s#9^uKnvLc96srh+uGY9|pF* zNgDtCZ1)hbIQG+(aV1~aumu=zM^l;J0AvmvcD^SKe_O`D{!2dtwL-=lQ7mis>~0Bh*3zq;rv zi^hrUfzvffdg+%cg|4-e-{eBS6_68zA@ZKv@2Jxe@z&+AbF9@<1qbC&Nob2kgg1RV zez2=|dB)9C2Q*v!Iis8{|3EnQhna%JYFhJ zW+`r|2_1;N9Z2#E=LVGAyN5*bX^+2QBz(N!J}KxfS(jAIU{|&vO?VCaX6)S!d@jNb zM{|~cvOLcO+-+BCMCQ3L%tw+BLH7L1{r2Zga%yC&V1yiB*#1;Q*bi~EGi^chfq^c( zqs<;!I9>OMTFm}X?*atmlj%*>J6K$QAsA=A9!Tyec4IDn&yV|#ldD`nM2#dQ?=!$C zjpkAy9=XNTdC9tf0c&n+x85OOIZh0KHrQ4vn7cJ0Oef$#69gQL$sI4_0-{;VGM6v; zL>Qi0E*gpF<^Fk7kF##`yEYRkp564LGix&o@L6(j)Pe6pu}LoIsxWqOrpW{JM38 ze(OA<_HVmv=S>Jnu-h+{YYE_#Um5X3MKLaE^;_6)`HJLW7_yKNjH$qlDd4yT6C%jn z6XjGVw#xZ+VWUE$i4;M1H3$~@zwDhXf8$`PU*P5k^zlyTL6$y1wh`^ z(OY2I;_Z*dqE{>FlI^mHl-O)kDMm@L)DS-@NG_xQC-=lFx1S@Er-GYC*sKFDYNjzd z?Y95jy0#^(Pi~e2J8rT1)GAl8_{EF?Ux~)Ncym{&fHg}()DkIFRVMLEa)QON@k{;g z2A_d){B>e^S7wQ$be3ZyIw_x>O8K^&*lJv<8~X1-g-KoD&(KD?}hrF*X(GPE))s3 zOu79CP-G3O{a#+}JLhsmdQ%Pj%rAFja{Mk?1poeO1#-X% zkkWaa2eRE){D3(h!B(wQ53|_OdH+*D;rqX)>)v~E0v%tdz?8=>5op!4rB>cu_9WM&^)9s(%h7*~5c#cV+-CDit zf-aIv3A>Vb*90g0RgSak#2}Q$q68Or0j&C_uWO4Ax2*%bQFFN@9s@oOI2s5=Y>Z64i0u(by@55uf1?`F{OnIS@f zuM6k>YpQ`)saCB?L?rj18+XVyPi9~_?w=MSz*4n><&IkB?BTIk zuC)k6S6hxbEZB4HajC_-xDS|r8WE73(iF|QxRC-4&f^cHpJqnOm_Ip@^!wMB~uE^0&OyJ9& zk>s_+)GZQj-si*7-&}JU=?sdfhdWxK#+B)M%Ar?`Z=dHQ1W;?##s$O1M_epSA?)Rs zS#;9pZYh2uR6z5(1!+go>drA~tzBwAKf zgZ*={;;5OuCl9{hi@OpCGMqQn<^gi$Yz)geCywMIGBfq~d#LTs5#W0$KsRcgwY^-t*foWgwcJLsfO;Lp&-EsgFY z)-U&lZIo14NEdmg*(~JGmWtSJOZUC*y5qKCKJYm0G#gByeR#&sC(1jM^2!H&uxhbe z<=q<5WaV;0+Qftq`pLxxGy*kwW7;R1M(tmg`OsILZ#U2lQy-e=Q)vy3e|Vqyf(E5q zc)*r!`L}(wUh_0fj*r*20vF=2&lDh&5!zw}O{z{kRXt0+6GtKxkrfRd>Z#pFsR!t$Ntdda zJ3hD{sMIAOY7rYiT5 zV98}psnS|wlueg(e(*o9n@NMP`;{QSVfZeJIv^GoDVhGT^A-EUNI#0auG8Cj6@{hr zJ+<&Qe4fNbV?f>x>dTU?(02hsoOgkf@%6reGSbgS(lPP*3^FBnwVB>5TPXu77~ZkP z*`yi*tj>v{+~m@e%&>Gwt>}2?5A<@-^(tlxP{aqjLn;1xKp9z+BJrPoj{3-`s{AsE zKHzsfMdM24B1VIK7(F2*)&x0)D$N^O9*NxqxXslauX}$nJIOeK4}RTv2-KigTXIZ% z|2W{@t|`1o{~CC&f&uHKr`RtVg^Y-5OAs?MD*Hjmj^Wksy7q_tbM-n?ub-=s<8O}( za&fM6^=Z}5LNnWW;Y1vE&j38n9j!`wUFUvOe<<|AUI^Zdih(AK_dj5MDSmP3{ls7j zjop|Y@;hG%_S@1-UX1ByQmQtxeCwXJ@xB@wDi;WB77>n3Y?2)(2YiOxLVP?srEh^j zZVHf7ZGSvpZZt9fW-@;nfkwX-o(0#YZ&>x>&l=ljfkS5YuS#dV=y10l=dVuYa(_$p zoj1B5F^2zh5Baw3n>N^JRI9cQfRg&SqJAAHh^KlyHK;(Xb)|~Jvt7)80Lm5l2z6|; z7#w;>n@kcoA>R01BFf=9pv5OKQg9|w3++1}sKHs4-RjaW?|!=Su!H^0l#7_(;{brM zCPAzjxE+@zxPT=5ZBk|!t6%qb%Vj0ky32gJ*mU_{-@7$$chK4$%~j64o&3fOatSR! z6z~?*_$7Ix!(juUH=~xKvlU;G-hHZaXd&t#9#8p5)3E>}!1B=Ol{Dp##=a}YE>$)jGB)ZN(&t^- zSaO*OEK>bp7o|M5qU#JL(QaaDV3ej>QjY|o8WW9+{f|P36DZGoU9!HCqTkBXtGJ`h zafH|?tgdG}E*jiS_cxadeEu9UzijBT=$(!2rJd~5HhUgoTm^J2aZ*m8B;>BhOJoSO z@M-^+jJ#8HWs+yrs7bvv$(DrDBaah%FV$^Ct%>t0Wh-6>1@%r$N}Q>u+O{~iH9@8J zu~3yhe`+H9KSzJK7yrQ^EZfh|BoE70;xJut9rs&TSX8nur_)8M#p9q%ckB?!6!L!s zy$~&T?4scOh-OoAz;}|qbXCJ7;>%AxJ z5PTz1MfcMS_`;hILk#l7rj1K}jee)wWj#aK4N|N2^L6pg`lo5&TY2Z)4yptF=Tgn; zwS7aM6d>+N+c}A#nwl!}!_Az+)!?L&MOtS=Dc^KZ5mXxz zn(I6sKv4Y+A}wA(aq)J$JOtp{d0H1+2rL`R+q(80nAs15UV@GX8-eJ89&am)znI#e zqbPQ6m+hJXOQ^c@6;2$5#AoAml+i}1!h?mO0{RI(=7T`l88VYbh0?`Qc`Jiqhu%S^ zctGZt>47Q`6vds93ndjt?71~uypz8SB-hmOY(Q4N>JBHV)c*zN3ihaOG!1=Yl@W~P zGuXkWv40NBaCB&gDu!*>K{p%%UJ+Xx-N9#_LKrL=Anoe`WDMD?w#;P}LAOs(M(;Laj=R^z22QTutVQ zfd-fMzej*$YKVd?3U)bghlq##jjSU-DFT5ctuQRyO*pEf$!aIokN6v5?8ig;5On0K z&b;xqk^mE2Rth32H7plu7%P8Fjlog(+qC;4x20@5%kX7}$O`7n0@>&Y8+&EJGQ9=b z{O7G0G z2FR*h3T1bMNRaF1Fj&$&YLA+vcf)X@(}xeC`Yz9vjj}W|6${pX=eHzaiDGqOF5{$f z9NeuJgQW`_@c!jVuRIRAhlWSfTf9WtsfXvz`a~~E>TT^67zF#b+VE}NpKnTgsl%5q z4CkW;im0KgjnBjG%umHX8B~Y9OAUWr@j`Y+GtvF+k>0nu0<#VLUtxN!8q!yg;QqS( zng5U#k5SL#YEQvq(+7Pb4@}a&5em3X>3+FY_@5Tg6Ux-F;0~r~*wis+YcF2X)OEca z5Tx^~v>Thh=#C)iG;Q%s3IaKOzAeX(j*D@jplz-{z1nc?UePxr-ImNYY0YPSynVad z4wE>c_Eo0#{^3jWZFeO8VJ!-kiAgCD(lMoR@?`n9F$vxNhcrB6M#H?Gp#%!>@>VaD zo@V{>HQ&(Z0av!Q%=ZaI_n;FzV?z=9zg`T2?UkPsX}dn~o5#)0&}-g*VYz{B{|LI9 zMDdjB$%?~W=e^g(b`KTZ_bgj~L8-6rd&2=*g#Ng&{4&iq5-T~DLrVU!-CMp>7buNN zy+~;(&;jiBJb(Z}HnSmSvxAAnU*_S8GWg(RZNCRBMT_yQ2LO!wH?t3($6N8n79b8) zV17V-MM!6Mo<9bmv%&`}CLv@7o)5UkF(5z2S`@+7-PeOu7 zh)X+HJwR3FqQ-JT=7pVH2dgMmnCDmORJTj>9ko6XPPn&7C}`G(r^7C7b|j!guow%% zJO5}K-NhQ?t>7d6`&bg;hCZl>Be{0CwlLa1c>^u;@72h>U+o;_2(YmS$mk~`6L}F4 z;~eoL4p`8ViZEcdA)-H*pTmXC3^^uZa*`st6UqSw>7;EON%Qh#sh zl&^Q6_yGpFWCItX>-$%`6K|jq8|KH4_M80yhE;yEd16v7C+7lg%jHWiEZ*J|?@`ez>Gq=6Q8rS<(n?KSP4X%oNlMo}2bG#VZ4Bb{-l5ldA zoJEYN%Y`ik;&HNV^Y8bvi#7A?_opadrcu}Hj~N91R;tBT9I{pj)&Kt6Q%hC1`&hd8 z#~Koo19Y1-A!TZzIt|uILq~OB-1bR}nIs*LNe3n`$z}njmBP1DcCKt(qWMw=p6YbD zb)xyj^B|sDA@dz={OmZN>rqTLEK34L)L_mD4qg0vKKCCy&Oep9&p&*++nb?v59?A&6m-ah-&4^X7!9 zk_stvbEmA&!iqdc=YwEc5^`Yp+S`Jj&&)w2Qjok4!cji@*I8 zSfEU`=hWyouMdKU9Yd9BJ?hq8G4@nz-4S97w6-unG`b*c=*i+ys>Lbv!VxdGiFIO5tWORO{vGj!4n71K zhLn5EST~M*8uPqDE8OcW*eCmS&|E3#k_QrHHNm88O{T_@4{Xbm250GdK z{VqH~+4t1`Fz=esPZ;6bc6liWG_F&x8xAK?7*@NEhwoqu$N2u-7$8!;OQ(wl;|QSh zR5=9+t{0%f|K(cops;{( zO(Icg;M^Id^7&)t_wbV2!kW&QNy+KAPW0m?r=6~a`MZtPpA=7qI;@TZYJ?8cds^!b|_Bn?9n+?30!`jUVa$u%eLsBV)m!avgy3|0xX5nIlKy8dgwt^8T zk=O%xnGye%#SZ%Gf*rYOZjlQRACEw4LVxs1b6cf4$Ywd4ZN^S$WaZ`I_3o<+{go>b z^^V8)@fr-@b|=_PxTQl=7=68(`4?feWvEfm(lL0{X9I;MB`x)w(cVGY{?66w!2Efg z-~$Hyxubt&Hfnf`in1iYFjXlQyo{+>w9qq$VQ42I@@m{h?%3d0{ytw!(AT%%0xu?T z0X-*96&Jjl@F(r4eMM_)4~E~`II5;Vy!?Vvq?P~3j&dVsPP2?-H#8jix5K{?VfJJ3 zq%_UE-LG#QT54J-|9l=BiIwW>j6Q1Pq?GISi3F1=$%;|72lhj$?UH#lTfe1s+w>_- zwm%pDWJPQixTHoKxUzgLVL_W*C~t9BK#p^SDZ`oi9sxT`xJ{}P18s&gR*KS7FAs6J zqr9i6Gt%9SH%?W%P)ojRPX5GH=^-PptJ(DA@-y!{{wdCEjdB4Hh~7bh7_D^vFZpA^ zYyAVUL^+;~cv_R!1iyB=fS_Zy&=iR(VuQKeMUqb7+fy|99hvOwB`88s{a2QOT0PlJ ztJ%2nO$ZgUu;Hs`QM_xXpZz9!UuymUhj4b^;BP0XFf+&Qp!nRjZ%gApaImGChyvy@ zDgBVSeDYAu?tM<413y}-PpER+w4iLi>iZ5>kZ~oKHsoHJKCoJ>IzCVS8~vGI)^1(T z@Pp%TIBK;W|7|b)M-IjeD)>yIOgXSdpi^(fV?Ar{Yv?ho%^H9LOfv*RetOI0RZhR9 zDh=94)J->gV2fl^BnfcoDJzvl?>b~=eiEkii|h=NbV#KOz44Fr6NAO}jyy4V_tR;D zS#zjy$SA2q#Xr*1#0|dx{&21P`)`0MkJe11O78*q2uRz~1iYs$^FZGd)d|3E2Rk=+ zQMjNs7>ou}G+xVfVJjNNM8sl6t|a+`<+}%L8pZ2>cYDqgH=>laB4Yhf^p?*OB~)4V zRu@BHOtavF$7JB5dLZ`^h!*Rg7^!CbQ>aCORww#{^gz=3!D3`OPll$Gkzkq$(g75Z zh*1ev4iKUz@6)OJDkQ#A@_Inr`u%j9uDTi^uhPdTXRqiuyT%QplfP;+R4O4XvaVW3 zX2~P7-q+oXP$}c;w#bRTG|*)w4K#X|#GP`6Qx_>^MyrIk(&_8ofKWqjIM+~ytWai# z>q5FwSX7&e5;fV;H73@BEj?DTl9Y&n4uEuZF*29OhYz@9AY&WAL{f?)SCf3lzN~&7 zTx&uHKjeU3VuZGJDTVC3;GHkN)&kWP{GM{2jay6f?G3aJ54X$qD-_V?J$S!{ocnP< zk%~Y`MB700)tmKxQM#~!y&y#v4=e0*B!Y7Z=H*@C-!AUFJu69Qbr`58D}L#UbCdYk zu}*pjGlH)9$Is^B`d#PsU)!(P+LOy4HuKBKgA*fcOyCXL9 zl<5w@c?8Cb4C!VpB6o zsM@WEyltVL35Kmxp!_~&i$eev9nE&sNL+r8=iepCfCJvke5jC*eMd^rhDML6DiDMECnosL@;`t3n07->T;sg{^ucC*nC*Nm^e$ExH$6 z!*5rVJJyzO65T#2tT*TV?Qs;c)k|biXaS9a@@MQ&!iImd;PiTqX1Y09ni1oBZec21damZ2e7HM zQ8pE7NW^! za-IP1fckPYBc~`E#fhq1s^S+)5~`8KA74jga}O-ujM)BgI7`<;uJJo>ipN`zh(*n{ z-*aGp)I=qt(-1yK`#M=5ITxdYZQ{|A?xexcJxO!jdN&EbO^%j7rCd}DiMNr46FHU} zAj6pN1U!V2F65C*38OfOk{liL=#b|+G^gjC zFQduf=Zq4nxF4Z&8<7UwQ02BgUdvHdPk4+-L!-6gzhIz!Z5vf@447GmP9$SgnZt>E~Zd} z*B_lb2PMvwv{nISe{tdBM5ZKpP(m_l*Yz#i-*rBnwnp?Xp%w!8!fdI&_jKVfDj_?B zkn9JC-gm1DwI=hM*_m-C%Z{gGbkFTxJ({)#_!@O0;eVV5<-KlNMov!0vs#swt_kXP z=n~tFhI{6kQAG+g!Oq4xgvfI|k+)l!9$tRYdp)e zZqQ(!t~8_a|KAP3vzryIT}<6}GVu0&R(PC7 z2iC|ET+D}3b|y+E^Dhdy8HZ+tBb3+(ObhD59sz9H=?b}9zttjEvh>ik440xTIhN~% z?e^sz7E-o)BtDXRefb!KjYa1QOTKN2Fl2-}Hu#kpOy8P;u554M)_R39PqY-l&nKVk(F)zew z7!kL5Qe5I{9@H%F=fssSbZE*hpF~JJx z#{V5iKx+Fbms$l3CEpL=rs2;z{rhn>wQ`B}Jx}2BiA=p4x})m}*!JnPIV=;LQdckN z^NH|P>Wl{;e|NU|UzG_EQJ*e9-z_E$veeAzsv@c1d@pKav#HO5h8uu#=t}?=+_Mkt z-j3KAhu)-jMXH3uQd$AyWPNHcq6~-B%HwEMpG+^lB(cY-q9S!0yujkvk{r_vrO97$ z{?Hjh9qDRsj%^Aj?HosKwbmFQ6l_GY_#^(X=DRcEiTvq4txSS=>k^(?DG8RuNT<)Z zMl>N?H0XfpV_GkpcDV#GJCr$$6=#A3F&szoNoYCdizxJ!A9W>mecByj2j+annBe4s2T{y%R1Xt2tT7!`>g?a83W}N$diR$Ruk%s>&}{Wyr-u zaycAYy6{%yG5I0G>$UXtmYN9>ngP{C)~1$u@L>E_G=5 zIoB#!=lR-Xt^c8NdecB8M`tz1kx?U#p#J$cI>x=x6lx(y>+jKYTIdO6^NAn6;FvgV z{)EQ@GSG(H7f1i^x^nJjee%A2!`daLFAoY8T}Rlr>^fR3vpy|PBg`D?!@fZBQ7zXa zEuv+_C8|0n`bu-47G1cIre78CHICde9e*x>udLDUA$FfE-7ZU8>M8>4Qr^fQ8 z)Pv1Sb^S!aEg3w5lpJEciEvW)Gv58URXNm2DcS&Ay&UgP?ZQYm5{TmOU!u(*#t%1f zMk_=afb7{f6~GT-BWQDk=psTA{u)vz>x3Ax`zsPwndMjuvMQOh?F($ZTyHXETVy@K z`E-b{G}diLC`tA;%mcQgBaQkOq_(~%NG?qErxU1R>4am^lcg`t@sBcYzh0KAuK-E7 z!JH@v$s9h70EU=sDNN!0VCLd{{~*^VHLtBs8|EgPVQVSHoC=Q1_Q>V=5fkejx=X3> za3r&F&e!$A&p9p(2IjA)m38Amjak}j3W+^edrZ$8z26{Qbe4OvOdcGldLxtN>VRKE z=#^b7(hdWw@Xyrwulb>TG_)u?Qi0nAQ_x5Wc$`lT#J&N3YCiCI*{K{A|Cc2v755wq zE6%PX5|t=<26xw{-lq6ZNXYZ+pT6o86#s9pJKsKgk$>*>UJF5v1>UX(|2My;k}vBW zzI%V@7bHpq_p|i&k1zg}AAIJ%q~D z&B?|0^ADY95!vW~q;{jcJSu${#Dtl7N-1HcH*y&fj`iVz8 zP%6)1^tv-}pbw6n$RzyoDpA?C|I0+&d~?RSc=^1zxw*!nW5?&^uy*=s`%V9ZIp)dz zrbpn7SHInA?;MeWmc=)tzOEP* z3_Zm3eEiZ+S6t&kv$F?*?&YqY9`o?%mD!i=7!4NFzy19l#c#?sBgvxxUU`dAY!c#em;p>!U;*&PkKM%%PlUSD>oR{2YfD1<-Yx(i(uccxepU^ZOQeK(K-$bh@2!Xw%qy=7NoNvM`#!VjWCC#){! zyx*ik&)R3Rfeq66F7-iHviaaR+g!1^+{)^fCZ%;LhRS)Bd^kV{CjM{5_e8P+7R!Vf z^sz~#h)NUsbJl5hq^hRT*Zh|1U(UmnZ7ZDMs$%NCZ}L`WsXP*+Q$m5tyq zBBsP2fZPaoRFXwa8MLY@fn*yHFKu2 zs#K7%v@oYL4MVY1%!#vep6ZvG|H#pEJW?CI#9EFO{`RxpG6f}>%oHvnlKmUwsj;Ph zA}!?hZ14MkX$%#L&znHxHsIWQp(*3=Zj=25zP#NnbZ=&UU)O0INB?8*W%63Wj#+tb zaDyras`BBePxilf+ zF~(Hw0T~~hmcFMeJWcU*fNparhO@eF!B{sfEg03Fg^bJSb3ILK`MaWL->xJA1WG{Z zvkyv&wA!!lvL?~v^=z=D^iB&jJTkEAS0^g`3cA7S&SafTkrOBx7PQ%v8cz zK(1@nT`jh|R$@G8RC|nu{{lCh?|g*Dsq`a~!4L07DEz2#`3lQ~aleB;Dg7`M0>vNv zh^ef#UElSGKW5nx258G3zqqDtKOGA)g{Tg&vs9vAzP)~gxDNP2|l)csCP9wIG6QF)mno(%v98%6|KUdr#D8yRqukV=n7$&UT&DAF*3m z)7`5zlt}pp4U#A(0Z)+{lyi+pgw7%rjp`AJG!%p$%0XDDNnE-&*K26!RdQJv^I(3? z+5)qwRj9`A`OXt6lmGd zXPKxeqyIme&MGXcuI<`%ceiwRmq>SacT1O`ba%IONryDj-67rGinM^lKcDYE-aR%v zz5=irvi+7O5O8k(tYN6W&!@;oqp9v}2<33sWssGRzX9!6c6ltdIo%}+QO zqo-JUij@Rm`m0`GycieauCSOiWSUS--au3`^>(Z)Cks2qd&Oco?Ch<`>F<_dXX7co z?Za!Q*#|5k%0=g&&bytkUyGuTx&(ILY(;*WNkUo%-P5Do4e&3($75~(iM=d?JRzqu zIWLcO<#**JfS0WaIzd0-!M5PctcKG%wCh8J>L)RTIwDuM!XlQ( zj})>Pjs<3>_Jj1EVdw=2*&QN#&|R}Y*{RX zYWk*E$k`|)e7he$WH(>Z)u6}{o895};vO?>R@5TJwD&uJE}s1IN^ajN&Hae)@l?|} zquTpHkCbH76z=^yWd?;Tf!UwH=AxdX851=CUIb?1M|)ff~c?5*yaH{ADU(0gX!)L3nkxjVXv&( zl(aw3jL6f3X?(l+)(%F_e~?`UJKVJqsmnm$lEgfYGo)n{?KRViS_`T|M&dk*F~JqzR?X7Qp~h zoOsUPTjgI8&e>w-p(}Axm?iBW!TuSIp`1r&W~1{yfu7e-&+JI;rG`e5d@W?}UH(;5 ze42TEtaZCP_VjJizX*;{&VTs09gj}rRgqj3xMQtxYW@7s01e;y-)imk`bKpixI*+e z%DfDS6rQ$;>ms=JlmvkK4cYIXE^da)yJy+Cn@}fJ#4V1-5YoAuK&OY3;k3nWB{_(Z z+C^ldEuetib{^d3^KY;0Gjjg5oqa2o51u1&fL17gK~$lPDk5ryPGQX`Kg8~zsrj9< zLi2NKHp=`{hj!5)D8(~kiD3Bx^U!VV^fO9fxXt;mkME}C)flq*Xf%C_bI>tjQusGy z{k|+cs1(_1BowG)k+NZ0rz`Fk>RXF2*(WOU>=vNvWbl~;s8?6kFa8d*cZO-{g^!o# z?2^Bflv{Pr&Ao0%Knv2ChqC(LETH2XJ)<%y{vlx(W`pKj!iKKoYzGwR1t($y! ziIBDsX)BUl1(F-K)gO-2wNo`n872$uHe0cJBrRJp!>#m&Ev84mijekDr3m)wtQ7Kp zFB0F7-cQUR<4rpMEU8Gf1K0x5M|z}`nM5(E25!|V;e45BIK3a3F!fPSQHN}TE|UEE zsui}sndWV#wO`I;=B2XD*J86idVJ}0`}bu@`BmgoMP(w`TSd~Dw+82yeLv=GFxa@r zmsfDD&!qRN7;GTtnYbo|n`KpDZxnhyqyI?Qp}1%tO7>lIG%5hmc{0{$Q0x$YA)T>` zGHtCaSc*ilc`*#eWDrsxu`(2AA%-92ptVl7flTacqgO?IBU-;#aWnyqy*R~*G;VEx zyBk5V$GApiE7SLQ33jDr9{NG>gITc;X+&`+W&D!|{0(Ju{mSJsRjw2&<9g7?IA-|P zROnDQ3?6i&hEp85ZV{|Z0*PD-?{A>fOXYLk+8;?ke|J9tOs(K#)N8b^xhEAY zc)dBAetG$82dq%j`8|PO*Ax3bIBv8-j)XC4RT}##@#!Ec^1B^GzW?#bdG&iKz`1Ae z*-=&?C!(yY_kP3k&72Ho(*xbLZ4KT7Rb$yqPM7^L*?-^asN&emGm^CH+^~40pU4J| z@nPmF^6hTJN~!p*T6py>pEl#?JZKXye_-&!0rh+@7w6U&t;l2js&ljheyZW!FC^$!>$Olj?$`9oMT50sVynI7 zDVk}ROonL`lKe`FNkX5V?1MZcFG$k*ppnCx`lfeeRVyhsHRSr_m@`qtDfnw4FKpq* zWSywcU*hnaATjiCttZoQ7u!U~h94C=P_}DLnrvs5oR?VWb7)3L56?E$ThQp;ONx#n zKQ{fD1<`U_)8J|^+AjjsXzZ>hj>66CKtioiqxEcoN}l)ie+JEXVxgm%57wW~Hmsi8 z8UVp3;5q6WFa=*<2)$sEFc`L7I)f?wb;Y-vqcI_m6AYQ@=);M>?^fOx=4XJJ|Gw+f zzvZ{ToG>yuLHqWN1C(`F%TXS8pLw*~6NP*>@7S51efw1|~`EllIx!yEXwVMu>9OIv0S;X=PJf) zqv~j$8VTinhv+~1k>XiM4klOj5!@5W*&`-q5b)(9JVIZw*@Az<^t$dQSv%*#Hkn;$ z;0rZDI?T*9VmS|dBs02DtvqIA&qM`_a`YHxJNYGnubnz-Cq< zl^$|(zFs`6gi0X>k0~8Qc>u9*hwNO@#GCw+?l4&8J9QswA4~xZ!f!#(*GHTEiWIsD z0CPWqJ6ft&+XsGV^7TqBroUkCax$Ng`>Vxibq|UJc<#V+dkU5#hTzdd!evAtNw!bNoN6j@?J%%kf86yV; zUN;%GH|$wdJkn`o`5y#X2oii0pSE!BAT_RkoMC!*Y76mkhjdGm4N2=feg$#tRa0Mm z^WQUI5N0iG>N3cWh%Y+Gf;}W~J-M7OIg&SFIz0WE2cXgWSy@7Bu(Pa~F%SG96Dv8p z1hlFa1r`-wl7QQut`)e;1)u*bCxM2U39LcW$3XLw69XVTBA2EKK>LqM_^bgESJzv* zqMJu&BXLsgXHl#4q+J?Z>4^9#Npk=H697wb_Lf){l)vBES{B6{@n~hgQfe+sHcOl# zEa+EFTtdyTy<=4=S1k!&o}7k?w`nN%t_K#UXv9)R4Bz#Exi4~^60HVzT#7{8NpK8F zf?rb9^)vK|qna_+oir|&ak1SO%^*5CLZSFhq8qIufBPAe_2*_Uv62>x0se;^W_Gky z=|s|-9Rm2aDDk+71-9p5L`p{;NFziDTQjs4aWS$OF-Uhw5g$y_Nz7?{4`jhWCeh2! z9{iK&r;(8e@U}fg$pxC?gO3F`^e(83%TI7JrPs|lu$b}9P|QvF941Kz7&V_?mK<|n#*q`@Fpq>#bD_2r(qQxJHo zlZRrMw5km&HBW(~pZqc?=;Ly&>%(|DlhCK>9B}aL+=1;_u5JW4VWRYb6S?Z+@<;H9 z6k`)WnXyuPgs>RAEkGUs(FrJ}GpJ$Tg|U)jk?HzN`av`A2V;2#3?&*P%*N$Qy5DOo zB+xCjqH)_s*wBgysJ%n%28kLPe^gJZ*%`<6u+l|QJI{zQs59GynKY5k^G_KwvR&Li z`rO0wQggE~!pl#($&EgT^X_p|6bn>4c#2h|A>J^Rs8+OX^qie$b>BLOR6-#+M>-m% zcA0BqyF4ah_;6@qjKXv9V$Pjj6GVnGNIM`2;~)$^Fl*{SRk9AF+K{7oMDtGJ#tYcU@`HlGtex5Muk|XAvT5ARTYAv-a0^c9Ned3NNNhg zbF$SfPme3@@H_`fkh8FEKVYCr23j+CGGByehUe3gkF}@whxqwDPvaArz*2HlDxWdj z)}qt=?|NcpCRYfoQLPGn?Z8b8lo_wX=XGFwi9PT|VK z6oDjX-J9IU=8>Mpogrpo-H92D0a@i|JoP!P*X3$hmYDE}*OHV+?%dpus@tf-lCB=q zh~Fba2%7Hl@x7g|m3Ug5n5@?d8vipP``uTN0L*#?Vd`0VP(%P z3s305LGx;SfV~5A%8W$P(;pf3w;&ppWwE?^(~yi-Q`!)Z9wj3!;p8c8cEw6Em0l(L z5{DS3%#dzPPUU|ts8={NNkFFywJ zc79Qy)7v_=HrEb@TFB7AtlRm95n7%WB%jqMDj2t;fVwFwQnRNFkRbGiZS3GK#z%AI zqT34kxi!eA(d>~=p0o>2~{SPj_2r5^ltp)ZTqIRX~+; z=>y5tc>GaoA#vW2Gc#>~_+Wfj91$isx|8q4q2MQFm_R3sWMVx$IFVOvOr637aiZUab@Zsu=`Y2*oZFUE)46S4cG}4==?kN&96~ zf`H})(fIXos+u0ml5|j6FuCcnOGmR;iuh%GT>~cb9`s0QlsoJ*)COm{B%KY z4QtM?HO|rHcbUn9d7r=?Ch$Bkr;|FKC>kd}VebfgwW)1!6UJ9Cj}0H#J~~7u%Kg& zt|5-ZYZkc+5lpznM6Bpy{i@$lcIi0YrNVG*#_H7lgSd%BeX8sZ`h`d}ltWUB?^RjX zXA>$b0Z8t1W zRi(JhOWD=(RqyEW)65pgF;KeWf@NeN`#y`{@^C)N&yB3X$~_@4Jm(o}rkm<0eI5<` zM6j?LjnzMoY9C(YA?PAfYzeFN!m(LzPQ#ju)=BO6l|!UVfYdWQa2#)Q+*HUwvGIMQ zfN3Cv=(kvcV)xgysVK&V5B?;H^91Z@vlsvl=l7+8sGMh5%<6;TXlSLSe7fB;Y|7$2 z&b9Q0gk0!&6nmRg;k8z87{Nl4VL4q~9D5%Lk*g-=FG^B~vyGwalzvrls2y25N@^(= zREetkT^#lH=ZZLSXqD#z)B++0b$N5w%<^c|Ca+o9`O9OP>weTqE`|4Su68v3NI(f| zRX74W{3YV@a;3JyT*Yg|rvgW#B!<61@hzuz|J{Z2#;USciaA4&5SoKiv24jbzx&<5kIsO8pEjR-$7*8xIdw-;DzeBFuTla$qs>dnuwd z)L^h{q!z;i+Lj3t@4OushZEXi<6i<_Gqm3(miGUf1w_J67Ebc1J85nbYn`1fd)WSz z?8kzTLBgE#MS=A_<0b>BFvKWQeKD%Zio|5twZI2a;)TN|CM&4H77-}sjmy6?R$Uri z2GKv$0|yI<{X0!nI7BRZRWK`KE>*HY2Sy(zd%TzJw5OmJ74Q0ReMj6!*u!P#Xlii?Io+&Uh)(5i~Ycrps2%6nT6o}u=FPF&L#nN^ys zzXMOH@`!lhnY8gqAJo6<9LV;4tFR4IHI9^T8@-sPePMy>qc6UhIfu@6;TbMGtJjq8 zAJQ`?iKbWOW+g({F9;SLYfiU9!mQ>@Vn2IDof-6lgXEGRkSWM>K>D5_7y9e)D}foB zY9C2~s9-AP5JA89&@i{LCAdk9uT9}A@Ku#Ikyzv>=&TDUDOegpcy(VFmd0>q*W?P}un_YUM@S63Bi(jn&<-OvB!j1@XjA#` zr6U+1SsaCSbnq`4Ih8woZt@NC_|NX;FCvB@oB~Kw(i#~hQ~~vR7N4*I>rYq_MP5bK zAB&b03>i$53qA=Q5)YM#_e4_BAC!3fT$m)Phtq^CLc!cG7*?!^9*S9{uu{ggW!d{E zV{++Re-aa{K4nS}(Op(fI&5ZE{K+cDHBdDqIb&M4tekBYEzq#(-LRQECBy!|@*gC} z8mfhJ*HheHnIV-tmPo3n_P!MtH>xK9OlMJz~t0cpNXvb2@|j3a_0BdrDT&8_n#rpMcn7Ez`eXDdQ6BI^lL06 zZht%S=m&x_!;LSeV;_wN#THkiy7Tbx3-rSvMeAs0ei_hrhX)R|pmkB^rBvrUGsz9| zFE}clqX=P2#}3>JE63gZA%yk6=kg;;MPtw5i-mrkgy~aHjJ4|Y2oL$p0IS9v!Zy4s zbj~FcL8Hp1zz7fm?xWU{527N$nrf$Jbh2}_aqvk`Io&vq zNX*SUX>K;}aEYu#*kM1Q>YsBkZv;L}ZnBM{mj#MpA`>TTH6X_T6*ka$yz7(&8x;`M zv)1ZtQYa2fcLvm#fEywL^msvZ%*clz108+XWi+?lw2O2N*=z6@0{5K;244AP0dd4t8aOq?T;q2 zKyCM_p963Zp2m;>T*nHq`GNFpJdH^Q{B~6XSyg+Qq8DC@_W=otZ`HW=BP!d8E|GcE zN=2$lCTvOcdsPt4nVOh)ab+06Y5ZCSg-mOaMqZ`3ZsxFk4qkG}2uy@9HBodF<7E=T zICY$pYPyw7m5LO2W#Yo9DrixoB=NJpCCQ;ZUGlfm)Gw~Ao-FgoI;$u0#e*ZB-zuGX zCb9(avQl@m1GVFgec-WoQsJ#)pjvi})RehhEsWG+GhJLyIuPdqu$O-`>l6D(7=F?8 zlni1ctosg|)o-}zn6U#Tk5ttcD4NQ(Vg(mZy%jh*225`y4!5kYork0tO_ov2G|Zk2wlP8*oGxIHvbt0qn+oQU(80il@;*<@5!SFw2n~JkK+j6+|bxdW_$o8aC8c$+ss12Mimc>_sRP~L`c)HBpKih@Xm z-B3Izv0RqZ1LDZIj39(rTRvLfwL3EgIkTp$I-qz$^G~}RA+mrv*JtV{J3$evaQoHW zfmI&ZbAew#!Zr$0gug+XASv%enuO2f3!yu^A5I&!+ox3tw#bY?j;x5N{B^TxaST|* zt*MnfN@IG>`(SgiG-Uv6L7Ii?L(|J1L*U=MKX+@9+j9_b{q>MFUv|uv*mKOKve$Uq z`wH#cb>HK}=TWC$yvEdJV%p52+cs;V@_}RTco+F-`pbRTmVn#Qpr~(I=gZlf+ws`V zrN(=;#j$-l<$TWHW$Vrr`o$d|?*jR@`;wlMzLAZbOTeRdKaLZle&xPcY35qvb)9_B z3aAtIy;MDQv-;X%Yh0u`CiW-b_D_0^a{M8acH2w$k90Pp`y2F-E4;7*G*nh+&A;D2 zqhR!=;Uu`Kl3GG1p%n}<_eQ$1mnU}%eIbA!Axek#Cx{@Z>gN}=A(hnE@#R2MaUNSt zAlfhJP$n7+9)ikru%k9$gs#E_GfVW`L1CB&%igMb_777gU!B zCfOHDL`Ux`-sjo4lLFM)B4V+ExDv==JYV%%4|Z|l!9=j5`rRLQG8@q`G^*f`FMTO zn6sJ?Ftc(Eb2eP3yUd4>VEC8nOcJpF0xz*Cf^;F&P&BSY;D1Q3O!*1`&470)G65i@ zP+>6N&WqfnM9cO?*AkjbpZ2X6=FwwXf)CVFc}JW&2PfPgfLanZP&@o-&$#L{I%-o4 z|E&nIYO}UmrafkaQel00h~bpIj84yeUzh)JfV1bx&kF6_wP+Hm=w)nOm zv83?d9NkDi014R4KX>JU3F03V2)W$oh&k{5tmAWlL8BJ9l)c(_z|7m%=ca6)R!Ohj zS85gM@Ob*Q(H0;@oahxouEXn08l9H@aM>@{8MGL#_N2e2ab3fRh5guegElUBby_Q9 zsiQY6xy;cEh$RsG1xARdM10&9B!-0cOxgxKNhL^c`C|$^D6)KNrX=!u)piR|eXdjU zaj?w8kmI{*{rr+$6_KtGF_M>7BZF9M0+P~9t_<*)-)9387OWZIZBj*+wDQ6PBI8+# zIzNhh`J<$?`W?zjsKorj*OEW7{2gSH;;lEFEDJf#JDWg@$xJmc%75?o#63+4ax^uY zY&m2l*L=(7m~_Lf_>{AEViEuQ`WmZZ<&;<6{o z(ec;9p;Bo>D%Ip0nV>>c(um%QXzbv~&n7u8&d83U_3s&e_Me@D~(YIrt42Q`+^sd*NK^x;RsKba0QD(3hgx8BV+b?45=+>M5C!$3iGx0gZ zeFme7Q!uI)7Zvomwgh+@om#`y4$qIF0^=EM3GMlUp5vT8fK3ZLK!3rBY2tUc2%Mt| z3)f+iYXXMCW9F<1#cIK4*7;YBBkGxJTFA6v4;SjLB>bfo*r5w->0+O4&Y#SU@#Zqx z?iZU*e!CF`6BMH*4mf_zF>b+&i=KRk_D)3jAR-@cGB-f!uanQsVG6!$n`VdNX?F9+ zsjpnlH)To~S;8J_pCS&_EF*On?axv)%>2KNq!jHx!TN8`Cde&M$objlSP zJ$Y4pP_vFgO%gm9jiV`i&R_3!1Te3?Hx8n$M?(lk0)#$M=J*|I)%58qvXTnBKm<@Z zs8t!~>fZ_TgQLsikidY1QMFzaU=gY`>+Yw|U&>VSq9G#m1?LRaUx4V_z&zt8eZN(# z1S-^LInAx_aQlh>BDraGEl5m5Tx}zuP=|MeFolO9pNB~Zg(zAS1Tt+9wJb<_GXmXR z@aIR)D=Y>LG49^r7mWLwHpa-`(NLLq9)X`mSx*+?sTYp=EOGzTl}|q2 zsQd@H(5hN;B=H!a=p4pC8PN?TW-j%6=ToM01y{uq5Rr*vx* zCSHEK@lHy7GpcZn6`^cWG;L#&jV{6#nQ~~1XF}KdbtF=ppSGVtQngf(vKq4>S`(-G zy^75*lxtjF21Nq|#vhVCKm9Hp8DIcKUqa(LD2fU1jESls4AEm>yab3mDcBnTm<=G0 zc2i&_USIz?hY~P?oI+4)fm8QCWjOP0x`ET_TroUt#7?LSTt7W1lmvk_7!W1_Ky0^D zps57V-?se!{nq<#`3M|9-7ohBva>_tb12RXiqg1?J0go5A#7Jr(0J%+G?z);*VUZ2 z)s|awPv7CGn_0~CBwyf+a#Lm`O!m^V9&mSvFc-j^5PGo{JKksD>F!VWYQS}96qrOK z(xu{-)!Y2dql!SsXV#sK)9dgab5{|$*}$HuHu!<&-0HNN%%xMy-qAPzeMsTUhewpx zYMJT8hxu>Ho*f+%bHy^4C8k>1DL8o?7Se5)`{WO$Bgz)(D&C0hjd%H#>yLvYPiCZ z?Kf;QnJpPKtxye)B@H>P(jxJ3w_- zFt?~h%p^7O!*K6_eNHZv!@j)aDl*mgR^@A?=6XIoI(_}p7*l#B5L>P#n%~-GOfPZv zJAEpj^3lxXM8Mm1rLHo^htvWH-t8k$HzSF; z(^+2a4Ly9ZjLb;{IpY96s!7-99}}GncAueKbHFchvEEvzoGXaX3?m;ehe7$c7sHkg zjCL+^d(`oo%7cI#!kKI&GG~t+z*Lk-ARJ{!P1AqvFEGygVZ6JsG&-X<8b;i$Sye;0 z_?w{XK~Hv8upK6R^LRYtc3*iv%h3O30gPLvU$~Tdq)5v-CAt)sD~WQv)kPBckMJcl4GA<7LG@WnK* z60#br7OPoxS#--whNrS8^F*-4BD8gy)!bFXASC(7e1GjSVAHZ0*Sjo6r~Un~RxI04 zqSFJ8(XNXlV)lLENm?RjC=$kjrqiDSL*jex1QD;jYHtJl1Of$yBNV)Br@ql`HT##^ zc$WwDgk{Ad=lq>H-c^yOD{4B}$368%QmygO5N)`WW;KgPA%=oa;utmeLgO;}Q-%DQ zAr{4XwrcL-LO4{9>}P7Ky4e)N8cPJT*#+{-djW7VQCKv8x{tft-CuJ!ES|pn=?-C= z|F2N)g%2i;BjU^BwlgOWEVgg9|Bq*IVf5n~pFXe^M5+pbV`SCsP&to(mmOdL0V&1| zpl^nm8FeatmMN{H5ORwDq&pDEqLELvox2{tK4cI>p^ zAcEc=$YSSe5{tbq`G@RoAn#U0e2lmj2-AVdBoLMY3Js>ZPB(CZ0UK|PS~1+#D_{^k zbj;`_HGn=KQ~<++9fAa4Qa2iHRI<74lGEScgf{L1C^Ns0R=KJfP`dC_ZU6YhCelgb zP5Q3%0jBT2`jNtZ=Au{x01TIE?7Z>7?fA!)s} z4Rpgel7dko_8YiUcE$bdnKTONOzTZfvuzY;ezg?Uqr}c#kAE37637IyDJ-P_CS^5A z4Xt`(?J=o0oor9&{Mv41`;;ea{E9g}iibP>cskpB6Ug^Zh3o@^c}T?rYyczhMh_{Z z>;CwRDxZ$M%iew7V{|;7C(&vB*8M_|EbtL;p~9ORM0^Pg zBknlUIn62JcONFGcZ4{{xb|ugY)czXVjmq+;p3xiAwuM-iH8fhX7Tp}U5G&ewj+2M z+d!UxNd7<(deU0O&JM#R0%3vD@ZKjC5$y+mr>{}aNtL0Ui0h5{@5B{RdL-QNT^9c< zDM9)aac4|-O?ca3?)coF56~pksC_{Nc8%Z_V)8ojOAjDE(8Y)|z5yrLQf~&Q^=M?{ z%;U|mz=#*KE_;tt*XJ*Pf4Q2^1+w)#kbKKa5Lv>856ky|Hk$`#-{(<_NDbe+L;O*f zyTxS^K@VHJ#xd9dF01id(}W0M-h6xcw7uuh{P~W^=@;M+0Eh8&Z^(PFJ#Kog>$z^* zr~PQ^Q2-|jwVcR6WGf-z^iHsD7A|xKrPTrJ>b;_75@&cA@mvwHBpCtpM~L8tC0RA{ z-Qy6jY@Wo)y9)K+_Lx_y>$+b9c2AIOtKbW>06fEh5J3>H5(ijVtVSIRFBegYAD%Cv z%SEtbJU~bta99wCqgO*h!=VsyBjhuH#&)}0t=Q?uZ{Ca=;0TPYB=77m15NG)fQ^th zrNb1bf(rMM6Fi=wbB$ILUI5lB5zOjPg#Qzaw&k$>Jv%AlGj;GXhbqJ%4a`l}D~q{T zv+s%^X{o)$QS&?4S@w3qZ21w)x{>+_AzxS3O8LYsttUhR1KmP~XBH9E$>?+q8Ji)z3r7bIH!&T2LFBl zU8U%&*$31b)zY6iS&f)&6~n%SdMv6~;~A%AY(2cQo;V-ONHF7(;r3^1aih6Ev+xrzK=!Cq zd6Nm_j^`6`pds9Q;@UAp=vF+Hl9&cOl%vAxPeGb~5|*J48bF1@w-tJKE=ix^>CJij z?@eunADSf6{F&nmFPhoZSCb^*@%(-`Ug%ajk=l0&0;B-(=?=7{2u*zlB=2>8c;$kK z_!gi`xjoegU@=&_xuT<0CipW5QjV2v6KD@a<^aiJQZT z>Iamw<+}VY|CS7#8?Xc; zWU0tNiX~MjaIF%LA7n&~B6_GWI^t1^1sC+Pv~EBlcFfWn-;5s65*-?nXM0FCi%}t& zaip>|1H+|U&FnnVM^h}UMYZCBPwRUvkrMq*-sn>i>fV^}>S6L}f?lVZ1*VV?TH-rp zOhXE(ZPjKWkM75dzSC5>oZsuP7&RRLNDnj?eN1Y2su&ZwF2q`cE*R1L4?)GCceu=g8RCa18svxI{-SmJ z&$YLY-dx>g`DO<+R=SAG=4P$UI4>^UhDZ;8PaWP!;n1r*+1*8piZ zLa;|$JH!La`3@K2b{@YE(VooP>krSLgNQ&MFhmwl0547q7^YV*=)!HM`2ipzA)cJc z9I5}Y81CsZB*1`Uzp;FZo%`J-}H`8itG?4nuPyPU{*kg`;!! z@8Ot$T$Y1J^IXP5E|M3~mTnPdMKV^Dvatb0l4gkg(_dJA!vHBGW_21YZG){0%f%h# z`4mz`tayyjh!-qvv>T>bO0;q2Nc;&sr!1tH>Zzy`X1G%aiauYKC5+j%y+;Ac{4Kn> zD$D-g4v+)-5Qii=b6FZ1i$%iUG@Wt{^CZ)Y>t#{Wn6LLxq}A>y(#Jd4Z*ed_H|2X4 zamNLj+E`=`AbjnpnV!+TlDFVQ&}NeQ2p=uWO}}3vo|ycUA&pZ~<-zhTTOv#R?qHUE zoFtuh*auby%)9?|{4*XzC{xVi23%tso9otXUs9a49ZkiIZUE*%#%EpmVWYCyakoMb z@3+zu00y0puJx`hwcy4+24imJHfR?T5nlrFkRwyGe{US*@0jh5-cud;A?$`gU zfGEHMl@EkqDglP!&<>+L=HzHGJP@q2cSPj3d*qYydxfaPJ%YWJs0h#)+G+9wGw$`p zreD-4vk49=E;NWX>+4|ONd8Yom0GYMchLEcC%e?;4PcKapj#jle{v%bxbS%ZH^4@G$uy(h@GRvN7wglJ$Qhg^8;|#6*gf*5WFbO02 zIfk_%cq*Q_=egW0Bhh?~db#apBtS1Qkf9;0M)m7QW&6!V7M2a-$tT<2dk%XUWRZ(v zkzNMy0W;*h)DMDa@-~N1;S`K%C_B7FOOxQLz<*SBuFF2|Fu z18`0k*bIPq9gr6`s1}}>Yy&#=+j(8LB-cgzUlBfx-%Q#~1meW|BXLt{Ov_+e4gi zH-CJhVh*MNBA*!m3wc5z^yO|OWU^~Qgr&fzt%U@GO7fvBlG*SH2dSiyHWs0%KY2J( z<0p7dfu)P_2WiD3tBtN>-Ek9~))QSXb%Yo>(F?Zz*Q2k%fPchdP-Xv(tkyd@Ho^%) z6u|1hw%a2}&4Y|6&ZkUtfw;#r2jjikYU|wfpsc@n^#(6OrUgVzd5t?Ioi&dJ(%fRH zRy>L+MDW{~!X*p&9=3`evpoD#IgLTbJPMbYMRP6ZjVGRfOq%?4I2wC)agYO!et8g< zv^yCuIjyQXy10Os9*LS+jO!%>fm)@V&oc*F7u^iyN6o{goBQBTLwhPki{=RsfbsxA zZ;A$mv`ZdL;LjfMJ0ADw872S18Qnc{k}`$oCuFxN{GkTdC6dG#lncLRn@f_{Zz12y z*WRkN<^p@Z?sD7JcGdnv z2Z#wLroehIG!=dR!P`4MQ3Cj;o@9v4@4%iB9+MJ^y}29%V2dJw^Fih$LE zy9RUB4^5YPXjtC{M~&Zvx!1aeiU}l(E^&l z%pm%{YTLD>`%p7I22tcaJ6f4kRN)h}OrMLXq3n4k2O3+{xS(f1AqMVn;k$Q}Or61| zB*~BvL{$}$VuK$U;OXJAj_H442fw=CA&YDrzpnqs3kut2gPF}6InVJf8b+sFWBk#lGYWG*mT(-*;ywCnO3m8X8SCH?8=XMFj_lqhZj^g6S@|eTmvpVRgd!;pA ztvGywXDDm8e~Dy8jU&Lqjwj{qdwM;my+5iE*JZ%O3Im)nPPZ{??mF_$iFQt zC#}|4GEL~FnYU!C_5Z+M$5KP0Qx{D06_`#bIG$bbk1ZiFj%4dePasgQ96-&IqKCM^ zfGt(}+VX0JigE(ydMW!la%pVvFgJb>uy8s>eFpO?c$#d0bvSRFIe6y8YCW-6m=qBa ztd|Q(Bk4gTCH=>^&*5j7XzxC+_imCe4~<1(Y$|8db3fi@+hZB=y3rypb;KHh5}`*( zB6rOb+I$f)ub+IH1b{x&ezt?@C%Nf4gT}}1FjFI^24X94j#E-pc0~nwVEF8iGZl6C z78C8X3mKCyHV_uYL6^z($%+qI3XoX6=)xzGotv|EY>16Y@9{Ad-fi?d8~#GM+)AqC zDlHPgGq0GKgp-9GvWI-dV)zy=j#+BmQK`0d;~Z{Slg6@rOk&{p!~D9`fq(Ia_;XxQ zgBk_na~UcBrkEDKa!$ou_*syBF^rl#HvOnR;Tl14Ytgdm9A5jA$C0W<6|%}O<-IyX z`b_MPz3TKlh<@&b%5x(|B~O9C@706Mq1Q#b<_$TH`pR-$SjA3xny09Z&&G0$XJH0)9Z%mU8r~d6i z155L+3DdaPdOq5tze4^Us~pyw3Rq|{*`Vzn1IF?vtb7pnR-7Q>u-@W1R}@CfUB2CT zTul7-2p_E8dn3s=L+bAR?`h)VGZj{~Q3{qOx{fk%V{f!R{$W9;eJBk)IiSO~`#5i+ zBF}pHnpXg0^xe{fYDZ zowO{}`Y+m_Q?z)Di)BsS_;Ky5X7qBo`>Q#X{VW2ZE$Q@9XckGy#ktR+3EayG(0tR7 zbw0WxOmGF;uvz@xt1UYk(p5h{wZXv|6%Z-RF6dP!%B#hG078XY$Fqju z#`h#3_9bb-T&n7jORq5_bP)ci03(pHAS}=$bECIB39bL(QBymcF?sk61x=jW`Vsf5 zUxPIA9+TB-!`w>p#7|k2F%4|F{c@JZAX^M;I;ry3$XU4ZKhWThzA*x;RJ&3 zE{^zu#5GV2cREk->c$V?sI6;)*W+KwZpJDG{+(?sj7&9X2ZUxk+9j<*CFa$il%Kh)P?y-B6C^winGzpv@#9cjv_vq0EErJ))ej z{2dJwc8urxl!Mg#f+UxGm%Wcg+3pSe_wY24<}?Pr$Vm^2G%$jWsfgsy3<9joV!=7f z3X$6~D%9@fyN)Dg)#{Rv>*WX5vfL4_^e`3Y@EFcVF%kj&e_>`DGdXWz@46DFu1U3o; zW75qJyvt-#N_Js{eOE;`SL&I}G+crqh3;}PV#=;DD;`Ie1Bub=9M4t`iO*6C=^6~o z*4BJ#-^GY`BV=8y_%WYNXg*|=m`3KD1Fo9lSKb<#ga{&Dhsf5Krx$0C2IGD-S)a6`$$;=%B%4HuAqWGnflA7Gu+m@w8t^_RJRvY| z`0}TV&+g~o)K|X$x@5p{8~@5zSA90F_D<*bv#PC)yfD7g=*gEglq|;9o==A>GnF1~ zD)Nv)kq9$!mgGeI{rj>7#31-IOnLC4e9m$V?bg4{IT1G^?wfVAPI%Vd^VmMO1?zoe ztP*48-I6yiCa2nR7@Loj)kA;S_6X@}Cr!X)YJ9o!iKZNCj5Y~O`DJ_Dx423Yn|GE$ zjK1RLO+)x}R9+^US3ELy3AISR5U^n%9YtJlVE!Q>UaH}ad`*DbcY&1&a)lt6IXRqP zF(v7Z=N@K@Qe9W63Ovo!!jO@W(PGn>^HQThqKxckzhsA>FC~T6ZOGfiV8z}+#m@>J zAW7e6jL?G^m4&}jDh=let&ZYsXBzBMu~^_)Q0Mu;E2;Itnz93%o4l>Mevth0c_@5y z4(=!tkg>8rZ+}M6Jj19CO!*>#lt@0610&=F?RzHxHai38cl0LR&meI!vK1gZh&e4U zo0pBx3y7I1qFw#|o*zwT-TZ3o%JbeE8%v?}JX`5|#F{0#*_KWgVX+7nwrqx~Y4FB=}Y8!t+%oO7IBlLwXG^???1Sn;V-DVfcwNB@R%M2Pr{O1mJtB|FD``p0I7i;t(bNd% z(5)US$Yn4{vUriDuf`|57-DjR?LMfLOZ(2$6*AV~XwfUDWx0fnj6lGfL!FgT`sTF2 zWtA+j51`9&lZsIiP8*#%;*X#roslwCD~iCYK&wh?NHvsa*|u@4#C{M{v%pFx-bqAb z!!_vqm0%mKk7c$K;z2u6&4Ogz@eO{^gdS!P(>CXTx++mq6hz1jI+GKHW~J15U(){x zsaxC0_v~hfJZg}Drb(jiym~qZd%A2+D}6BHy!0Q5{|y#L6(DYI63q1vdO&Q*!U+ec zlLywX!R*s<{`1C413>?Xcwefp=r?%-el~k2h&==YcF$z;XW+5gzWe>JKctX{yi_5h z`>e7B=xV@dvzFXTlY^V1k3plP-`tOzwbdpkV|eKLB# zGvfd&&o3-h$;UE(a0aSDwDuov&C}>XmA=<$Rn2ab-^`66>3IU!X`<9cyA(n({`>=z zP|VDAu*6Ph)&on)74Qea+5zRAh%b;`0O5yu&kCn$Z(zXdBfJmrQtXEMg0NPQU&W^1 zbOOl5RIA;QQGae`1*ZOmV7>rsp2M#o4R7%7eEH8>a}{{KfY3hs^_H4Opq6`6@`-aTS_6D_z;F)52YA-6>lGuhgdrAE zAO^z({HFmO*8(Biq)NL6JRpd%+~9;_cH}JqVUYjr`VKQpatxStnYTvBzM#~pHSjvF ziQ3990K`1_K)Y?B%^?(MArjyN^&F-f=w|Up`hjp-@1Q$f5bXvgrREfE2ABSTPRyJS zdeGZb?5$<5y}_vbfLB5$SOt`iFk1as?hYPS<`kxPr)RP{oul(jb{d+Z6fq4`9#jqtOF(_ndAS!8oCvf8`>p8NEf->_qP%e)M+ zlkSGm?Gbr;(;$tXxViubAw3|*o!p$OpqSePL4KjT%Ou#}iw|qThhe9MZZLzzn)~4)92>uJbDj-wV_kg~?*o;{|AuO6%s#h>E;d?Vdx;Z?X0XYB|YyLZ%YuzGFt zAZ*MLas720MK#VjZP21U?9e{xbIOobOdQord{qKMEpW{cJlKGnJUad&;O!;CxFP6W z1H^5pO0Hm-!wI^@KOO(|k3fQQ@&imD8^E&SCCY;$`r-eF}pr1#zdQ4gOgvFU{&S_ABRgda>FZ2>;bVU9a}Y>DMT)uGkcc8#9Ei|KQaUf!G9nlA* zZyrtOez{xJ5h6tj&_PiHg)T_l)3}{#J*3g$ao!iSDaiLM#+?LnL7Z0qkEgc^tLl5dzBk>t>DqL6NK1D&(k&s~ zp>%hHAkv_eG)i|Uoq}|iG}4IA^!tB*?RXSTCY#}5IPGNc7jur2?$B21veBHXrd0K4+*P_EJw6N?ia=tPa@fLq|W;M9^9j;U`C5 z{knmEqBja2Vo%{1ixcb?aOk7KE`{euH^OfuCtmjp4P-@P3y*$cWF0`sDUw9qGPEd{pT{G*~2W?EH!&nt-iOJ3uT7WO(Fv`|8yem?@@(R=~dxaSG6lSjlk zf^cn-hlt}TM_mj)WB_Gbr20ni$jJ$E%ha5F%K&YJZr*QXi18b&{}~ zp_aK`G$?i5Zj**|J_`>HSH!nM(H?dICD!g(4hq&i8`!yXnEfBCn%lh}^=BP3>4C(t5N9htelfx*)_ zqngs9gIY+9x+Tooajlb_cecvl`ET=}hhqUmdJ`m#S)l3C5WVrVcwV^ojJiN_C>UL^ z8(nxqUt>SKO*3mAGz4*Zm(e)1kXdPZ;lCj6@wtpFn8Egj43K!72?+1?>VJK6bvPu& z3p&5j@QuEx7j^q{Z41y)(lVAJDg6t(MB2;x5sp5bf3uEWIuHb75dmPxZA>LTr>ph; z(&W?}VdeMN$8n!Q!O3BhOQ1`_jW7L1>_@|%RZ_s8IXx(Qp{rvlAT}I;HRX&m9F?^^ zE~%3UU%0076Y;n66*eJ+A?+x)n1jX zAR{M*MYr;)!ENhD&{wes#~Ki*S;%;~j&K9UBievZ?f-E66@Y<%=zw@f)s8o!58G+# zo`<``*!NwZ7?DWHLrb;^|+P z(T2}wp(Tn-8$ozEVi!{jdy6(>Mu<4E_T-c*X&eD*QR1+NWnEh}3RdIK-*-fjtFj+g z%03x>c;O*T*|rh!2_Z+%+#4D^{_6{V?Exd2C+*XA6_M+kA0WAVgRQ?CW2015d12gP zt|DTAM)(o+((829L{O)f`Y7w7KJko<+D0x)DjXX1p_TuLp_^`BDClOz2bHEonJDm9 z_%Tsvzq$s{!>_E5xwkEJMn&oGUhfeCFicELN#sJ;%?1r z0XV9ta=c&!1=e{?(84ru#uvM?35WY~Re)R#K%DrOyt;`E>YukKEqm^Jg2nXP_nmQqg1dG^ZcO1!joS|LmNsM5t?XPCas=hf)I`bdms!@$5r`u3(%EqT z(z^YAVZF<*4vlUbl;nN+2Z!v}s?(von(;-=R zKoR;dD)T$zg{;-Hr2krMJIC?c->S6)U5^*WOuxxiXOL;?fUIEZ5VQcZi;KBkXri z9#6KJ0?fU~Noy3XwmS>b=#@Rp4yK&Zf8poLnmD~qO z_NY`)*?omP_can>c?~P&+KgA|V4q238F5Sj2 z_eAB1U5TevAORO{!$l1b(;tXN0y(6_Q8H{nH{BS#lLpZtS6<*t1gsS@Cj;QPC(twA z;QD5yW6U$F`N~0YdOzWeH?cJFsnvILY)MTu?bn5=+y6Nm7{Hn(Wjj8G;*Z3&H*&c0tP=wO-kP~ zL~0l8^CZe=64nj>Zz{>=abU62zl9|3ukLDJEGLU zy88=fBIE_lW37u2jhf$|`yU-qp#(^kH)Q~40`_AT7gM)<%J&nb9rk+DKJ+X^O!qo7 zYR4?dhQ?{}0ET4xyJ$*Q+~)*V6qpz_It?nLsnV$A=Ro77wx<|V{tuscx&(Y#RZC0L zfaT)peBLA?oQaB46qaC04-W&9OG$wT&;v_lnFA5XLK{#uucJ%yO;XCuB1qz??6l{L z`<_u*(nAfQn%RFFFTLhXqI~xOI|gW=_QDHn#oV{Q`K$$e1sJA!Ar9`mh99e7G_&Ca zhC48qAdmx>2F!pUs<4fM4wMW)B`w{IVp#xI0z$+yAT(8j^@cbR_zJ;z2~iRyTyt9o z{1<)jA(J7ws5;al)NI*5n;eK2eGa9Eg_42t#E4r9ibUxiZPb#+!ar^gMLj5x0fKLa$B>m$Lbl@$Kv z2`a6@yyRZoOcp|T>{~$%E2DaGu7SwVj$K`xs~lrwzl~2BY6_V038X{&`b2KgV)~H3 zBu9BMvsc_@JCS6H`B~tt#oc*b9GHEG14oahR)qzF*mIKV80JUy3Chr`s78jOHEZ&U zkov?+n;De*lcSK04BU~wK$m-kA`wU3TDN|`=Pw${6(8JsBAXQ!i6NP$tiy%k@d=R= zQE@{m>`edF3h-9+1Y<|1LZtRo_H)>-b5ISAdY&_mgj?!2diCJ-|rxne1#)Z@3? zJs%)nOh(&VJ~{>u^ssY%;O+$6I;Xx=d9aNsWd&aHl-4(fT=tgHYn$od*n4o9%fAGQ zqn;I)P&2XRGDL3~i<3R|9Zs{a`YS*9m-(*;)xh#rp1Byjg!{U2&cq_!9l34RD^22^ zFSZBsa#`jhW+g#r<}Yw3>Q(7)_{S2MeQRLF2RX|}#sz;5PR1pqzEWcH9 z%zJwx`PYA;)?y*({73olKU3kKZJ;pS3L}*415ZPk65Z{ehXq}_1P68slxGDZDD|aN_M50qVw+K?B20fACA#)R~ZBNeL8B zna_FQyxDmphAkDSt0|ZO6MOLNy~)liAlb8pexN84%6Yy(gN+^{0hjCE-~0?7Q;tN9 zY;K1+TbktC;3+|?lBnhI*h__Q+HZ0M<22Uc)uj<|NJSx#tjFI^U7iE;DvrIi-n|VPLXh=W<*E^Ibha3Cq66vSw;x`{iJUyh!ndW!5-I#l<-Fp_YNJcR( zl;D+40@P?vndZ^b3R>}61c`>`un!`Bq@mVG=hVuqTN&gzul=n}QSp(g;mQdXB^DcO zeL_MboCI@iUw3nqnv7Ga!7+90?2@gdy)cmyC9H7>{ zdN>V=Z$HBn6`^7&`cmCV*=jM+vHnZ!f&cGH#Z7V}2~S7_uX6GD2SNUwt6w-PWp95e z6CTAlF*8f*`Cm_m1__{sT*YPN2CgZCw&bgeAklLGwk(Lr*^5DdOs)3Qzi86v7lRPc2W7|vG%?pK$b=0Z*jmY70ruXWxFD2jy< zNrpOwp(J>y%Heuv#?M3p%`r$!TuW>y-U4?@_3YCfD<|5}yS zo>)vkYz=9&x$z$91|$Ya$gb|tcXe-}K|Rg}-eZmo1)jZ{>z!JqRv!7SX=KtHS=jp{ zDeS!6KOXL!n`cJLJssviiZcC9a8L%Ui8eusam@L|C_!{EtaDkWs=c(ytUK_E}eqP)bHqc+Kd~iH7i-X))T`*%`Z17o6lW zwN2FIkQAod5nOWZQdsU%`Olw)%VGs-V}6l6{1v{)Wy{LPv~vbC6|(jIqd4wAxRgcc z)S}#oxv?8GXK|Yqo#UlnOA&8HAC785ygw@~sGRi)e$c@bVAW1=3&!1o)Zj>!G;mqN zRnI|Mk6AW~1mZLMFLbc$uJBG{*|Cx>uyM_@Xgy`GU>h&`Itiljrg3m_f`3X}V2Y3< zmd*xSHq!}2Y>#sjXuEHbQhhm^mF0!kz$G312;9A;BY*nw)CEztgl56Q1#D25wdDOa zBR}W6K&u7Y=n7bLg_kEjVz#r>*Y4~+Oky=}PbL6HI;b_dp#PA4rD{dzzAPyCxYl_^ zl{pvCzew38+ zfH;bOmIC2h=?tu!n149p85VLy(65z+Ah(M2{-Q83vxR>~G$h)q$A6^W;$_s0u zy+)d>cs%BQN-;cGohW%7zMo zAVRUaBf&__g$N>p548^G5GQ5e=4iH!+8E_$StM!^8fZ*%XgZ%lh`qxP%Mje0LJjS7 z43xU}%_>+X8SgLV?W%Jzp_U>(;fr?LKTE5Gxor&CAxC8hh%-(_fq ze~WZI)a=nhmjXKD5mt&He}5jVOJXBGWpWCh^gcvB$V#VVmsZ1g!%j_1Ia=adw( z4Tswl^?(#gSwbW!jfTx;`@TYeaId8d{f83(()nL=m3xsNguRq*r(<(JI(K@vzb6js z(9}#fQ;rdi+b-7sn1HQ4yq}#qTkgJMAt0V&FOHW3^ik0Z&Rl3r;VhJHIydXZt4sR1 z{?T$+0bg|9Y&iN}9o8CT5+E&sBog%2=)TCZhs4=NH25;f{46n&%9S#B+)BfG9r-HR z0&OGucV-w=5!xDTcf7^&gKZiVjUb5HMXy|I0PAO+l{}EmQfJk>Eu_IuqONx3vN>$fjIiPtT3RoJP+7r5(5ZZTp*t2Z zAd<{x+lq7xp4tEm1Fj=o>Of+Y1gtFx9D4Mw0yh6kepr0gYvRlX(F_EYC2v8GX5n3P z>{a_bdS|XE8nYy6^NV2(4G2C*$r}gdpQpnZNT34KkkVK;0mG^I5MrxHo5kZsdv1slnHs>3Y|=# zYt^3uS_me|I$YK3y|N!2@v_k`6JH9Se*Sh5BKLRlJVK&fIhBRbt?ZF|la?BlA4iq^ z7Zg_dwS2_vYqa!No`Kdxt<5+VUZH1O(a#4hcJ7q*bdubB-@g}rYaZX{ziS8Lb(7&5 zfku>T5hD*pE##`3wlH;$YEahL?NSRnPFtw=JKB}mFkH0-0)FOEM2TzIDtxqZ{G7cm$p@W!DIDb?|}1xJ@7%Mv?P(g zxE>^{>AGJ0RY0UrVqN}(|L(mKMJ%Ar35nu?W8NfPM%xZi^X)A23*T%F^o(Bw#*$XC zwUkwisg7}i;lw*M*ul4Kn~<}JG$y_d{`1C|05Aw(qXQP`ybqz#f8_{_;``=lzQa%b zyxH08Q(@%S57d#xM8WyFu7L*VOp|XObRyg!OsmrmN4jWBMNbt9p2Shba{69j9fp-5 zzs%N!_7+QFkWOVG74k&(GdrIunP(mlriNLL{p2as<4%J0SQcFK1Wzuw6lY5X2X(1o zUMQdDuDl{#z6P3mv`irEg5AbM%FKNLS=CpJS$`zyf>_f_DkixUhytL=!k+)ad`Z1J z(?2Ro1EbsD(dQFl_xn~rVgLq^^ucnnPS~60`5TB+sT%nFs#A%IDqBYDZq#Y^-d{@N z^Zp6hIM^=UE`4~!uTd#eFI${`#goFqQ(Dl>`>t!J`JbNV1C}cKA>*v^Jp)o11+&IS z#_(v#6jUUE{Rj^w1*zNi(l;dqH7_i`rQ7#P7V_&FzjmP+)@X7KS#e!w0?;$BDIV_# z6K782p=XZh+~`?MERk_ep=k_sY2RX=|0I?qFXKU%>F9exUAiu@zg#v?K1Q-u*E0Cb zZyby9NpG~wuW6KZxbJ%(sdfc(GS@R(9vAUSWL<2wjqhE8R(jSX7R86FG`#S#Xc|bA zSNF|TG2OBe5K^JN3}y0}S4+*I(zWl2UGVS`zFRByWQP7gxbY3sQKwTd@WtP$z?w@H zF1q?o+ry-?mVKX#I_7kz$y#Lb=otV`1hF; zAx^yt>c?%%CE}^8kY$%)R%M`JGfGa?JZG8qv)`&X7KC%DN5%ABk#1MkiKHw%+e+Ty`mWMOjQPSfi(z+HGXAY= z7JeQ+4ZEi16<;zIJ}bxJb9q_^t?g22nFi_MgO_UO!Ngl7U=I^Vx2+p~#sa6tX4C*Z zv2(6+6zkv8Nv!n`h}+@acKF$bgN`C$I<}FXI)rTDDSPb_ecDL?*$TyuQAb~I;M(p+ z6=UPu;~8xdkWknFD}cz|>U3Ywd3pxr@z2R&DfOAvx4bEE@%JlIP7gM$8+(FPNySGYs|O%%EKE@U`d zM%Gywc$-^Jq#kMh%0V=lOOy*#e$*Xou`zRZA39$-(&l|4I|Z$Y{RM0+ILZxmz#0wl zwg}=T>Z&uTJ#E(XTH*SUT{W#1zP4MCm?)cpb+GUYey^B#?fWxQmH>Z>Ey%h>V(^kK z-I635k*oU7q!n!aEenbfX#p~fn4(%lqB#a>7xl6t8GE3UKtZ!4Y%Dw`QN)vqcp=3& zDt=RWy)O#8jv5ezM&EH#8eh=8`x?Yfl9r=rFF*t@i{w=H=M~GNE*;{>EfSB(a9ms! zZMA|$Te-v7t5IPE=dsl^{yUxDFWD&gC{zPtNv#6#bgZMaLFWH> zvd`$lt1xT~&}2p>g(B}#9reJdJfFeW^}wK#!k%36;Rajk-1NA_2f?mrZ>ye0gl~Vd z)Jik5v|y8v3$Ba$qm4$cx9!ZE1|e`LDp_bC^zV8WY8FhNKfFZcv%zC;#Cc;wXCkB9 zO3_%T5D3&<&%_@KWaBacm-%XeHl4#nD}SI7Y<#dxAc|>&2MY^ZscJWp}Soc19q_zTBcOQqROI!$}vhk|$WF-W9=- zR~^Ru9|ti*i;9z%Vq4XJt2sLCKC!m_7k%l)42zVTVSnM&JNt7|dy!Rb>s%CT3H(n9 z_M@{Qf=YIZXR|A0xh8bi2`mz!Uu==LG$NEGg7Mc^QDf~+u-@^4{HH9Svbk1{0$|T$P1xSBYW?nmqB^URz(vem8~b2bj~JC1w2xLQrc95a6lFwTSDCe)NgbLT@E}-M~5q+vsQg&PTOets2t~0MxVc z9@R712Mi1-O1W+&VJ{8X&U~rozK>N#EBf~;Gk>)I7qk%gu70j1%NDG-rR*Pa;QL5#z=wC0it^lB+WI#g z2bu0;d`hnI8-I>`ozEEe2C@!QVvAarwB;=^kIogigW+Dbd%#y5sXsTHBUp}kBnltDh>Jb z<9=mAC#lKdzY;OzN`~*oJY(W%fh79hU2aWu8GWT0O<}dIwj79)=9$qm3k>Nx zZGE5+45ITfL=Rz(JxD6zHq*y%&x&3qDnHZ9)iy3jV5;DwlL%NwRt8A&HR2+du0O~w ztU6_YJ0A5)G8P3f;vd*~0Dzq@hDU(J?DdbTe~W1D+ltCX3#uaGt6yfjCH9jAJ=e!e zra^z20LaGj7DVH2pmr~S*Bva1F!5-vr|PsB&%UCD&LD+3GcD@>mvJ(XWG51tWe4lc z`Zlw;I`&*1aX6E1k0x=FHZOGXCsDf+rKGq*Dh2cOX!TQs&aZM``N{Q437lpUEuJJw- zVxNVu5V~oOZTfmWZ1c;1uRDvgE-|;W68;-K@~=n!Woa{^vTbT zHuJ(>Q&fT{7Up3qnV;L5m6*K2dXx*X`*T_&jTuCOYxpKuk_hsy>H1%B%oah9w$U2_ zQl(oFqjQoKPqHQgj)d$Za6OXWygy-7Qjk?CmpDd+o~OQ3+*Zz%jAy7vD~Xem^JvM$tN@+tl=eBae+~)_Nf*p)RXtq z4e`CXrwL|!2j*{bc3cb=*{j`4AFX+W!h%sNY<);d{IL>-FTVy+=v%A58k2f7!~~Pf zWzp1w7gMn`k;?(m{eLhML5-0_{`}3(aE5#mg%<+Ik1yC-cs2IE3FzC&UwC;Zsqs#3 zNJk<6{jCiS0hjiJkMSft)rL*DYb-fwC0P&KHXcU{bwC*auC^Bl&s-64aicjLjS*CwbR|uWWKZ+|%$)dqIH}WZ zI*l&lf1<79o#_=|gl-E&is5O*&)^Ze=C-^+*|$c-0e}bsqlaY4{1s(Y+Mf^uy`m!h z#aGdWk*VSu1#{9!v9};UQBSpWwRf?|p*fnon=T(8LyA-Vzw=sq@qCQf$`!9**faQK zJY-R)hqOH?8(Ou6DnHZgZ#Xdc3Sy%h+O{awU=qQ(9!+lC*;|iNuE#T#q8y;?Q0}9Qc7EZ12Lr+){4 z7!jaP0_xeCmXA5bOQ2DbOCt4J?+%G2Ul2l65I!_VObe0Ga-LJy=<1Z{j_5_47G&q4i^p5vTTO91<(2D z+QGQ6yFfDBf?WYHpn&zSLKoUlByDL3Qn1-D4o(pZ+!;EYCl(*BQTt+uy--e}&l?TcT-t}ct->$!@nW1nTPr~iCL zD$NBe3;_b0#b_4bj0POo$Y+#aoR1ADe?)slE9sOQUfu0I{P?VQy5!RNv%`TXJTn2G zNx#;1V#uZSZWqWKCO!xuAUr!-`#Uyo_6|T*)bsa7GC0Kn^i95ZidG)r;_}7+dQ4n7 zgG*p9KgfH3T~huLT)~?84mE8Y0EX~S@z76a_5`ST-VxBobML=CP=ULv`3>K=*DftP zKWJp*QolX8-7rEmcNy)e%OjW&8A^M2d9r-p9X_JgArRqfm8HQHepm8;5M|D+b~l%{ z)7km{oo=dtEacKliRV8KFj{Lk^T!QgS>{$aF7t|o!Pgbv(kl>e?W{+cw__`_do9bE zqU(jUoYX>f_UJ1|9cEq$npQN;*LatyvgtN>sH@mp5ZRf|l{w6Cao20)xDQ9ysWofG zR(BigX{ORe7oBiUJ-7#Evnp><$}iHPW)#_beduD*rHA&NUWM0O{?sUI6WO#I9~2gfCx0WWQ?l?v z4kGif!whM>_Kw|W`1U1@y>{veWEDzx>1c<&rRKhG6SLT*%D5p5f=_kFk?w9i42YP6 z))%;o&jE~c?d%@hxV;Yjs35uGH7!d(Bqrcbv;!q#egOY|!n~PLmvinYExsI3<#2Sh zEAFxxRNDn2t#X*AHX0(jMS(5q?zij)tHbBx-Yod67r zl+O{LwrryeKO$L2vX1FEgv%KLZ{?e|`yVtf+k(NP(q^;wI;k!ck7g zZpR;x`(><}!35H?zf$*lWVQTmxB4#2)O0;!XPV3 z%Qrv|gVZw5Kk(ZLvB1`$1ijX0{DbubSPj^fr}!ULcbuBUzij)cFreM!iX zS%Y2`hPTLHYpE^^nyQ3A3XB}fz{vhW+R4Q+Hs{Q zD`c1o)pA?Sfqo7NgP`^J`UUXrMKn3gr?cyEfUHwMV00@21g!~oVFmPX(lNlsg~kYa zU2Na115I!tAPB!fVz;0!}l~+MkTf=(QD8sre4er!IM18 zuUA%{HyT^ahJMXQOAB+6iuvb<*XeE)c#0BaMED(#7b6@eM_NGY0}gOPTPs>)rSNZW zfzFaz@alAplF~a#?4F5^Y$GQO+{TiPvo&U2He>Gr@%seepy7)pG5$9T@R0%o5QAFt z9sn(Dw3~w0qTets0wICm6|!IJ6pe9Q7zc%b+&Qs3X^kZAC+___TE~!E*${Z7e;&0K z4_j1)Df_Kb+Cy4`IhjYbP1mP-o_Fjft*$+xZ%^Aef&A#GF}5t}Ik16#9X_SQ*5rg< zX3%h%kLP>rKHly9^Vw8nG8q&ZT-pGR&sa7qqvOt?|+w_H#DT>k-4F-`sS%o zGg<9-@-a1+hFB}&^zrQDR?d-$X$@G~|j$YM6PH6bI~LXhCBXCk{wT9PR=>YL96 z+^gk&+j_Wu_|Q>w&f7ao{tX_Uh-d>&_|cB?i%xE)e392DFNk<2RCd*|#AacSX&i#0 zmIXEFj#J8D-AcmKFEH;Rb8i3MBB#;gKG@*a7n%F_cr>5$wbMZ6XCn~8X4vUJEmRED z=%&DFl(*7$xdUwQCoul+9KmqN^<#v~HNVTM zai_oEc9mu^L#GZ2_og1ASh5GOErLc*6}`sP3iujn1IYsi2-u-pw=VNRKqnzN8#Wff zMI=k^Yu2CRFq*5KTHiSmr1 z-$8%71WRsSNy?^1uA*FV?H{k+IhA+M>q$4DFi*Z@@qq3EObIVfH zva9=NbiVQ5loWmc0a_b$1{|k;MMI#<$1Kmljk)t+8SJInm1fn=-V*V+t+h}Ee8!nC z?Bft%ppHN7pqf>8>Et`G^Iw{gGl`noY}6!U>#o(L-((NNB7_j?L-_nPWkcL<7p?6Z zHc1NnKZp*(xeW-)IOw9pX_>_%IC5jpTgRhsC9gAtWkbn8Pb=@3I!R>u0jXPthXj(& zn1uUTnKIGGsxPJJuKE>C1*zS>Njt)Befv2^hIw2e{W3SVS_b=klhH>_4Dk}FFza6vhf^#T{~+9L$fS& z&NEr`aiKwwH~>6jKr{=A4ziyfK-La;GKmy?;0gWRAbS??{xWELIQQYlk~5i*B_cz} zBt{u9q9@7nzIuBe^8m`xt>yb{+dS1b-)mh9i~Yf@ALqh#VIh>!)eR?tiJ8F;A^MB< z5~`&YnP^ z0?+f?CGiXgxKv?qsg7{8IfA(#abh$7n{U8OX3}J@3$9*pnnpfDWhCg`0ouktp!QWt zX9@`c%}2;&=@2yA4!YTSv;5xy zb)e3A$Z-MZ`j`NS4oKvu!K?!gI*+~ae?8J+WeuULM`2t^4X9E)!>I-nX8x8#w|`u{D^|y$BS2Bvf1@} zT6qDJsxfW@MCJw^)eQ}YcLo4#JR2gZhM-un8>t3RDSO~BSEBXJ18O@`sEhnrgor(` z9t#Xv>rQ_V@_5hh6zg4Bnn8Q&3xuPQJFY;tO!*?PO8r!fv*U648xAdZ@!OLQB0r4@ zq}06YY)f`?jCOE2x{ z?s=Z5!(4R$aCcbE4D2>q-nnB|X*SqAU?10&7XaI6UeI|dk@R#X#x%>z3SZr}TKH&5 z!7=mioAmk`d=7I*(@m8#n6-s!AGj>W4*BfduSn~b5+hh37h~9cz zN3U#bkYHHIy?euuN4m&%p*cwf&HFgZ?G$t66!y+h|MalCV9 z2d}+M%x2k4v_;=@+wY;m>bbo14qvL0e*0q%)uCApeKu*}x2-nzI-GL~l=b+Sc+^FT zAsmnJ>LqvdVcr1dxRn&#t9WThijk?tP@O@{{WXbp4+zm!RjcBTNv42?qAQthDu2nkax-&wKw*zX5K^bg$TKWp~=r- zw62Gfx5gA-u)f&&S;tMPk%WLsCSH8}3XDPA$K_-z44X)wP_0MN<=;FUKaC@8lHM$P z)Q|=CVF_#|Rfe@%81KtaF$}%nH>Q=)BrQzPXNzWSEOcwIntoOk#a}ckP&zL;$+RV+ zRJGK+k7j1Q2V(O?O@Hq|Iem9B^Z;$weZ^~@y6Oz*H;PZgoe{>5{_{UlxB#T7=VI6G zW*iEIFQh}#({rX&Icgt4>e)Vx@Kb{_Dtf`gn;~+ag2$bacWtJW!npg}Ye3X}YfLQS z%XL3J9*g_iWy+=fS9RN@th@{0Ost)*vt zBDU0Cgqtn{RS&nB=YS@C<09Hb3M~q{e7*;0eV9tlZlK0`E&!bJ8&@anr$2YS9UcuBb5 z)dU<^rIdkZ7GP@($Z`~jmKqZPsj&i`_CPNF6!1}>Rv!b^74^Y>>I0}}sC7aLzV}F@ zO#SlyvZ`9=^JwMMS8WV63qhodsVK@H)!HcMsBFpfibypwZD)Yv;7?d3db`2 zFz|RD{3mVo%sj@%zOL2>9=FzXJ|1>HoU)n~!oRH2E@2~Je~G`u*csjj6J;$O8mr2F zp2O>Ozx`bORHpqPiV|e@&hVtaunBj$IB9>V{_}86{bsY>*TYcr8YWbI;H~-ukMvPB z`+n@5S;y~1RyEr@s~<;$svJQT^Uq&=J-ywntZ}1zz4XL_Nh$O=5AK|`H$%sM7rS>J z*OdAGw>TGqE-RT}FhTE-St(4{Bg%9aCz%g&70Dt?%_ntHZ=szpUsc=FxTE zylJYl4XN4(0<>5ng9LzUBw1@2A>#3Cc+KzXJu+E7dDp#Y}ZNt zWVl@aD@b(jdSKhpz=sF9^Nr~Uh)@Ix_{QmL)VIt6vdNP_L+q0n5vGh*2uojXa?oRA@*g`1FILhQ#J1anO0g$J?1w!QlL%V6 zS-|-{iZf8ARf^8C2F4{|fm(&xvH61_rTYB`fsJTRbFyxgn&wS(kB>}v^a6I15Y^-k zv#1Kan$_xn7vbVOHlw$DltH`+`#;5sYztHXzaz2fdRm#2nq{w%vdZ}P=oGAK7!>j} zbRqZ6#hz&&6|EB|wQ{~V)pdHGEXRagN_j2nS7_sh`l^ERr*^V7Mpzk*?jq7;!ys6L zia#>i>-eXkd3L2mNMjs6YFdd2Qw+vqi=V{?@FCZFyyF;%BbWe-{Wn|sF(){V0G%7B zFtV=Hr}TPWjLykgpVV*`5K=UrmuY~)0vL%c*AF$MJ8qsHNco(@J$gwCBU{a%9^H3F z_*&a>2-Q9>HbEC9wo&_VO6Bxa0b3$!2tjR5(|3r-=Olp$z|AG?X*G} zC68-WnqyHrDI>cca%eM)+Mrj;>_@*a6~vpqy%+?I?)N-VqfZ>UbuA}9o0gbmUY$%b zm#1Y`YEZEHJhl5}L2~*Z{VqWei$=SQn@*e8693)BUcR_bUU5GdE1kCd?KA`q9cIJd z`3Zkv!oIZ*Ume`{hD|{H$OtLFe!VZ6ox{SFPBcDJa=TeV!g;smu;99zI@1r|h2aaXJ^h6!%KkSCKtc#{+X=xy?!jr|#$(zVmw}mnk-~>JpcrG$nZ;2l z-*6q%;3=N_z>z+dHKKFT6yh}?$^8lcvqdV)cI6Ra6`@esmsjDKM?0uCrFGT!jAXx&|!-#N>LFAk;j2$iT_Uk_9qc$GWNA=8a07A4nD zB6lM-yVCA>sZHJ$0X2C`4&oG=g}8d+#{S|4Qm35>NZV>1b58fDM`-Q;7Y751<3wi> zDdKED4GM&*1F3gpf8^S|#d{-?2yDra!oLU<;f0=HiCLa~#R!$y&v-~Ae^Er zBK*5k1DK}HWdDG*$>g5==MUsCg_G1k)Lh+zhXi79<1T^yNE*M)ep?Y=p_V52t2FBhRQpb)6TZvzi>Cf(EIjRfS^$KF_2(^vR3$_lN)KF%Nja-J4=mqPQGRHyw{58| zpZM!>*(ZhP5?B{IV{Ol8F}+V7)t$>gJz~=EW-LuK-&MMTXRizTno_=aglxX8>S>x@ znK6;F%ls+y#c3IpBhp2iz`~UGkwR$p^~ujL1}cs%83Oj*rord76KeS&_hEU= zn-INEQF*-ErZm0MVxezudyUcEUad50JEVvsZ< z#91bmau8=Er1-rn`Y<2%YAWQr@lRfUp|^_LUa-{H#>OJ!TdsdyU5-eSR?jJq=iP$u zn^kFyNHd)+YRH^?=uo~b#J$SZPYJ*MAaY3eY%MMViZdQ5H|inFlOUEh5&WcmR(zPpNc<*SY zSw|<1RD1=xq~5)IRe=>uO`N&74pipOW;v>m5T(qDFfG zH)au9>%M~E+t3y69<4w3S5tn3@KY@TAg>E_-7f$e2F!QG0m$Qs1jf_?R<(a^Pg&Nh znOdJnTHD%;TBW7XdL$e*5s%=P<`Zw4jKp6^;%g|8zE0O@m2Pd5cJI7g?(p$(_qkqu z5E0pb3?j-~0dB_e57`|)6G65%+-(0wM`mx2o5-A-sq69Jm~JLsc51-<5FaTiS2+wD zQ7Of?UV|=_3nsgYXyq6$`T?s1I=v(S4`oWC{?&?lw(%t`be=~p;1-b>*Mn3hP~Qu& z2v!74w=gtv|Aj@~i_AJ3- z^$MXS9HPdieih`(CY13;%+`4}<88e}iF%v-eL5e}RjpOGTQvHtSHa{efKo9p3ePpW zhRa4Rdum&;Z6Xh9nT+S}!k%25h!ph2CZUz8eoh0f<# zqLHm6k=@w3G#MT_;r*kZ&kxT(a4!aA`$ZL#<-a+}F5~Vo4&F0S#YD3Z!j?@%eBYfU zFy%-{mbXPEl`Pl%d)pOO;2?YZM^MKp-ez#j4FVzXrhwzf`?Mj1jk=mj``euWvQmTz z6&8NOG2A>rASXQ<1ChX6%0i&R9NQQ{sk1@wU`MGofZ>7*v9Yt|7iNkxTu=`Z@O9OT%@~u z>F$PILJ;ZhR_R<)QUU2MMNp)>@dD{a8U#g235jRCKhJ+(sV`7x&zbqnHwiVX=7#2V z%1SXglMsxFIkV9H0YYT`;hBIpvvoi}pH_o@5$Ie3qsQ!;DFdoExbkFjn?P($B$`1S z833@sW=T5Ka6@BQ;bS%O3BVWPn1ymu894e6|8~F8){h$Ovckia;~*G=d_ItcQhkiU z$cr9*$KIl`ALCxv2(9pz9_A%)+8B}SK3_+z+6{W1{3$CL?g79;Fm<%hnyd8_B(wxj1ndux;O+JK}+CoOGkrOj|}sw8)nK3 zQ75pdzZ1DZLbVb-;lv6O!tjs;rI-t@&m%Ibk9!%(7)WftczOiPdf=RfI}`8WgVqAY zMKgnL17UZpQgRuf3^aIN1nM~LW`=ZraU#g!4tArtP1-rrYOm-Jdt-8?fPPRESG6S0 zV(QGu32pK?Y8%pdEApC;f^7BU%U887GegPhVO9%d2}%2OD>8(jI^1gig}^&4=Ti_# z6XW;rmik%FWFUD5ch%p*!=WKhPBjtpLTn3`L3nQig?1YQ@9M4oFM}oyolYJ>#GIt~ ztK+Yo3ZKMQpOEXn(qlJf9dOe~@g;RwG*wY8YiTqv7@Yn6UT7=DsXxnv-kZ__Wh4Xy zZO-Fx%U7m2ZYyD92%Gsv%D-q+fZQ7rOy7@~{?%cK!jrX-@3Y9HPuwA0>7NoA6d zR?WSjb!QRD<707&nsKWUn$Y5V7q}WOPdonP?U0zc1&7m8BkQ-NN5Ho1c_R2pzNRmp zrBXNlaG;Hf+XlwyCe#KXXKMrLP@NTq!ddaI3Oy?6jmC!H<^J2GRH zJreldw@Ag@2E>A`eNNVZTg!Se83Cfba)2cqJjslWYR*7ydqT!+==U4gIeuz_`IaI+ zeI3+=f!l~WB8g6FK6xl>(w{UJbT~TgBbuuIP4@wBjz7p#SpAEv0PUWg39CAwSL$w* zUl~lprc%SnbT7{r1)r9Cc3rdm;6Qd>V@fmvS-|!K2wYc=+i29w2xM zV4;1(Nfc?abTkJuA=-soi#@hK`9&V_Qw{B59h9O z!~-9PX5z z$b=Z@2TFHj;Wkq23wy=IdOErdVO-BsiA5TPErzOjTnmjU@`30*{hgDz{p3BQ5S_@- zV-&*l%bz8|q2BLOic@loRUo9}C=`xK@ z=0r1QB_)fY^zo@jgvYAZ-|5>E)UrCavtjlL$ZDYKaIBDO+J5BaV<6*#R{$4;lavCA&^xGT<<+V@#F^xSo=tnF$Z`O0g!*^+WM>Q2b*?wcrUvw z3O_)@LFMr3n*cw_y)nVXa3P4xtJt>CO8!o z#<__Ot$-&}cIb%msh#Fkl`>1Z zixuPXIci$a&dm<&yE=hc9e*-A+pBRX|B0SZaWA<4$i#dlz+C}9m(!@?FMY5Tz%1!Q zjv2*T=T#^X^#{~i6K{g+kIu!-QG(*PNEWn=Aiv-0l2YW z>sA8bXtA`r0}m@W`~Ob7{6%v*lWJmlPam+fYhrnQ@#dLi>ycEz)z-|{!l0doOiHn4 z=W!2v!;i+iJ2sX`r%OZ;qxE*Vc!Zi1cP-%*Cl6vqMd@J9Atp-x>*r6m`wyGxC4->~f{xw(WAE zG&J54`KKV4-a|K19-)+ya}&9nxkVE0E)OK4BDblf*IN^=K35UBs@gf=Q}xb_c6eg; z1Rdag_(hgz*hS?i;aV8GM#6<@7SF9|Z=`_jb2{rRh^yE?+3n3Nik2D1U#gHkWxb!Z zu-G1`H&rsY23J%kQDq>!%qyhXsf0A49_y`Zlco22pjWnlr2B;_xw9+#It5PC3$3fo}_OV6-ZZe;SQo)gro-EBwHCyvIQyGju=NTI9q< z`G=@jkzY$cf#Ay*{*Lpju!1BX7v*C2#Z+rR{v5_bxz|PBZ(=C&a-i4eFx)1b5@O6B zC`S4+rJyoZk^GG_8ocbRF(-AW@tUi7tGYxveDf(qy{2qlMFWEoA6_icHHt_rDizbMF#qO`60GHGpb+nzPVky5lLoP`DJm7-LmF)(IiW&NX5 zPI2m=$wB9H_Hryd`@n|hP1Ib@BGZ2kpu^uqb zefXeGCw;w>p*Z$5a1GI5qf*Gkva{xBXl2Yv*fo=Rfk8Pv`+i%{Fq)C5L$`_Vh}ru% z%;6W_b=Q2v(t8p#E2eD|tFwx6q9s)L$mY`}@f-{+|c(~;@7 zAT_2XQ{Tx&RFqeFBhq>Z)lf0|3s)U^}sD9JHMhm zHPVT(nUd4#z2rv;IuxIvK7E6e@t?@Kueb+Tk6vbS4p1r?68}n+;~Vi)rGe-EO0DTo zHEpt%>H$XU%~~yYs<;04RfOT6zG=V>Y-t!cf5sLpHGcqWL#=Q6Ieu#vbVv4S7#G$>h&^s!!hq?oJ$D$RAa&Y%%KD$^eLidwWycdc!8 z#rLX(IO)IbJ`=Mfr%)4(5%E_8?fNQHB1AAAa?B0;Xl$)@j%lO9Z3dJG!e~1mC<>~U z&(;=tMD)h(V^6^SdBo$MG2Nri3S40$Aqiza~E4e znYU48!Vr~eW!#qNiUF~ftAohgjZWK3WO2ILYqOYDVP z-T+KbJ!m;Re7_8tImc1s2p>`8SaF>;6mYWbfW}dgL{SeDkIxN1^ms#-D;kD|dcD6N z8hJr2B-ek=?!tQL7CPLEB_JqiuZ6&wZg;P=@=oxc#VI!B?9f6zzZx33i{kD6A1dxN zbR$gz&)r4YQk(Q5qzad=M}Y30RC19CWek(YWja!5ca4B|3Ds&SFg*QK|7R~Tc4X*Y z`XepJGQQvrXTJuj@3^HF8=?TnF#2R_@#9*?g;^o7<@OxrioW%EQ!&2<_4S(i@Q0UP zrl!cia^;t3qwVDet9Z3cvicFx^Z&M?ThE-q;o@}J!L<(zdZviR=*kgs7tL#fM>vd5 z=Ys484yfj}3r>2Kvln^2%Enbr7$c#nxq4TtAIQw>a!L#sSH3UEN6mXD>tEFUwj|Hp z91@8L_o>o)cLswrL$$VpZo6BF&60(nl4{r1QBY}h`23`5BLc(6n? zA+$+3vYS%AuUC!r?)%pqPT_fAN7R@*Ygd0xPD?6sNC@j#F`0APq4|Q(vBLfIZGvp_ zJ8o;?jE&#k7K|^Ve9&}B5H04NS%nJ&-^EdWGsOt26ND`$3% zRhR0PNx5I`iGX31Q1q+QGJV=Kt@Q8%N`)c=kFH#NoL%nD(B5UDC)*+$xB9djo^{UV z6K_wzYEft?l7vd6TaqUmMGn;qJeG^*Qe1lgIi<%6Jg1Myc22ALhl^{UQg#~JOLK}Q z9>CLzQ}$;CRPf~KLNlxHgZ2$q2OOa7L5hF@hKonWThW}2GNguE+C_|wFk zF_d{j@TBCxmcPTXhH~5}S+%ku_Q+o)d&NWryX~E^h{cTL;io*qhbw8C-tY%`XL8gp_Sq4NCC4ZHgw=AgmP!ar4dZ&XO1J1xr@a-ALrW)X zEgEA!DyX9CEmmG(>m$@ZG~$0ds64H$t%=nj4H?Hwo^ZXwka!1C5x2^#YQA}S1VIH95CBGyfF@XvFlgEvB72UxblV};u!1aLR}bITm(q0N97!fe*XIB7sZ;io zt~N?`%TtM>Iq}7YJt^^xqB!9!^sU@6g^2omu1Jp@ztn-e1bJ`&A&asqtx3gJg%Vu6 zszB8{3-+EP4w6%D8C8L*90-l6{-1Ya{gU>USSR^gy~tS85)%5VJ@nUK1k`-)NJd=s z1yoLh2$2S~ds#nRab@h$BUj$-YbrkfPp|uz`F~|11DI0Y{e69XTkF`{g-JnSt4?T4 zs{Xmk-O4)D`pG1tNQ{m${QGLjpxDKC?XMq%Y*A-bqF-!XuO-Dq%!vjKxY zWw~tRy05$qD^7g@jY}9Pj@0d&NcR$kyPuz;!64~G!^JK%x5CouR|ftRZ`eKZkk$bj!^d1Z*^HmiSVY|-PgHdQjz}b$2uDr z`Nglr&k&x|N0{4Kmj-teM^?k#gX@Gb#`@j0d~hH&*C=^kG5IOQOvMe<(Wb@g(=1$= zEsDb{Dp8w?Z=1w;=n4tRCe~N6X&fK32X-a>?*OK88aWHH{u4uOL2EE!Nia=uh^?FC zUvATDO!Hz~-oT239N$9`t~xhbL;A2ugzs{NqQS`F7=4GzIuIxfX?+~>#Kjlty-Hig zHO%NtdRK*aF$ezJBs;Kdq;yq-!5fmJ*U%y;nn;C_n#o4GfOnCtjX{E>$YPfs0-5L-~@j*XhOR`aE(^u}D zMZf?Db7T}&2v1@f6sJseV^Uy&GcfiVC9^3gn@EIyI_~0;(>Sqa@6b@K7>Z`yf91Se# zpN**%*BPnZ>B|fkPg5LkjqG@WtiM|KQtIJN`)Y}lmWrx@C)kSUr2DW%3(VnOjQ2KQ z5bhWs9VCD2A^v3(sG8aHiyo?Ut#G-*B6AM_AWB;%m6RKNP^?L6GTO& zhTzb@$u!@Bb+>DNXHlVg)+;W>bW}Z&!Low+&?~&^OFr|=Wb84@209|<4BgKWuWq`Q zmd6nLmr2`mM+~lW1!z9R4ruNwTqi&(>-JoFj-1S%}G)}KJK z@eJhllf4DHZIHQOzys8lgNc;gTM693-|VG8h&i~@{akM12EQWbf`12iB=Gab5z+?i zjHCe}Hz5zt7CL!XdenpwWp5Mlb#m0QqGhc9Sdjtzj#OKb!hq%{p4vV0O&~|b4)(pY zN(|B>1pYBo!K}8*i(el0@~Sb7MVB&|6b|FI(dE zD(HI+o(G-(IN~`ou`6AW%vgnQfXXXmTSQXB6paiXLxs+iDtn^V+$X`@{S|F? zttEI@cZ=MVyv0vY&pR7ZUq5R{^o@b`zzkVku6U_I7}#k06hGoklrt@u&c!_F8LwR6XCF1Ine$!H#Kx(6y#o`GKWLS5{N?q$2w zEVqNi-XQg1!I5AV<3+d>n+hZUv-#A~GMdDlD1kL%n;ndUa|N1Xny#jn+aRA@+H$eJ z94GxI*=XJLHKD;&wx2`KOT~J?@jp5Qnj3(~b-F#!GpqsN|Nn$AK_5V-Au+S+KP?=! zP?`1%liRA-*^E6;7k-w`2;CCP=KPo1MT7ATPXtw4}?mZg@{&Y~7r3mJrx@CH$ z%|=-kfx58M+pw?(YdMVYM`?sGNSan^S*QG>mA>~xX?&8>6r25jKkYaqAawDi$cG$B zGzkd568){(^XMxW1d+qRO+4U@Li~v}W;;9_lQS3f*WeJzFe=0FtyQ15avQlF+C(S3 zH#rDF80d2??FA#@+}mdj&L?u`SAO{#?c^*rc8|eX*Ei_QvP1Auq-P0XMmD}Evtopa z+lU{`mJ%(Vg_X=M&M|TE(ST`IrM|PSwg9PV8Ffi%*0o%`OBS;Jbt=P|v-l(tAXWAh7H=pdy7yYe~G-gnf(|&*R!$1kNbS?mAljpjuHV89)30&=< z8qyEcsYJ%a2qNz}Lvs6ITaHHk7($12*i7T7c`tj;9)}-9L(IO>r->?9B2a&|zd!VK~PxyElj5 zA6eb$G2W(5*80l{Ia&B$bIkTOGdV`8Ow;J(^Hbu`)?ic1D>SpKkm%^$vT}Mq@sWKd zs)+t^yvyY4n>&hwe-xE`C~zA{67e6KcmaUe9h99dCCg{>m^A_l1mM%sAvYq1v;Y)y zU2SQ|KmcJlIkzz=bfn73ZWOfvDua zqGe6a@Z)2n@>EjC2QEVdIi|-@u?BnBTkjDpry0d=c+3bbBD^HRGB&gnFRD3Nzc^Wz zkR@8(S-0(d^kuZ=#GI&6my!S3t@s+t%>*0E?B~)AND23n%l}~kx?An6anj11^bLuF z*`J5Aab#cB$ZTk4{~SX_nbKhb`4nok8_R5uy&d$kcsmF%FQlGAiFeCKQ#|l|`m4V) z9zMkx^NJdCOwo$?-cf^*oXkqXn!nW{_i9cvnou)htp~j?;`oCwES+$Zo7?Rno5O^u zD`AT^2etcr>UKv&=EdNUEtlws$+J9-AEGMcn|{NdqjC;93HIVVFt^nfK_L4YfysBk zKq@L=LBZC~>qtl^=?9Ge6DRD2!CTM{kR1)^%urzXm%G}C0$v^FexE^d6NdWD9k74g zgC=MoM+XXD9#hi+>|E7n{sV4oXl0`;Df;#y@>_)i#C;^`c=!nUyDI2vk*fH(S}O%g zu#EPiJ-@cX)|u66U{+kO1GK=5$c{YR=P~vr1APf<>+AyAR?cwoZ(b7EtA1Aor2F|% z_0>X6Rqm)M(I-}H44o(O#w15ZD4~6kBCxecZ3*MrYadW#hFz31w%J4yS?N=j`KVxw z_=5k07Nr0)f%(@MX2bRSL>u1ZMl}pFTRzPQ!)lo+w^yd|dsKSgt)el7RgPH1_>%Yi z!u$PO-nc<$BuiMyGo(0HP9nb$ba(RhrV)H7Q_Z`hEwhL_2cv{V=G!oT@6;$PtnbYG zWlub-lT(-SI2iXUaVZX<*bwMQ2K%e_MHUFGdwmQwKnH{2s5e4>Ya7^I)ZKYZKWsFw zhyD{3fJa(T2AHmwK01Qe%#IQ^!vi_@?-m)Lqa^eCxMn@>5y&ecK6XIIJt;^SbRtDGum?qeEG@SLUBo z3)nnNQTJ!#P!(*@e4E`@u!gch?>#-3O@M#@Utu4JZml*b9Rg7Hz;d{8M2rek5$e9P zspxkX@$MBubKS643b(t}-BeYgw9VOH9tHE3TV-X}jujP|i{4<0Ik58`*MvE%#;8e&m^_M!?OZ7MXihX+;6xznqx_ViQi@V!O}(NQbm{@z z5IEFPKDDsfORvakY zD;mw#jol}}#;)chY54wl!kX)}Qli6&i)_4DATNO@S_?&8@i6+6wWLi#33oD5DPVvU z$;EI?_jtG0i!R6x7b1fc(Ea#=s*(3;Qu_!1;6hD!iidNHc!@&q+1m53!{Tm*kZRUo~Y?l(Z>-{3!|HC)!Iu zvX(JCqubP)stKrTGjJCAOoz#ca{0@rS|F%X-kkCix2=DSgT&-srH?-2E8K1c1N0Un z33aMmC2l3fUvioV3hEi?hRk_+(fkgbCN!FC=NOkowjwIuz?z_vIeSebL%6jc)0((k z`sZUP+9JOMUNxp|jpS%9X;mgm;Ezeu^5_BQj}g?)AEvNco*`@uo#?!o41Fal#F9i= zPu@;!6()m4&6n_X0RH+3zG9fqzp4T-;u1h%lS`7HfNUM=QAh=Sst;U<~I zesU*dEdwKfmatxJRy9+~J3H%Ql#MZvmhhRLveHJt3qjUKp^}x7z6F7jb;hibmCVWS z%p4`_Nl{2zsPdOo_-c|8XiVLV;7_qHTT|NdIO`Er5$!>16DwM=JvwtQb;O$b4GPAd zp6>niMp17!aJt+^*03q8q(mk>XJ9k;LAesK;Ms0^+J46I8&zMD(a>V?4Q8-KFG4Y@ z-l86_lCw)lkXFXE+A!9W(Dn1Mx4@ggpG;E-P5F5Y1Nskr1*mmI93(8y3(+ied=y9p zlnSd^8{DPr%3znX)icNSn7HIotQKz`k%%yVzl8mIfvbV}XsT*2D6#LG%*zA{?qAG` zk&mxf&=azyKiym%owz$3MzLj%$3Jn|UJnP!SY-T`(DY~!B=o^%FaeZiNBD(r3@^%K zm|0Mc>^x3k(`#nv6r(weHx@!CA*pXv~OSg7z=F<^T$v%k{XBL;=9 zJ^q489GziE@Pz0NW#~Oo%zrk{Z#EcZh1xd9^N{`;%nGwZ6)xQba|cYkOx1`l5vvL! z>f(i}Xk)qkN2LRZySJ~ah^oWVsec{1vfTWL8j{65e3V%90{5f8)3ki)DhqD+7c8pl zc*Q4ms;K(B6*%u?8DENwYm;KodMptp!d1a!>hQGysQs@U_x18%f}mWZ2bY2ba7r~jR?luK(u!l8CBbA2!`e9!1jEDN3B6*=V(;@tCv=(_q#mBJitEX(I zH)vu{J(#4D3&qpHF=RcFGdz+2N3geb(mo_(&s=5-C+XQpb7~G_M1Qku8hkSLDdvUQ zUUIqM+(1|-$%T0)vC+VTw{J|@V!UfjO{P$2vYLFL9AoKq&L|ml^yP=n~~AQKiXOCH>}>59F_)$s)^i$shXA@zjw=nyEM%iE|u>35p9u-5S3hojqs9bRuCX@!4-?q@WZ5 zJ#vT=<#>?07kD^}JHZWzdwuYxgY2*H7!@>zedl7$)tV?wLU`JFgT6|!NDF?|gWqLU zXo<0^_G?chYdI;`dJ@NzF`g|e!)j8I5nZd-+8ispXUy~ng_!1@bT9sttWPu@u-8aW zIHGw~k6(Q;m&9*4WlwQH@gH?S8#*4#sfqT&*~$_#=V_tpt9b zWX!h%W26pF3b^@Knl{*oudmW+GfYyWSZ(}wB=z|ftyBO|Jp$Voka8oy`q<|HBV#CV zqfK~&6rO*>C(y-$wS1X%!|Ye6DNxNCv1}BxfD*fm?DqSBl+@Mr(L$LJZrY^rk_A}N zp7Ph1<~)3UtKhFog-ik^JYejeP7ZjW-C9xXRTEkn^+zY3oKdAcQ5^Z}=n8`nE)*HM z4tZC#;7s~JXPYgj98~M6ZliL?PYk}3&kXnL?o_!oJ*q4vNY5uAA68J=<9-gswhzh! zs*;U%@;)CZk(!nc4gOTs^s8+gCS`M1W<8xqUuyAZa6Binw(~$PXtgm@a z&7aV_VX zBmx25kSncMQxie2uEpZ#sb})EBTGR{+Ld=o;qyH+b(s;1*kG5~lkK5=P-GJL@Fa{FE#_Hs=B?|n1A6}=^uHvSePb`YJJJ^)dH$J4j z{`!Sp1|+9ZLvHKq3AYEqZVz1|~un0oQNr@H4*>^b{uFG=Tq@96y6bb(=1H?538 z@9>k)rA%*Co(s!!DCaX6eF;!$!CFJ-(Bqx)B3hnc6J+l)lBj}ZMX9=j>H@1z!f`y~ zy~B&BB5~ZAd`^(mZGtpLA2k8u-L+*#HlE#TC+zr-`X}UujUJYNeRYrP?O>JeNoW$bzOGXL(hGL+%YXz-FrZT{ zC4fO`AFLQD`+XZYM?jNNCV&jrSl_^_En?>Wo%ETbS;8g&rUHJ5@3tz}r@LpA9QyBG zT+F*K2VeDKbJpY5nLK*={i?wh#L_+O3YMVo1WK}6V@j#hAW65yNq}gT+yfJD;LmKK zED$n-_@@si)srQVp12lpELQjDJ225Z6b>C& zqFIDb{Xx)DJUq?nnh4gtg6zYTcbBiiS_ab}__z>tBe)dyM{7**{bSr*f*Y8=!yr2N z{(`}&Pq@C-uU$-@#*pJe($QNF!fU{ z#O;$T=@j(zac}()=#PQ66ddxcV0GknxU%ItGy@+zc(0@6llN$SEy>76)j=nSLF)$I zMn^CAJOqX4hqqHoIH4J%L9UV{ahrx8KKF%S#W6gn{-ZfA$+q2SKGcja#)GB3`NZKe-d1Zf^lvsF`IbRbnGqwP5@DAz`?XVC{L{F0G&iu zI8oLcy7L00k8lqW#GVIMsRM{?rqd|zF3PfSzurn2C&i__@6R1wc)*8`rHpZy7~b@Z z3tt7NnAgrq_5vjL@8|@fS<+^)bo4W9#opkj?(oBld4?H(kl*#UsN^5^dt-<{Lw%ZY zheHQ$F7vO)pxE(=+bAgZS|Jh;!H!28KtjM&$=9D+&<0hTeq9^>`KU*XewAa5*`DQV zI#Ih1N);8oTKH(d0sc2>+=&feu|0M2+pwl^po+mD#O`^5>NwjztD$pq{r zgA78Rbl72gMwH<(=yoxK{ePCzDAl6{m^Y89%+L#Ye*tA{&}jJ^F246gY(H08qVCJu z#w|fpOg!xXJ^uH;)D|?p8{%hHWiaqD+n-1{wB6hNT1e0IcOUeahJYhIE+>6{#wW8Xl39DVp8D}DA&Hd!g(qq1srUlKMJ0?~yPw=na;c@^)mPy@j&w4}`^?3(HecFe-{s;w)nK^*<2P&RV2faVqNV`w?GJMZ2qJ$pg0kXQNrlHP8(lkXu^j#E3g0KXe9X66~AXhm^ zuul`6%HZ)R3vBtb>%#c}?*3JA9Q~idUSc-+Q!jy22a337@?<=jjYbCmY}3n^&Pjj# zw#cANop`^L*~N!X;$`=2UJlFM{UuB{1yNcm7!h z8JreiH=DW-919ix=cyB*4#DUkl*Y_vbihv=Fpzjsy8w z>C9>nJMp|3@Fg%mE3H~gn)SlmKJ_MAHH-AV`x()I=)HVAs{Eozj7d)7Ab1`t+NBi0 z1^?J#v;oiJ1f1NCdJ|y})|NgNUy)qArVJR`Ph7}fa&i|m6~=Y9Vm$J10wr_%kIMm~ zXTv##9JqlSC6Zkm*`_E3+2qOYdx!H6Z)WSr_j-G6%IJL;ph!5{1sD9veQopnMIZ9; z7I3?KsD@e|F+4|y#Z`T@_3(B_Y2Z?4%|&OhT1!y(4PR6ii&y{1TSA>@lTTfQ@+>uS zD2R`Az@=>2-!85QbHLM)GfpGJmbLXQ2^$RQsIjM=BfOu&C(UV5^+9-)snyHDj-h*l zW4$?$|NdE#e8j(E?B3VoZ}a{3XBDc`%RPQ9k+K38^2}y`>}JE;CUCC|zCN9+L*T#q z4T2SbGWj;k#1fc@jX+=VP9c~0COE=CdH5K>NauOX&UVHVIUoHwF#4=*rf?6o$zeUH z+A`X!v>&6S4bmW-UH3Kfc9#Z7SX>?PhDro4I&Z{N=?x%$CeAJkW(qR92PI>;ARisr z%wv_ka)yW$yFm`DA1B1b#0<*ei0E{~lU?yCW;KJTt=YXf6dAh8RHGzwFnHm(iY}d^ z#-8m1-p8xBxZTv);ZOp#?VX;D_2yBL?}H6+oU80l-{bYPR1OAUkV%Zr(Op=0ISbjQ z%vW#&efvruZ~=-~$qu+jr#5#*Pt}uN*P>h6hMIag-V zheWWEy0$eWut&RXhSp)&$XHbXx-_PJ#hu0eOhIQStCcNP= zEE@p}{n>5J=WuouP?D0bfsK2=zI#3o77C@t*r910s8EM-fN*zkc30}X4LIBx1ugCr z+{OesF+iveco~(?cH6OpPW^xwlf6670Qnbdelt4Rrj5!Pb%Ezq2DIYt(kGYkO+V&6 zRzA;NEFw4i8xB?QDc@Umwy7r5WE%pK^EM1<%E8wq?PUe}z*6jh2b<|_Iz%bO_Uo@T zFfwRr5T3e^aY?d^u~*MLA}*hzQ4i)SiOpH0aMfH6kNikr(6YGYWn1#zBs=X7j*~J{ zOqI^{e=ikwa2Uk!B=pNqM`l6^xcM_gCJpP@Yqb?RJH=+QD+LCjT><-=8hXHn4H3zzCX zTo$x33ew>ZE>uQYp`QhIgSXmL&I#sTsMhh0s8~KF5LUWl#oa__se!V-zbi_$$&e0v z5gd0`ydnI7E^#oT)VRJ{yJJk`qS^=NQGSbNDO;fgbEU3x;22I~j&RsIIP!U$R}+Ro zE^KqoHMm1^{W#kSyVt0Ea5nj2OtZ( ziM&=Y+wqz=-E@V8l`+1!_;u8ghuHvl)8AcxAxyQpW1xT@xa}Rr-#W&pF@VcD6oj3E zKRbqjp8ygfZ7*mPriWI)03fl zo;arYo0n^G9Ozp^H&8Fk;p!W;qa@1bk#WjIV|^bGM@t{UU2ThqeKwG>_-U>ay?Qg- zKCTEZNrBS~r!*BwjL=~U{>PW1uGS~NBdK(Vj&NBns>7KQ44h0%)VB;+f#6MD-z;R7 z_4T$WX}JKm61)wqV2kGV$6G~h8J(GwR+vlJk|cXX>aLHr_p|C&a5W1aYeL%4dYgZA=&Xr-Po=w z_dk2=WUmJuv+TX7)#_YS?P!aVRBwy{vI?D!(6w;v4ab(_r5F21a;)d^wYp&qzBbyJ zeQ935{>~JsBf?74oN>`P4+Hc7cd9V4*-#ijRvRs)GZSg8e4wFf9OieJdV64iM^ojT)a>POfHGM_OtV z-fXB3xjtj$T=Qb&QAhnS8>`Z2g$u#Q_r+Kn#LBO#=g)Z zC(#VI9&i1_Gbi1pw+gx_5qtV%iSY+cM!va9c81Yz$-ZRdv6Ubg3qvz{7tr_o8C-SVihBY!H~_%G z8Kchm{nrTh6}=qB<5=6mQD54Cg`1Sfe^UI|DS@)JR)8LU!uWxCC~GGg z@oUT9S4Fzhh7d2KC+>jg34fgc!*4u+aE1CiVn1+V~l{Ff~V zuAU%a-Vro9@^x?Fsbj=bu`yfSR=UZ)0C}yz<#4pt-VEa70L|$%{mEIsm?iuC)t)Gd zi0286el}LCF)wVAMLnp5C(n(x-w6%#+jZ5vVwr*~onFyiZNqAeeR zEtopVw<^UuP86}_g*{Ed51wxlp>bp^?DqWzMEySm81DH~Fnv>$+}x%mAjnwf(DqtM zPXolaSwR_&fLys@Vs`=#$+zk&DQLv1O}X31sxp?UIgfx(N4-+7B0csE-sfn?HNGjf z%`&Gqm(Y?Y8e&sz-DjWoo0Bq@_rtf)7l|8_>%+zUmI&v0Z!0PP@Y?J%*~rA3AJ*sk zpN6?5y3qp76+d6LmTddxH{-?J=beU40PNrN;-|Ay>; z6XLOSj}zvSgUZ)NG)?wFu={k`*F^BU!O4E!M%r5(bug}ttGnlD|fg#3fVO!m?-2eu$7!{X@SZTerWgX%@$4D&7^W@Jx^6iB( zA_8WY0)_@7!39xFs-#&TADP|N>=iUdR^^U(mg;{m>w6{@k(2y?Sb)C6B+Szd$Dmtt zsB*NYD>jOfq56*{xz`G=hI3s-r^WD#=d(K7Z+R{mW_b2rCvlnU;1nxrsCL^-gu!1< zt{_SfhV-aIO!u!GKUy^fZt=wtYrUX>m*zgFhWJ);J&#RrsA6^UL%F(Qd{Vj1_Yq&N zWWrjR1G5v`p5Hr4$y?C|C5FLMXxp(kB9pGE>(5{x6xpheJ{GovQOVD$C9+$+e(C>{ zv|xk5i;V*+DNQUE``WNW(XYAQ@}bRrmUF_Ga}n35Tpa2j zTI`w0{~K3nBMe0HnrrR|)Hf)H8I+hyM6++S49h=sf#i{|avQiE*po3gmV8xjb(U(X zO?gjtVSYy!8sy|{VIlpYg|D%_pkoEHU!mAB$`TL(*Lq4X7cA+lc!n5VL@n-L(4C|t zoZSf%sXCf)p<64b?c}Y@GakE${p=gzNH5PpoiQ8J5C5RRrzi2i61Rt}%feOhg>B1E z2`Ly_Nn+sVkd6ThZKZm-8?T1~|Jy?m995cBI>jbC~nu&&z5vHE4$Dx16jz}w7 zlEqoViHUs#7z&bxTEkHDNCy-hy2_+u*-TdM9&yGclZ1iRb_psP`nMs-Vc&Z=GdqiC2E7X z%ofZl*7#6E`o#6EM5af$Cq%I{_CyKNUi=all~;0)R`Q8GiBSbuAKQv3=Rd6P09|B5 zx>exsUS&iz%PI?`hFd*q%!RPxK7IDtqT18c#Q(&$kr;SbA)^xE$FEK>1w-Dy#x3a@ zXFTzOE}Inw=T(mx1_U$$D>@e@&_7T5Gtsa3UOFpJaU9yS0$B?OtuBe#`z@&gT)}LR z=PxpGh|Lteo{r0N(UY}PgR&$nFWMBogFB=ZAre=nqx@-agsnbSe43BeN~jn;z0Z$h zV2)boRZq;9S+!SIUQ}I5@8exEIBo5*R4O7uoFirnayOZRYrlRKAv{jf#;fkF5mB!B zqqK^2{KV!M{Ve;Vm^Q6$?|Y4ZZWCC&{%YM{CVx+LCnTK0`ytaw0`jhdEegotmu_>o zYA6S9`)LK1lAd1dc}=ft*)4)-5|6}}SS z2*Ia0MTH??Tx59%F4ex0)ji(dKdvsyR=i4}7%Q%}D_eZU6s3CirZ#*rpnp&JNxE=W zR3IkiH%$*KqfrdjaYkd*gS5Typ!s(L4Sq3HUk{47K1Y5r7FYR+-+FDpcv!FF+%5l{ zDyo_TkUO6o1x=$*>JraJox&ZDp0kKDbe_^c6a&>3hki6&g~gQ1RK+VJWB>o7N=f9@ z^Zan$A-Cq#pfZRs@*5%CJMp3B^iMoykXeOUi9EObqvtGSK}|bb}=*!*oVhmpT%N zA*!??^`F_RG7{*1+MpNlv^U>`aOq}Gt5~|4i@cL~*+$2?ITw>eNKTjKE3{Y{+V@AV zmp@#^ZTxKYy?i+Jj}fOIxxRg8w7|dae?FOvzRrI?_Vn8FuyudYS{*aVRIquK_EPCa zHQ1xKBJ0J`WA%EN*A|tSRqk2Ddjd<_fld9ErolPdJnV+0D2(GV-9HY}ee_vMUaL8ZxT3@BlM}j#;>Ak`T z;duuYHrF+dab?@+?3-AqZD-pmFW8ls$%v9khOKK)AI zk}+BR>(wr%s6Be$=lqIhu_vZi=uy*`q4Y8y7bX6G0}-6?+r!-Czarf{FZXzK`LKNC z_-;Z*cwbSB6x6;*ORv|V>vH-#6dx!Zcso1VCvnzuJ@g%M=6Q7za{0;oK-1`VFe+0s zH^1%;wXZn+s!6xqwP$^2y>h{Aw(Ms8V2E1iTUOhO%lhw2?`?RsCC)ETZs=tsj^0yh z?ejWW))$)x&`SmPzxpk~R*q$8{?LsEzv_cXzR|B&ZuPmwml{@o>Hlu2Dx`c_ax?R4 zIMCr7y*t0dqbX15=k zcjc#KPG(&|^BMf+9uXPdu7g_QT5~D}%4_#dHR#zMy@}50JoA-dT12Vc++@B&hdf z;t77~$0600swQ{TSWpu0|J5M04JTk*$uN!z;XQ<;Sn${pJ^>}`t_?x@xg9#5B3yla zn1a3Q8jm4%q4mfrY+6vy}sYj1C)BN z+@^>0Q9r1)w^QJuT0I_CdIYS0J?|KFcyG;|L;NA7atd?#{YSm zs6YDdGbd(t@=-_{Vg!e+-LY`=`(X2ewUF|?9vU7Q?o$DWBosOrrY7|V&XU*lX^OP^ zw+It@z!{F?G-@ziL*v2gy^`JEDY(9WY$h&aG#0J@nkp`Cx3Bu+k#W#MWT%tLcRH%A)DbgZ|oRN}Z{L2qjlU zhShrZRNeOO$#3bmi{HdMiMS#SHgh+a-+}`>-wMZ7C*(VC1Z#8-bIB+_CNkL5@K^Gx zD)MO}d#N@TeYd#!woo&7KtB)3nYOsP!v11{0d0BL-8KG^Ed|;Rtmx(Mb1jo|{8P8r zHsrH2G*wIyMz&03@9>_%S9EvOQn{_2iGNNh@`;-fa{T(L`$2OkJ3% zQsLDWBEI*zMM=KWBKq zFI_Jx@4x~^oo2Hwbev1RmE&t!To$|*qpqF}Li`v0e`eV@K|H)!Pv{ucj}M5(Z333i zO04ChYTl)z<&W_=q%rhvP{sK>h4T6VuiCBV%DH4Pt0+V+9;Xl978>ryr$;~bN(5KW z@osCgy*IA%Cf|5WR-Q54BW*{BaAyUr7fuJ|vR++>PgxHpbaYCu@H1L?r*}&DS6?EZ zu~ZX=GKi!Z8YGM^vP&SB?GwlK9>czi7%HB=`NRAbQ~guR36AZoksmh{`c1*KlAfSwtL1cb14>|pzUQ4l)Kp% z>iHZNPQmTc=D1G1$5lOd=ekO5{B@<~pUhJRTlW#xlQ@5NN^B&Rb#AXdDZ6u8gwcz`dR@LOaceEGQE6;-1M<<2ik&`K9{eiD#(8Ud=eTca90vPg?A6wHH$( zY=0V0Bv&5|PG$#{zI0}6e}uraIq8P#@NzOQMnb)=H#59kOF`4D--)Wfn>dbEHZ9R)@Jdt6ES6#dM5TnVn=3@S~V5HmW zeBb`jTBMGfKl|HSbGBM$U7&iusZCeL`;FVRD|ci1DXD;~6Q76ZdyjA|rMXiZ$6&c` z2n*-D<=w{p;?59uQTn{Q6W#I4!kMVR4(U)xUn{GGf$eJrPRFZ!zBKY6v(b9Z8)amN z>oUCttC5vX&Vc84UJrI>1a$D7OnfhjnANRy#j3|x$rEB@<;5pUpx5m_oW$7as_{^2 z&i-V2LeY?pu((OVsuf=z1O+wvZ+S`jV>~5qYvHu7{S8ZY z5aOegTC}iC%S$@HNPgw*sI8o zV_UF=ZOV=Ud&(%{E*IN3?&K;#JoHj-WK{f#7_`u!8wkF=L@8!v?m6LBwgmqhP?1R_ z+#m#9LV=UG13ko^0eikES8ejdmML=Nn=equcHO}H@a6cU<)&AC#1sAe>d@EiAV&YE z8XP1GYn(7-5u)p)>_3dmw8aY+72~Ez`XheOzc-6#Oyu2YP1^X;tZ?P*jqeGRSC%f@JLwuVEh|6H|2e&(TzF2OF&J`Q zet{1(D165<6ASt^vXUrSwPw_O{TiGY_> zr`H|d#l(vhyO(JBm`9f;BkaGwD-Ivr&VCss3BEF*Q<*?%#6Jp z@LL=vPG-HII!mo8^=e*??Z4_i?~UHPv~6*H#4UIJo4uWBrrxnU`3@yG6i2bD9P{m8 zMBcUGr~n_!b-wAYlos8G$wHIH!-w@_ji6_0P=Dh2l6IlXa#?j{r9?ih=hK1VkKf}R z1Q?5SneC=`QJi;vnam@+QFn;{o$=cbDUPkXTtoHgZoy>w%S|j$AUb6|T9Ou{-{o8s2uY37 zoXqJ@F|UX5?JlcH=&@YLm%M_-HDp=%m($m)BbT)ad!4&Uu^AqLjPpv zrmE_bB@&<~kI6v&*I>8pWob*wg^UCz`TGh?#v z2nj^CzO`dBCck#M%^zO8`WMex8d)(l6m2$)tfw6VW5d8IdRC6IHP>5WE!3xs+FP(h z)MGT*-_S#Zfp)O!=}%Z?nMU0gn(l+Mum)>~+G}I^9P+7oOWL_p?Rr8U2n@;!otmAY zZj{PXRW>!Q*b&5+w`dc5!w>C-#e>pf9^3Sx*h-w54Ew(uv-7QfMeO_NI>vcvf7&dFB zOss}#3ynF)^|MT6n%%3<_sA-8@cJRj1X`;&L0+yK^M8u$27Xe zT)A0OThyL|CashEXTdFPf!_>Ne&>94a8dVJT#1ZYa*I8X+@wb{5fG7a-)I(kN_X~K z=m*O*T`v1<8&!eArnT4#@pS4g<{M0BOc4D`o^daSoK%hz?>sj-Ui6@Ip>W8H2rhG~ zEk45!xXpE&Z)hl6L;-fyS~6IAJ~9Z)D#gRv{~4}4+tDl?>$cV`Gn^*}*wb5@WxTl# ze&zGAVvpu$D}d+64qUwx#+e;H^<^$9dkzs=)*C}4Ano##UDL-Wyo@bt$**iV&rR00);37D$eU(@gyiFg%1VbnFGa3*UsX&bbCkp-zgNSw9C6zMkBiz(wx|@KDW?q=Q2&nBi`Hogti(8ZCcO1$i8fYO#(jTb(9MMKnEDer)`Eod+YU(B-wJt=-UcC`ee6@=emjk(6QG&OfK+ey@a=S zs>3ROCjDqqYbpJ(*ro0H9+e`U?yztgXDs(w<9YBy$B|d8!!_FJ)oUvpHqFuQQ?%l- zdcNGp)2yC__ZuVa{ttSk8jlXrOwCGh6_U|6>Wq2y5TSxJF7 znb-ndY#)G```LC=RLGqQ0Saoh!vXjcG>*sC3!Y$$3=9@B+8n2?#iL*3F2E z+wYCv`C%(r@z-6fXl8V#^SVA>3$6nzaVU=6{iy8|@R|l(;lSV^KW&4e5XHm$ObqwP zF1EUd5^gSiSp{;v4P`$VE1U5Vw;kKBHVK_zgJ(;)>FCT+VFNPQ)yv#&J+s>7VtCc( z9_Yg<1v73=J_?XB+z!I#!$Op;Tf9E71U$UzXTO(^E~d9So&m+1ZLOW*1kFhwZ)^Bi zqsH$7Q-Sul48^@XVWE53wV)VkUKXin%Fo>Y1~k>5=w63jPmg)m*L5LD=1x3PBr`A& z#ak9FpIjST(>>&87GW?9N8^{@&X&9$g@Uc$os~1NHgufz;${+bGk84yHQXr)B|x@0 zC4O5(_DS5~!c6HzO#lrQA3EcC9F(F9x13G?NOA@=)OBn|RcduWFltDCG+pGivdx(M z!Y$b@#by2%BWCEPLupANwfNU`WCT!CdAZ06zBc`7&bnZ3SX3U0^$8&5z9D#Zug7=1 z;PoOmFBqGk2qG)|ZegBThRf>B=gx-?wipkz-{-%5U{;%d-x-X?_;WaB&y*;q{M8NY zIHOSu%a9ouZg@eGwK+KaU ztjQFa)M5<6RNCIhj}Vg47rIOf!l#PYL1-o!`4X!Nbl9n%Ujw>k!2S)nI2z-30`l$RQg1-C)O)Fd$wN$~kIo z$HKA&K{YgBn*HiJ<;!)p+1=gHn@pQ2He2u7Rz<+9{LuvMg)jgK@(?-SpIEP1aFvOQ zI&TE8grQ|9yf%KOy;8iSzthAV&9JuQdynn=KP|xG2}-OZfO)_rf9Jck^SSK@quvFd z9qO3PW?%I<>;n%54_-paei|&gRA$kj<+KH8nFaxFPW~%+4B9@wIK!HiDr65W^uDZ%a{@cDL#;!*K?Mo23UvVT#%U(Qa&xiF*0vn| z>6GT+lE3)-TFrur_ZIpgsUUpn%n_g>jc!=_n?tECclA^a#(4Ei-vi<#aOV#dUm#jK z!|O7e(&1%Z{z4pBeetOH5Nukmh^^3wb;g*wKZrKyo#GLAPbdf(d1EjdwnQ5LLGx%o z$^#Z(W;I5hiy~mVZlGM7%!pv@uS|;J&uS}j(NM^dPLTm_sxnW{p(ec-=_@6kN-v=vF73Q7RYSiAU#&AM9aHBHCC~I-`o;g@g<1t0gFOm2~CL;8= zZF#||o?>CzL8ta7Kc{U&6O0Cee`nxDr{jGZo$d5d+D}afS0nw{@uf+Vmp4$#D<+oW zw-o zRE;n07os|GgIiz_&H14M^i3%F)XnDr*h74a-@if?X6`qc44G$m*R~Qy!ePkwu|~}U zU;}SkreOk_es<$b4-`z^YpRn&dUQ4IO?B*N@x1_enw?e$D)Vx2yA@9_^iR@{7Py@{T~UE<+XUS?f9#;o{TPQ~688rx_%&{3;-7cJvVy_?vi4#>L z(F8Of-^Z$Pgx;(7DwDpxdC12h4~h9OmtZk;m`ygAo~RMOY?38Xn%@JuAs?qL6VKV@ zFowx}>1+@p%Lk|XYpj$_2hb@*`UNzh$PXuz=wUf9cKBr_a}UloB!p3cYe1{ey>?Xi zQ7{j-)y5;B^R3b8TqK)^Ks_S8uDU8qR;EUc<2IZ|K_D9QExoeHc9sjLX{0TB+z*P$ z4k|vMwV7JQ2#ho0S6wz%?#m4*23yS0DhHkIsOeW7u2<1>Aiz>0Fs^mzGvKGUvP7(q z=3Fi|z^?ut)b5+iGlX2^_;38h`%C*b+1%*8+mN?ZpSc=8qdOdcu*Q$ht02GJe zp6iTeICUz9nBdp9=DkY%lfS2> zcHik`KTcYoW|`hbrJg@KYF06boWpe5XIeQsrXjGCW~K5UH&$rWv0>?JCHqF7oUnO& zjG+Jo!erWS9LmLaM)F=~uU8stlT&A_Z*Ezxqy8eBl7Pej+&`Z z$!FY;Rq3HEMNAycCB=Y_|^Y71z#S3xX7Lu522h$arf z$qglM15r}|$65@wB1{)t{SMn0==zCxPy`S#v!0-_w z{#sQnyK_p?`gXJ^^SIgZn_%P3(jhAA9fjz|%jqtIo>Uu3fHrgLWm&BNxz%VY_%R*h z;%?*A1F=AGe*c*Exjplsf0aq(e6{9=aDD+^Put#k9fi=_@;Rh%RlD3HY-EBAmI&`{ zePn$VVSi}7!|v?Esi|AK^E~%Api6a}6-NEn*yPaL%ChoW($Vulf`llDRYWe5<4j-O)7q!TUnN zhZv8>Z&A)Sk=>7ADC-c5OKLa^-n!0Z%1N$4&Uw>m-WnmHsSmfp^M}aC>#gaxsq2r1 z&aHoFdcKS-#$#N-R!{V}cQRuAFq}wzbD*}d2LPLn2)sYrZa%M2TVKAJEIeikdi#BM zX&kkjILCR!@t*qbsh9jXn`iK>)^$7TzQ%mZS_l_ZEX7Sie?9l;%O#Q;U$GYK%3bOs z^C>Li-K%|eJVHkGiy_S}jtJA*_ZC(Zw1oK&|(QZ#Q?++|w$M8m?&c~6b)df*h zT?_B^_TH*QUcD2C8~PbYqGLJUsMqH+L9g3=VWLa{_XdT?wZrOZ{wxdi64HMSdiIL5 zNVqJwX@X}*@c!U~*6-&^<=riwtt0JTg0WUZ@<96vArS3L@*V1R3zD4O@rsck-K$Oq zDEQ9hv?51O#ruHdFYCF92mG}%DhYNb&coZ8s9@mGbDn#u?s1uXwPjpx4TpYi|ITOp>(3gV%LOyuPRj4J{#b@)C{C5BPCo~Tpf8k&-BsP8*$%@ja(2nN z8HZq#($Qaed(!FY+3A)4n!7Z~gV@%pfV9K@Q|d^Zqr=^GrYEUwQ%MVfs#V={A&_U{ zQpfliu&vM4Vqy4;(~9Zxk;?Gc#jvC{amF~({yJ-OJe?m><$k|~At&P0TdO~hAl-SE2rD*c ze`IiBw_dwAoIda(Uwm8@bcmH}!?{6c*x8NfZ%l6(k>mb5vZFoR3Lt&Am7?o`U0A_Oj>#u+LdPxXc-;8Gn6IaX&7v7BdF#3W7_P-_Br-VR0aCqC=O&J1dM|gmPfEKLkoAUa^9Q3c z>+C!_-5VrIe#g((l2pMjK3De9!Lqk-a5ugBqz7}cz9q&^uVkp^n7&Jt>`CdDjsq9+ zi<)6BG5^$zaUvpYph84=bbSnfHhxT3R7YU{6ZBP?dWHfzfr8k z@`}gy1#wxOj_c)j49MyFgpY=#JoFp2iWHzOdANtjCfpahy;Jyvc{98Uy&8z<7IRgf zMrkvGy;Fro9S@KM=7s#{bsHcSr^PbJCkTV-@J+q7xaLV^{pA_B)TZiii;d$6az&^D z|8BPyn&+U*W|i|H_c5brY=cQ4bh`qFQz1R2WP4$;8s?YK9X5_Uu`l^+3c1tOe;pW$ z&itUUt6Z>JM%76*KkM<7PWAfK#}f5k4;K2a4Nj*|O&kNhO`$*ee^tw{OPmv`09MN8 z`>%XGU9w_3UEjx!s{f3obno2RJ(YS>t|j=W0_^Ex2qA8cKAn8YbFzK?pUl?9S``A{ zy(Ybla2*}vpGQ%D=fkLqtBZYDp&^W5ex8y#sQjfE0?K*SGu7# zr(8rv5b5N~hfiipr`brSVP7+66-s4P^F4%m@Y$mwHQs0ASx~UHV7h^AW6gExMrfjk z$vr<&5V*3K+Bk|Or`JU5hsM;7i1}5sHU0a~-GD>OCzoK~19`P2`8h-vjXf_LN$!_t z%s4-mkgD)N=HM3$UHr$T9mi5qQ6I9Ty6+OR$b5=B6V6ip%eW=aB$*d9KgW={ztE{X z!hTM@%9I%0RKN=Qd&Gwf>NKUx&gv!YSMw8jZwR+cU2`fqH%}CXZ&RLk zl&d4&@SxcZ-J>=yS5rCFE9U+CME71n99j&{6pjCWPmaVv6tS=^^)eb#%e~ZsgUjtN z=-f|rVotLNcbsN}SFfs7Ulv*E$AkfX6{9{dj2or-(Ni5Ypox0G$xUK)`8`*e$K*6> z#TW*`70$`r?fVr;;Uhm+Y+#)CM>`NvJn%hL$WOsjIG?}gJ{p|^={wdhcN=XGD|Wbq z9A7VOzU;GgM_{18Ln~}>V1FR;ID^%|!Em{bmak$-H3zeN?VGQjt!0r|K&SLlc<Z8t zgdTOrhNGB<7`3berd05i#XQ87UNYn>teIUrEn4Z<`rkHVfb(Q1nq;-Xik-fTl@Yqn z71tDZL~%-4oJ2S|N*q1r-IyUcGQG)b<8zAUf1rXeZw@~%L}1Wx#=KDC_hWugoI;j2H^lW7*E6ni#TU8dPmwD)qjt2PHb zB+$-p6uC!)C&qEX9rx<^1RZ6|m~yOPnO{NeJ(F@9!b*7ri@GwsBpk2ds?np7Cv1jj zG+JN$15)io12&}s`d#kk)q;@;MhadJ1OXbSBMmuH2_f!P7YiO1LwJxfU+;w6GB6Tc zRi)rBHK{`~=Ze=|q!|ku%$dj&a1?25vB6=d{x z0@^MvhQ0ytmI-o9_NL)dP>5HaAGuMd{ueBTZQdLx6%3vbNkYZ`z^|@L^%{)dmsvU9 zgNd1FiY^JU;0`y`kSBT_XTH(0^D|;GbNnF+xmNqxvofoj6^E z`je+(8`r}LU-5r$GYI-kcFO1G@1k79*yT5N%v`VE(DCyJ()wjvDAhEv#uF-NmASvo zI{MXr0iK?28H6i4u$*-Col-+K4P(g8WgZ9^a)z6q?ixn4IJq590;s0vvf~> zfU_O|#0$W%3eYct3=II6J`ioA0XW70Ml8U0bsA@1%+>?mb7XWYe3FdGNrYykq$>w z-BkB39wOOb0!g2p$^RC{mhkNJqW@5(JCXqu?YC{uv;yq~xFhCQ>!v+kFY$mk(CRP%>PFGLG3QElvxLY+$dTqe&SUMg$skF);;t{4F2}@I6147mAHr?b$nbdRf1ZAX;Tho-uBF1=Z|8ge82&D2fU2`KqNo+DMV1X2LL!0Gk+2oPmdN_9N}FbKV79hF)K zUxD+{%wFxJfW4s?@T&mxFuTPZoF|a8_3|H+Z*`%(n0dKxn+PV=kh=m@{ogYlV=}oa z10){0<>sTYW_$L~cr(4``?&JTo?Boz4VcPXEj8yLwCVLPd*$tn@bcQ;1j>I8kNGq9 z4R^yGUw*$H{1dj1=T!IgS!wg6j*pk*1os94KHD;@KXy?%9(zWr$9_PO0Ezc%AQJcN zE-FiDi<2K~bHnwTDw2Qj07>fjgdO>wP?npWG6-QtLc=%zkTDp8KKDtkEovMXGnad3 zzTqN`<B<)wozFV~X3)^ht;L&X@|gE-~Z>`CPh6^2^t2S-M~+m@2*nxcEj z1^-0XiYh^B>Yd^y-!gW(JkA}r;mIoJ`lHO3tEu{y&gKV29S#c+rUB@Ii*}I$$u8F= z=*h3m^@gAnH;&=*^K!>%qNq*mS)}X;^45XE~0nSapsckV; zMe-GhopN2SF#?R7Vl1CX3Hj`wZvpPb4Y0Sx1D~adinJjQJPTtK|8r64Qg_ViDI9yS8T4qXY1}vyK!LfeSPj{kS|uJS z?a|R#O5Ta4hSMdE-iaoCbr`laX(zD(gJ?R!pY6cJ4qq`v&Q0iKT{1w#S8AIl5a@N< zpFI49N;Yx>|D5FoU12e6qflvY)k)n)uV#M<)K@F>*|Y$b-4qXO(@+MFy-W{K7O>(1 zz)J|!HmksS>SM?$R)P|JBH9b%KZ3Im;MQHN`0M;%ukpo?L&y@QWrM}WFTB&R>pNkD}1(wQIDN|>5&gA=Wy_S|lD#jvb0ZS6{I*rW-l}Q5Vk@Joh ztE#F?@eo?ZTXDSR{WIXE5XR)PiIL zL02AdL8E|dl(|Ffz9CP!f|g7YiXOsphd1kZ0)L zR8(3_b)+`0t*}}dS)l~ucuVC|JSmvHn7PHiYnyZ(VB-TY6(sW20A~Q8@Jy?>z%z6_ zZUg0-{OjE!=4eu-M*A~BNSFYe;6S<@lllBef)C}A&XA9+>6AYdKxD3F>aD9Ee0$AF>;RH=bhZBHTsxQ_KIh}6^oZzDkvE^))gF%%la=iW;oYs;d!h0Bz$`G^JJxa*mL>VpY)=fh?uC}H2bD5F zMeRZwysA(^DEaZJ?1Tz#)J~mZ+bdDMMANG@5wsrZpwl-TH9EJ(0Sd)piFQ~ATY{pX zB2GG_^i9!+OD|NNt#uEjdQP}2_o%W{)XfbtMBUlPp0N}If1Wg4AZk~PQTQ^#LCjA< zCgNWmWZ5Ji6t(wLSkFwdZJf4r$q%|E)VCtxb*GcFE0*G#vZ+rst}1G@tGshB&*--S zfvaENG3mQ3>(F%V(^WXc7R>2oMRllF^|bVm4n2VBXw&0^orP3mACS1B4-f z8<9%y?lcB$Qq#cJN6aD#g@O2mDjF!90Gf#BRX;WWF}468aR7;rfFe1J8sN#W9-A)L z>;W{13JT|y9AG%KF!{Yl+PJly-6X+b@2KWL}Zizzn;fApsiUyA{Fh}X`)I$vCFC1pP zE^wfCcIeYIH)K^+NcVrk2uI)v0+k6mjf~j>qmuCRr>yotJ>d>XYEF8hdlCR)||MXQ->UXNX=?W>ii z4+B+NMCLjjFn}%C9oiO5YEt8!0-0y)P5v7Nlax&l4GnnKkv_XWBSn!Ml+$*mpzHe7 zx-iJ#PX~pYLPn!@K$i=|&@G>~?ZkbELN*%mK(C2lv3_hR{;^^sRvdJ(aa*CTRDxzg zg=Yw#^#rKh$8^^8RzDvK;xBC|0Xf`X_+ZTc{<>vi3!Z_W!)N*_XWut=n}#Da2N11x z`Jgp@<3d&Y$4Q~jPzB3m#pJHh7+9y*_l=O7D zCLz3Ekckr4GXdEi0B$FdT6rJHyCf9!@&ITpYvmI3bl|@%l@NemuSyCF;b1I|T692s zm6PYiI=n2duEhFR`X|wbRJAncYctNouu2S$ZrTi&r;l~Ok4o9?= zZ8f^>ZqO4@|IxW_MD^zrb|jD2@N3P1!P2cEnkO|Y>)Y9FidpIPuUXv2a7wlE@^i4Sdv4c3N? z02H2BQI*Qi?}kM}dO=XqiMcKcDbxF_AteTrrbd(r=gV)H8v>b(Xr!q*Vy^g|Jk`=4 zHkk^%z{1-EBJU)2SC{`=e#Jf?j!ij0Ur2WufY_$#C34m<4gf|wL*`vFtFqbcnCZ95 zie)ObL<>}OM%mY5oqSE~q|!+2>!O4kVBnLd;J};7^QODqvcu)-Y@*SrbRIa|7`{wx zW|5_y>XqFBc+8=lW74mIpSh8@Uuw`FteFPUZnxDX=F)eVz5tZP>~xIS0b7NQq@&oW z${!Goy`=DFg2}dE;=1YaeQso%mBUvG`0sm{_=|u^`Cnf&Td)F`8IjO6$G*=c8*G4a zAeW^atv|W1y#X66c~T#LPOm&QEFSG&y)f*Ao0+9Z5L|6g;3V7b*N{#u0gYjn{z11s zQ5)2%swRb)kv15gj2eCYn2{o2-yU=SczIm9XhnHOl@B|Z(BskTmQy{(8cETLKAq1# z8?2?gE!x2AG5GkCr*`Lk1NfIvHA=7FKBLspsf`cwJU(36OS??~jyg9u-aljnq#9pG zDzdFEguw>7B*b!BU$p?j@xRKeTvcF_!dw@M5?7*5)Ok1>po0gKUy*(Dg z!aj%}zx3Nkr49fSGam?qo<5S(ZsTGN!m$(>$-pU7_r3XMZ)58@va#k3q$(i$40@$MV0U-Ey2vCEmaa4UJ8c2jcjzWx#oDbqfK&b`6Z zA1%2?>QNz52nrw`(MAPt?)lOzb7RYr4tSxmzdS?49wVTxmR|lTcxZM}KceT9cCI(8 zIQ01@kw)M!j9T!^zK}bQ;!>48lgR`oD#{>XSt<4&-~TZUx;wg~FFt_=0h*d^7ORry z@?%D>nATshQ$t<5Dm&*f)MiE(SLDCIp8vj%@YtwHx3V8?=lFx7L|jDcH2gUn4} zhM0;(MQ?t{Z?m=7i}8rIR9YmB!WtQtlKXo~k|$ZgwBPteBIr@d@27e|UJi`5?}6;d zv(g#Y*v$DK>uRDICjfbkcfY zSa#b;c?P1HFv}U}B3+K6SR*yi$66NrW}ryy?;pIXQP2#l;a69<2h-cyWo2+y%b~u4 zwxU+lgWqI4Q$=spLE{i4CXq!Vpg4E}-6^?ws6W^Ax4Q|;sVWWeBJ~SmQKwUWilmdL zpuAxMHicRTed?bIa$3GXn#el|9NsO>Q4l_cQ z$KL9FJEygOW>3sws|6Gw^p{oX`jt=sk0fEm&AmNr;ZZ*`qDg0}maDSZTD4U|12jxA;1_4Jn; z2foA+k5m}$BDME?nS8HApA0T)Z^ zw&4Hlg$g|Wrp+z)BXX)UOtQWZvi~CLfL+0wnEX~GL8Vbxt2~Ao02)GuVL(Ot+x5`E z&!^C6gj)zlNlo3NWB@sP0ta~Bk^b)71(1+~Iib~`I9-;7dg#=0YJLN^hiOK&X&3(% z@6XPAm&@UC5d(pO;o(2=j6-7gqh+Ni*_$ zovFmTh`}Y&=NG#U2b<_kYYFv8vV_GFpE28z6nIsiV*3N9&kh`@*LLVDYaCt^ z)AT5es@>@Vb51chdu%G;Bt8KteFRNwZj-DF8S@;47Idt?IdZtRk1+b#cP@_`$(bZj zC4CA4GO*88(s8@jLBR--stBQk_L4Pey%y@V%OnqdPuTUFlt+AUb(umK@U}2WC3{4P z?G-h%!SWjYmx0)`O@x^GCB}xhAdah!3B6DPms~EAOo&qzQ8}Ycw-9Fm@=+~Q#$1(a z@KFnY$d;<0LBYsLNM&*_zq=tX8<$9|QTvlnzpaUgvjWv0dm30V?#OJ^b;Ev{-SU!5 zt1YY4B2(lGv0Z{99y&EI%0p+CUk>81# zX72yQ`-Z6o|0##39D3eOJ{I|*0~3Ii{1uoL-Zdo%m9W7brr`DS?72Z6c_OIMFY(IB zrY|X91k{TUI-GEitR8_4=l>lMSlB!O`TXi5H+40;e4xnguCgIz-A*GexlfFjUAn+e zlTichBGn#TV6LikoWlcAp!6gKxAF<2uR%dW@K9TLXO|19f#7pB1#H3v1rRN{`7EDu znvP*g6D(3Ybs#H-s9#`bgD#=W?I(#y;&q`8apwa;QWvy44MzA8vWS}i?^Z~MGXqBT z5`i6b0I|oQ3_IB5L&KL?grY4AzuktI^HYnE_nhn##3ymL8beDq~Xv!Oy>6Y{4pgD*O>DSx43)R4<&#J5mYtG}|%UgE?iQ zTcRb36P!eIVA!VVBE~^9&;orn{%@5tj42*y;_3`Z0(i@8D~<%RZY^!b*T{JpRWxd! zQzC{UIRqe&Y_Nz6fqbxpoXWijAxCwFgwjFma|j(ALcMe_$*gd`(w{g2uLQ-9Z&Z96 zBu9k;;ekPS3aar+L%@T_X7Q_R^ttY{s{4=jrhjD(30y9q(4SJ+>qtBZjyGrZq~c-( zxRL9lNd|>yKMj%u+|s-aZ*etQ)aK*zpjtKf{YrM#Z~KV>O{eDD=R`AJqR+=>^#eE>rav@Kq64^*CDa*h z-+?^ZZ|FN*RB+=l=WE-$Tq=Z{EBZ{vBtO3)=dQ;>Edf!@->WT*YU0T4!KOSwu&Ohn z`QI*UDgz6Uk>MDXv}LNl{7-);X}_8t@{s-PaDpvLhrVG4v0{peJzev@8=G04IafcJ zW4%&MdNpyr_~02K%smoy{r^@T3fRm)pf&gZor>?v+0tn6p8*U#S~h()+a0x>e;B?k z`24f;^F9|9xi_KYt+oYn!rci_s&EMrgxE2l*eT95>?$CC(aWMTfvcB)yl3%s11od4 z!3k93GE{uVW*dhXs7(0$izj#5iSDPoIKV`aKgm|=zBGhQU?1`b&MiVS9Dx$?F%z7# zVllg71bu4#@*kI~r^nCt(Z2}c`{e&e*IPwpx&7hVgoJd5bV+x2w@M=o(%s$NEeIkd zDJ|U~B@NQu-6dUnvex>K{e1`fJ$PjdPUMZ}ne#XA`?`Yg=hI@8yPgNALpE5(h$xZl z+jwKcYd-iFsS3$q#|EK}V9n3@>whIeR1ppOMKPouV9p8#XMLMC4VEn@B}+S z_C#=(>lcvJWUms>U_2#4NEDS7#N$N9NZUmZdbvS&{A>w*!ODe%yb%?S1%N4^UtlA$ zjlkh?W|5uq3O%dUielD}p!fUF1iDL=xiYO-XrPa6Y)F?R#UE4?!LPyWuzRhrh1Ls8 z)<&-$cpNYLS5xpZC87~tZ%w)}iXCk@ zOjP-@tqmIeX{3;TGe>(64#?~llJ}A z7Tx}-kmrX)-el+pK?*s=Tx|MK0;kktVT6GnGnnkTyp+`z&f=Kw^WR_uVIx+H!?A)* z1sV5V_&g>)Yq!;BP1>2Cq>kbOJ5iVc$+>AzTJLkXa?;m7ofm- zK_y8yNg=&Q$^J5$>zJCCjH%liCu>^Rh{$YAjz}*!Voyq|u=IX{aEhhC8Biv{7t$9% z=y3@4sYTd*_$`rH>s`f85oW z(5~MSDR~+G)$m5tNECCW@ulEcBJ-(1h~Vf6y8EH@5f?(Z@xNIB zp>)lg5MN30C2JR0f}@Ko2~-N|*UL1C-35*}o>G?>nzUC}QKQ&k;UZqc0f#K<^r}Lm zk;NRv_f*jq9=-iFkq$a1B1F}3cPa`n)K9h;7#hQ+DjX}uSnAfj^H3CQ6pbm~0Ql4> zMBFQzihr*?5n?Si*q_{)GfcDJ7VX7u#22$}@@!_S3a}$CQHTc4381XkAG`t-sga1c z-Z63r35k0LMzm%*;MPNzoF27EjoTmi@}RAeDW5s(v*7!&9`q?!%y;DU|$NFl3&ER$%6P$r-G zzoe-Uuh*iPQ(g4G!ok5MB{y0=fAjBuw;vYyDImY-lpq~LUeQuLhEH5ZjRpy#P()j2 zy(BZ=XW9d9Q+J=bx1})k$d-T~*d}Kxs z9?mrHFFp}lA!_uwcuM>DZgZ6O6Ug$M)T~!-4s7eBP#)Iwg8Ow{5#Xvvp)^W&knF5f zkx*{@PJ^PC5aB#0qO_K^T4spt;)I^*MggBZmRKdcrxhu_96}OXlV^V89(}EHk zJGD%WZ2N*o)%W|VFnf3)nu#pi0Cl$r=N;9~fP#MqC--?)1g7&TaZvAk1+n{^9s5Dq zADz58XT`eUcqB8b3V|!G__1qKLf5z|G0?u4lkG4GaxIRuYk@2a?b8;#njgExj$4@L zN@U(j0#bagFY%Y5PoYLF{E1NfWzd~R(`(pnk0Js0KTXeL)C&VouuzNdXhfhTBZy=D z!MFIo$I}t%`JE+7u?&>*98$YwO>&9&`-$;*5WF!azEmsuoZBWzHMZ$U7|#(aUJLkn z3;SO_8nc7}HH!ZMQxL?m0_ir%9|uC>;kBv32|{ZWI0Vrb{Q@|?9)hH~_`y0%7FLAO zIY#)7ii2=iRFhQ_f~nJa2|>R$v@JV5FA@Z43dD)_gQ`MlCeT;*u}52EDtc36Z~9TN zSI9CiuITSXrA7Q@*C3Q!@C$L8CH}qTfV&PkQBYtXbtjJ9IHoCm{Cnpp5t)MPf%Vktt(~We!NT#2fLxCR8$TWkuhRq*r1c zqh98vm+F33PWx%53}QwZ`?Vxirxx#iu1>I5MnjF>Bp-(1Wu&5RjTDA_C>kkw&+pmQ zv_5JJ6;3|M+9q{aGzs!V1^Yvwqy%{oAoxhGM!;qA-pfzf9!x>~?^e(Q&hDOV!md%9 z_P#6TOO>YH|6INO*%9=a zlq;6lciKKO(W%3Z(1Vij{ND{TslV8j_*n+pLjvMk zZv%YZ>;fiR0dEdeH*-AKcV3ZG0{_HqgPx>vm#A(eWfmf+;STK|%G6hS`>aH)BZ;2x z;P9LS{PW_80*T{?NvqTlaJmQo{7|F6wx}$|mwn3v{Mpw%xccb=9#s|-1y#7f2-fO) zG*6xBPA(4H%<&?;=p^?=(h{%&194Ep<8wMDl*&7zpYg4_R7-ujT%@%d!E3x5yVhK5 zzd9(IUXkDJ?&WGV1O8v}g8@(5 z``ZmN`JINs#%j}r+o6>BH20nQvDr<0w|4Kpn-LsF+-LxxX06kNp~KVgoKn#|ZN2Gi zwOq<;iNgrG^cR%3f|d>yR7)c|}bKq^@1yf^M>3yxfc9!|S}2$$P( zvQJ_GSTjMOp&BT)fJ7(>NNd31tz;yY=+!GsAjU=h=YI4o^#4pGp#42a7j*hEu`}{c zr@2PlBrb2a-Chc`-<%~~NdzA(d%s!YG9RQ*M-Vm4dYbEp(Yb*5Jca%hVb;jgkhHq=`&T7k@AoDhP11}$wHVlHM(m2()9wIImcG7RlqScyRVo`1Gm1qAm47NUkX z!ktKmVz<0&-@FJ{f33D$NcMsJE+W_HqO!HHccN_1luNwEim&u$}!$_WFEz ztt$+8V|)2r_7}Y`hBd2Abt?@eU%b3TsNkU7;sqlQlCIRmL=z-)F)%*MG#T~T9z_#o zqsy~DVM1*HU4&Mc`Rh{2r-d>4ox$^)sJflmwGV0s>Wq2$uRXcUH;CIOpYH6o-|JR6 zeNJ4ZNu*Z*2?iHWoTJ$rfDCi0FXxvY1&qU+m=8+{vAoiIDjR<3Ew+0F&|U;drGM&8 z%*;Gx3Xt$Cu`3h^0v_ZK&Zk!P9j$I3Bz4pP8?5m-%#FH2o6F%S5!E!d2ksokY5U1} zqyn6(JpoS7=yHqc^TkTGX{qhxfCo4_?mT@0w4UveG-PtfcDAJs678p^&64M1_d7)D zN^tg)F5m>~)j_MV>F4&Y>9O%h8l}Ms^NrubC%XS4HcOBH7l%ngpNTIHBuk^gcOX;PbfvYodRETTTi6`%3M#)f(G?MsSROwpCXs zG@8yTC#mMqd=fOwF9>RPs`pPPM%&h!L4xCjk~@ID@$>fz|N5X(Lgn8PyWGm^(8Y5a z3HQ&vzkP+B_tLB1?GW=glnxzSKV8m-Da1ZQzP~HF?*bsnFAFG@{n8~QKJBW;HAeL3 zZL?12M5Ek{D)>O}h5fjkT4npyD>7reUUCPsP@$F2rJ-bP zu5M;?HEeg-Am`fS_!(DpfYrH<^X-FO+JGt_*0Tc=sJVv?x7-RDd5^PMLRt}+M8)F3eP+>iq?yBd-lF?u(+?@|{QVR*#RjkG9s%tl zr7y5kp3o6n+KiN1y)Aqjdr$9t9 zNAY)e-Bq;MEKB5#%3~n{vy>N37rnyrCWWxC%Oi(*FYu@#|Y>xREtAJyk^Jh!9nmh+g^YmjJs2uVK2D>q1xZ+|!mXpd8) z*I92XAYu(4|5{^kABsNsJe0_)B0YtXEV4P2h@4x5TE1Hg3VeWkswLp|R5U z<2xg$$lxXv%`3l6y9!StAQ zc$J370Nr)`f5}rcf6ON9Q_qiFu6KsNJVsPPmp`8^IkaX<^;7>wRxIE**D1yjx_Y+R z7yt*@M?8)Y&~|@Eue#vmHv5Hrx*`w1R&mg&ouwpS%(@D4>^INwy8yV67wci~Dv8m# zC)EH&g|lS)j1?|@^TV23C6D_4fYGQBfC26-9b0Oa-zC+`pWJViAO-8_Ry!E9TIbI; zZXsaZHfk0ger3PLzh%T=GV&CHNjPGGWzM|Z?fJ+-?JQl-B6z$hxTDca-u!7XFj#g%-F_S%hW^KwJ+VOTaUdkxo*x{(f?s`A=mFKG%q(^ z{_&l;KzaLAyBT0g^z6P`yVYE0uw(e=-hPk??K?J~@5R6+F6VvYJp}+Esn@k?m%YvI z1s#3c$Gzv}hElJW=In#7_aOw=^~T)B%D(pjh$Qy%=@;!>?os?as&H=U=asxeA_^pA z{Ps%kf3pBIH}*FX9+`OeUFpMCz73TMUmrr+()jZyxGRRdPA}PR5y*@}mff3%r>;NA z*FQ|TO}g3gE@kui-fdQ@_#u9YZaR|Pf$*wZxVCdES0{-}fEGiZMVma`Y#_14yIIya z{L|-i*p++t>BMjBs5~k%dCt?^ZpTjFt8R%?*u~HIsEz14?#6N_0s2ID88h{#?d9^( z%;U|nN?9}cQjjr;RUyi5FnzmME?D`opFFPYIhkc;u>8EJuNY}OnL57IT60eYXvF7AJXc9}Vi1G!}aD0-Y2xwkrX(!vW$IIJA){ z0**I>Dk`{B##paLIcvHweRtWQRxE6JgO9O_D!k;qYXy$swG!2mZ19O#a z%8!oJ5KF-*2{>nR3A6C>o#WUtuoI9{@8I->_R-cYDLQ&w{=7d9pTb5B@7E((%*MqI zHxZ4>I|>Q7A{$dI*J<47jjNb|Zj4|v8Hgt0=lZimC6^?EVhE};z*FRa2?W&(d9s+s zL_v=y2 zIvM{gG`V4t^0YLUz*Sf1y5; z)jM_=x^XnUqHy&N_65{j9NQI$1bDQf&xsz0SVIXV(vMMAy;rLO9@~Glb#2S^DDxTJ zI4rO43)IkpTYKYLCh~g$xxqrS{EbjBNrU-YA%b3>7O{XFh592dqppDrY1I93T`G60 z@nW6J748C6Zr3R>4s3KAj`KDUYiKy~E!AWo>G*}4W0VY*s-sWk8X&0Y9>zT|Rk24F9;9S1)^Mx1mhI+Mp` zAMi@`_w9Wi6Sokz`W~~UKdLck)I2{VH5UPduv$0kL^*-_Op)9YfswqfH|!K(9Q!(( z*yYP=>>q#97jWyZb=T#2qqk?Do4Z!T^--ErqFn1RyrQCYr>gi;Zj12-z$X*7Mb6jWH%NmYIAS+_2sdWfmm~y&Xq*o8=#qGx>trN=UVyqsR#wj~Ox8#9l zM{-M^J=RtRDPJT)nA}Ij85M&kE=}hBD@-R*Md@uC;aPe?^K@k!_7|d|hOX8PJ zZ06g`Iiqv&a52gZ`W|W}NwT3+B_G?@Bk2h%K}OE?FtsYf9ppX}Lgwo2NF=s{R-bj7 zU3;29P_)v1O>F0b8~9~k@{7~a+zea|sNbgrYue1$K>pKAx&&go)$D%v+H|nE$nSDT`&c0w}0Q&xsuKh{TD1E)14j`mY9vOcFILM{Mi+z z5Ap1)tdt3B$}})Z_;c$xz3TPnPGUaQ3r#n9#{d9X^K-Y-P*34Hz~v%65EBBF4|)>A zY#n4)&c-wNR@n!iJ?kOQKQ)=2NuCILT0KVD*-fGZ(M$8Fghn5yVZ9RoP{FTGX6(1e z2MJ(9dzH-O7@Kl3A2})eLJETco7t9)Q&AI7R?~^$sKJjURSYKelMYrmwPhNJ2N^$2 zS@|x&)}m?3nqfw}_Gi5ooXxV|13Yo*N;_iQ(ren(q5XNFXr~ouDgdI=<`I4`q?PAgD8^q_hD}X?wjuKk-)XAT~Cd6KW;W# zmsG8IMU6Huc}lB~QI52O+UA?f^`3{E98DeSLvGYFxYUn2G-jD?!^}>cHMi$f`Jp3I zEsekYj+XGxLXDs%8TC+#egW!!BU4Uv21+sR7rhkmo?WY>kPBSp99*RzAHD%{PUlN+ zcIQ+g(@eC-gOC-{CF*vI?j2F3c;byC_6kHC-l4!7>{V~*L)BQBv~hWev|;HbM_a~q zRTNPI?Wp+P8@V6w8gabGTzCSZkd+!4w@i5u;|vh%k&rS6>JVb@cf+50ojAMwKBE*d zku$=QAt*t7(h8e{!a=i0{4taB7L#Rc6*b_s$xOBX+I3Q#BQoxWnSJ-8wb(d3%Fb3r z{i0)3wMr-|Xabyr3QuYa1D8Uk04P2YRbhi0s`T>ave!wOYQ#e3Be?GYQ?U457w_;_ zc4`d#)b(^6=Hn$b2H*zZgGAas+oKW6>rwGej7I%kPdvY)Pv`0-52(fGd#|BcNAO#WuaY_6 zY!9p-u?C=zJ;1Hl$8NdM-x%PKqz*{Vbi^0(vM1#o zU;FmFq*^H1esE%^+%yglFT)FU#N?DCeG8f0y*>J)8RMt9&#N1@WD)Y&e;!xBP;! zi)Lf{ZEU(O;;q9B&#z?o!?W4{%$`N54A!oYvz}QUMjk7wD5l3qSC+Q> zHfzsn%rbFl5rmA7Bpx-{ijD&)@^x*zOLEh4EpM%g7yRoTR@ZLc;kz$P>+!GZYS#1i zLXLbM=U9n@CX5YAT>T}4PY5TnyG8e@elt5h7?k+?XFae;+jMyeU)z2}fhP?j?}epT z5B9{E_ruaL>{~H{`e7$9)N`SScF>GS`$P9tlM6k92lJE}^LT2oIdt$RzC7i&@n~!| z$#rO{C~0V_22onXoYd8gDOplpQqzSfKACfERp5#Y8iK)ib@N;V56Tu)eZf?Sz+F8J zmS{U!e!iFcQ&1fVKtyiLfkMc$nhp}q51aj;VGty;5F2BEb%W1Xz4crbnDT`nVBdj7 zSi1A3Vf==ORmLOFhjgB`JkZ3%)r|Y7=h4$PGW{~y)rH)^^tOIFJA4fmB0RCU+1sO; z7gfbB_rqC2?){~s#XqteJ{E zH>!?9oVIQ~m$BU7x+N4V4aTDJ*@l*D3OL$xL{mnfvSy9oueTQa`}TOeDz^L6He&WD zPAj?ilYfz5BVf&Wx<5xu6sDj0F12OYswaz4eL0#(LepLSX}nWF7ZMd#e7(BTn{vD; zur;zn^kmibv}Aj5`o#c{;W8YZzU8gj^LZVoTT>$FYnSYuZux!b|F|2uk4&Tj07a~x zQ&9W&%Kp)mLCT*wfc|X5 zfbwRaKC{u919iZB*9`LOi)rmDC+uh&NoURFKj{xJpsor#W<3`jDW&iJT5ZNnJiAd0 zSc?_S4x>Qq691tL=POYc3bEPh`=*V?f{|Yf^#mL&n>>qMs~&_&UT;egCdrsUllvTs zTaYaCV~PJe5j=ZBk)G5D$~v&;fPxZSPp5XHgS$ByfIP&sU`C16Fy5#6$c0bf1VLbM z04nQZN&}K)Z@XCTs6M%o!BC`ip>La`Knw?i!E=4Nf7%`d%Tl2@i!9x*Y5CywZDY8K zo{K~?FDS1hs6rh_LpIm5KkC$vG}OZWFS`MMNLqf~VNlTr+X*yg&_3mM+8*LWe+NvY zvz108d|1X*D8vHc-Q8apX%+DHr^}+SzaxRMA=|5>NHFezFOFa3@w#GAW(+~ZT?R7U zg7@Fjos&T6ZUeMU;x~XDoLlQSZ>C(AVC!W};&!5!XQW=PV{!e@$)XJPjeZz`E0LhH z$ul=N>*05KbbM%7pyJ{2*sl3}?9Fmz;FR$823QsFWnewPG=}!zix0cqK0tq85eeUo zl{%1&em$M-`Dl2z9ppvda{KG2Obp0yTik_Wk1YNAH^4og>uUucQ5+(l(=q5~<3dM- zV}&R+1Ljs@TQ^7^Pt%wAVz$M_;+ds$UR>rEi`^ZVPg>Ds-&#F}0odz457At$?G6BU zb<{bVII_jJ=Am(iyCqnD=O+`)U6> zD8U6``*M5mOD>pl;RTE56)r#7BrWV~AJlsK8ptkrrSO#9 z{j5~nk3_%0r%QaOEtMzn8R#6So!%1iI9LxA!zaA)S`&2K5_p$Y14hI_udUtD|2GTp`V%I&uOA(OSZ5fWwONXt5?v2Rb2-^&s$~e(;$h z%vmYqLGnKO0Viw%Af}H=&RjCqO|-w1z|wxSJyY|ld#~LNrG13dLdDND*7wV6=Xxv~ zj;yVcMQz|-Za4FQF60MIK`r?%OqN~@$^pcpk(CnctN>kl{7-F18O{U^5k_niRd+d> z6lq;#2{n#4aEaMOXi%apWmJYtpJ60iRCf&wOe7Km5XvCJ{ftm-xMDnm%(=J236&>W zd1u$3!Yy-io?j`%kHbhlXqt@CId5`7>@*^W7GliY%xhwZm1<#ad%s z)2bAZrUby{%O&pv(`Ypu9NQa290qq#{))n)J>UGSpy$woEn^w|h7+Mb92EA2Z&rge zt0PDMDrP(FjDT-7=smv!Hy_}FZ8K9q;C}IxgDGVUSUJ_s=#^;I(wUM?1nhwmsFShW z-cw+#hL`wKz6e_61}_1&1Qf;ZB4b~#pRUg;SKLVcl4atIX0$uE1%d(-V-kCl2nNN@ zV7u2+a@L>wuQ`pak_j0l)1#TZPOH}8gkP%-ckl0Nl5pRt(krj`65Kagy<^FAP|sea zSSh({_qaRM3@=r?eF}w3@1w>l#%6K=2LUm{A3P?cOB|>4PbP^xki!Lr+gwhciG+PH z;?6Yh069N^a&3u8aCw-2JDIjCed!&m?X`lLgTw89dui%Ik)<=S1UT2aV@zGgc7eqF5;ZI0u@EjiZ!>Sc)ID9aOEu<}SQp?B;x`ja`T)p6$_!?rDdxu=vk z+Ne_DnnZZN#R!HR+HRd_wC(yn^-GpuoKdNDTPL)wyW$TbkH{6S(w5!F-aqGA&B9Q> z|I`wlybfDDhj)bBT5cf8-IdnTjTLQB$pZ?a75(R5euYRrS2G>R^6EDV-^xc>trLWC zY*xZ;dr_6?Yz>JkwCf)Fu2&aMJtEbiPAKV2oeOZDtVEX7)u*4Zis%I5s>$w{e5kx{X@iIC= z~hKtjA$e&)1Xv)n>M(OAp0wV$%7K-{G^x?mcFGEJU(+VdUV8={tR>+u%jD_@0AiP$s<6_w%_ZQ!WJoPHHPQt=BeAy-C zC(gdP)zGqtFpq#^s?Whcb3dNJtS{`z!mac`z^+ute(MOB`~__6Y@libiurf) zICQG;R1?t60eM4UrOVv3n?o)c`Lgc<;(H)!hkltxfJYP}!bM#fJ-torH;Gq*{U5t2 zw#no$7?eI1a0a~De?Er8M+dBGJ&ac-F`5*w6i)4wM8A%#!^24C@m z0;$Bjg_r@n;TQi!3pUkeDF{0+I_ z!n;rj?RflD%($daOQIgBozXJ#JjKFN@-qHFMpp2-eMUL`Isr*OA)5D|8*3gjZ5nd% zm#Ym+<{Vi`l%7bXWkmN=KJ1q0UwRYOJhg!=V_P;)rC0WAq~f7SKt_%Sr0>hEUJ0&y zfaEbz@cy4Yl8Hve}A1$4=8fVY}e(EQ5La@kxl`V;+niMv?!9^PpH-U zTbcxJ5}2>sXw5tzh$KK(&cwyfFboOHoD_o&HEF<)m$`kqpCtP_Rz1Et`50y*pB~L6 z(bNEsl?dURVN*}KL_Z2zBlN{B=dpf=)8U||*j_YhQ$WII{SO_DJ}_biMdknfDeHk^ z)9=1gm+9Wk*P!FW(`L2zRUefr*?vouzzcTTNd_Dnw&-{8?A#IwDI);hb3`0eL`cip zDw~JV=itt!O2kITgMtQK#8bZsfmYxM(dz(_*Qtu)XDgFB@ZCAhbQ|%fAd&3Frx1{e z5AT_^G?Y=oC5X3rDEN~5n*;83OEjgdtyq`@B5P-+Q9v@mK@FtVG%5 z`Vd0}E+UJ@aQ;JX!C8ynl?U!WXKaafT6}mVyAagXNj_}wvLXdd$(Mg_dP!r4c|h4w zHjUMn^Q-9(ySB$SXs7T=KVY`xB4m-fKhMa>yuJffYxW$~l5tBe8#0W_Ub&k-ch4#5 zV%pus#vH)JN@!Xd`OcL(8El**k*HUW@APK(q2N4~a(9Q-f751&Y?{jWUaC5Rhb4a7 zpfu_OU3|&q1>=v$w}an&ZaqAFZ78!|;X@S!mu(dUleFxa|KVe%{9?{5OsW zp109RE=w}8=>s7*QYn>!vwB7Icbc9HJP3!pLsdO$5n_^*!w&lA6QS2r2RamHSTxJY zr#s~+H(=-?&OlpwjJfkg_^nmNwS78ZUmj%|tWg8CY>mPK@0>f70{C9>!LNx{g$sjz z0FK_k69TtXcNAHA;la)(=JDcY!PsSJI&$%&yH*f6d~E1hhqre$Gojjtw|3nMbYRlzqR zEkGom2cgZklfwhiGen&4HCKSyN%LKY7{-OM^h;l3`!xp<^*sJ1!2!RdHeO61FZye= z^tZP$_D3<4N@I4c_{Df^Iuh?hxZf0Umt%2y-X^rOlE9*fp~?D_ewrysD)~Z}*113{ zFSFBaAw;G+#qyd%ZU^mACVQ+c-sN;#$>zOk#vXQ@20{7HBq?_hJVi;)AjzVla%8yUqiE%AV5Fkwg5E0M8xZ#zIO;eQV@5h$EuJnH;VRtGgxy37$k z((2yKMqK?o=pb^gSGL|CzQ%_+!{hW?*{=~Hc*P^>GmCFW&Pv`{K_z#>>gFZ1@uK{3z=eoh z6|jwkHjEn?Cke`RTTDi$>d$#!ZQN+ zzwyt%2_Y=x0kxejkwfxzF4}?0&Sn+B|XSp1s^2jHbjron`3pJzS!MD@5wM zV+fY4705*w(k+_3!Uj!L6DpKL;{RSai<@PEGkf9x)>` zCZ?^^t7HG{)SyHQeglz&P_4vgNDd~>P^(t2AQS&W<~rmL#H*|V>M zkxUO2lJcW~oJ?VhM2Y-7E5Qs3=H`*&hh?^>??eMrQ(VB?iTBDd_3@LfyikjMrpnhW z+wZyt(`s7>n|w_E34_#^h)JAPo4Ie_Fzr=Vh{ipR`+yn ztJpM2dF`XO%yx;-6W649>29q`lO%WIMWzQ#@->bPR`m+Rgmfp_`FIGkApm}Xx2vZwE0JcH` zH`Bo~aK=76OI;E+o>eRA&byXofO42?|ub4?4gh0F%$|5dutP(|O@& zCga6jYnVc&#`TSKkK>Mt7MsCZG!;L02CrK=h?asdrR$qxRcDvs-(ZCwpX>+Hj-v{M zi`Amv;wqY2x4q)51%65^x*KI|16NOlR};ufm{B8XwHQr+WD-3tovTX57rC%*xU&yQ zo@-jh@wkEUvp$EX$ii*hz;3w7utu)@h9Z<>2%e+ zsp1(ahPSNc(4D2)vc_U?imq&5&ZB};%k>~-Mxby`v!>S`FEi%QOP1=@8!?IwJZ$(R zv+-BE+nJVdv&k$>{)xYV6`yJ^2nrgq9A>`QP9u-7Gy=08J2^?98VZMrX3es6TzbKD9z{>!VxDAKcl^3c*#mKZ zJj4@mXXpZ?O^AeDFdBoHpVPfpPmDT8zEzw8yY6Fs5N_~um7#6hA#cKJ{E@Kh zzA^CXjV#vOfso9xwYO}hHSZOnV0u7fWtYFl;eLqpho>Vnno9Li`^Ssn+G}EA9|Y7) z;%6g}q_P7H^SG$9?>8Noy-_>;ad}e7om5g~=__uN^tH~Pr}j&snhy5!VkT?&6Jr%=(cT>fmDZC#$^Unaur1M?)kyB0zSr^s-&kX zN87E`$G3!VAPl^2);5n8gVy(*sXZxZ0?t#hoedRxiM66T{2iY96WklVBT;T=r;bKZ${^t;7l&Zpk~gD~>6Zw5}t8$Sls0fnUlxahuYuh9ofi0m9QFLE`&aPSQOX?6J%`G7nU1KIf@5&av-!Q2>O-~GBfn`66abj+908!)LF(&*LNM}Sj6 z_nN6KjaL&&+k16OccWQP3%}KpbcAg5C_S7whA zCemkf;oOr1EFS8DUmMd$X;4t5(a%Yo2(@yJc{+0DtcH#Sj z>=(mgeLYA>K9@JO%9JOSpaLOcc^_%v#B{~O>NTrVU1`VQ(SNSi$&jwgBYcWVlCwac zhOI4p@&krb6waNCdh){Ri?7VOC7V&ZlXL9AU5j^{OF^xmqto^qSl%?-jXhSoln9l8 zvNiU-)EjtNLBf0nwcyYg4c3zm{kG-vNi+9AeZ=PsfmfQimTwam$hFnP{^Xs-8{dp1vzXx|p-m7+kNUf>rS4Zpxe2>WcxopMQ4u8 z#TorsB`b7#MK_At{!`(l+U^>d!9{Rv9rTrBh#1)t-*mXmEX^=>J_>?dZ9ac}^zIoC z$#+jo^k|;gi#94a_}kbw5xw8zxm0+0UbAycX`!u(8`iPHiJU&=CUCrc!&Sf=O!ryV zsShjY0|%v(gaC7Qud_SuSGHN)Eo{}0G|F%*Ex6P~B6PJ-V>rBXqNZZJbJdaPluL-7 zaHz6umD(k+BYQbz6@&UcYWJSR!{ZWGcyadn+!7gKr!S^G?W6a#)R*Y5rc3wj3H5@F zkEc&yS>|}P$SeJ{!HWqJ=Xbr(6_@R60U$c<_u|fYuzlh%=DyRt{#7i!(u+1?+yS#s zn_Lg^ds(5j@eU;6UJ}BB@8^L9s#PkVZW)mME1KpoS(?5*uM#{~_??28{z|$K2Bz?l zCp8;spAMndtryaeMlE0tH6Jd_#LtMx1Hk%$g1qS`|DA2aAt#@sZ7bsY)dcmA26vrD z1f)ot>oOb+$M-mQ2o(4nzw;Tr6`S4XLKe6V1$`xCNUEGitR|#hB&(_#l_F#EZQlLh z8y)}y`?dYAdyaW|OQ%l@lWrJu^U_j_wVJ4Ff9!jg$P`JGPF@?Gg7fi=Z|NIBek8)U zjlE)Df9KOTNxn%bui+ujmA{6h6Y#i-_KM6~03o@z<+}Un$9$gVSKIC7#bb(0;YP4Q zo6^i}I=!|c#&Yr?E$FN9_uIGg$4-3UPvYyDG>@I~s`ultB4ktlAxd3sdV)eo_k3W# z5lSueqFsE4qkJUj`pSZqP{|#V`0n0)G)oxA^VREGE7%qv$(YbVV&4k4E?4fSo8_8I zdPU=WAb|!8& zL#tG`!CgTbMm2l2c|#H}@~}OB|NGL@^07^q`{5?jxXtVU*YCsK`KYXnMs~*VfSQlV z0E_xas|oJH{X}neNa4fh@W;5CriVWHN5B22J{Mfho~Wf^soyy5!>?SS$uBg8TB>Fu zrI&4Vn$D9Sm+iM9k#+e%JhJ_v@cm4P&vLhX#bXLHe+u!zWyxJ@$h>d+j!BIZTq5mC zTjU1vUaICdbzavP{Z{x!RKg!0noVuBCYr>M(YoMb`Qd$CEeZLgY9%R(W`&cHSZ5Q1 z(6VmApnh--W)C!ptU(M+KzG+Gl#ps$n<1L z7!1b{9_d%fRvXaX-oeM7*yN+PCsy<5Bt1q(XB|ADi=Uq&r7k~Z#OoXi`*P^l5guE7 zCu~fZk(q<1(Et88>gRMM`Ci5N1L0tbU?IH<(Dj04{#_n@rSCJ7_x$trU;V{P<`JKZ z7#A%h4W2GpQMxjfsV0xA?f9q@URS){PaDc($fQygX;~My+8Lw3c0%gN@4mgrOSuXH zi{UP!#zIi%a`oJnxUM<;bx>2&&!_#kZy%#Grb^#oXHcpKq`>NN%5dqb=N~6e5x~;* zmivLh`xHV-cge}{pzYVnPbQJI(E&hM-QJ*cxs1H9N_wJRBHN5m8_JLov$ryo5lAaz z@rfLT(o86v?DS2C!=+;w!F~!uobE%`v@SgkL!>^+hjJ&>m{4RLj*Daz?w9Qrh(XG{ z&#~c;v*dUF6^IX_i<0txilw#m^m5HE{rwri6B@!l;1T;row@ziZ>}{lD_^gY3?rN z*P<^@EV9Ldu(nVZp3JRU{u^v`PVpq(PI>l{Wka7D{zgsGn{gaP&m++}q1_G;V$5|8 zywzZ8bV@CM_)x`IOPz$m)^}=ly17pd zS!aPzK@z2Bn7V%a+>Eg^K}r#^uS>k@?79b{ z7X9dV39LbFgP!u?8OgVlN8hfP2lb6THr3#-)ofUNFV`bUgq#$T@3N!vt)<0^73&n0 z4w@v|#8S4I8Or6Qm_&+u8WZyo^U{jl=CJJ)h#)PGGE!@QnqJ2uNGiw0H{9jOE2Q&9 ze6>l?sQc4XG_phAGE*JhIl9vjiI|>s+k&dSc;D$#cF&aV;FNWZR$g!0!v6Ua2i=8sF)Vxd89?q4Bp6sp@f{4-=oU#=-8y_qk%6orq zISpdB;pxh1G0eApAK77!I@m*PM|lk)ha7#v>u5d^uq5~vHE10=LQxCHa0Rp0uH|^} zqjqw|e5Fn0Krh+wy6p zoIZ-9Ht`RR@zuCwNh>+3$9zc``dl7_P0exsRw%-G$+ z+b>Vrd?Zu0uC<@pYLZT*k#&3}Qqa9o5jEF-vhBqkh2to` z;Y!iTJ^SA*pr+S`2BER!j~RC-3G+VVFDSU64g(f8JRLYbnUsT|FHuoIJMVe2{_&~L zOCN#}#ESAPvmREc%lMX$G;2^l*gmHowOkyLR2wHwT(Z|a!?HA4 zbgX>H>cW|UNI(7R$!ObDVB8( zr}R!gqAHKs@?&qT2FZ5??7jr;&&Pp0##k-rd3>LZB=0n#93Rt2t9gJa)B${iQ`3GmA#o9|3SG-a<5NNZQte1?b1Pv-Mzyahs7^l|94!^{q;Sy zvqpjFCPbqNoK9a9d zi(V}+Iq-F;=Ar$aK!DCdiB|ia4h0Y-~)g)&bW@ zbY)q+^2%$%&o}q%h~neZar<+!r!c^tHytK_brd@`Jt|nhr-gEGu2+>v`gI{q`*_ac)T0uVZ(a z$A2G$rbu^c%|aP1xaI>}nZUP!$fN!c7--n_yaf{#NGn@i6d2F-L#+krZ9cdt(h7|% z)%Jg01Sv`B?(Pny zyE~;DDd{ee?rz@2z4!Zj|MBrT$8*%Z_nK?WF~=N37dMS|t_r zaG7X2OYLA`&$J@3aN)7Pu(3&GqzvJDbhX)PVj+)9Pjj*a>Ce0*T`vNbPW_kfgAW0U zCkHtV|KvY@Opulsd)xX#`9@pD&=@g|oSAMlUUF!&&b3?% zu__ID1uT*xI-W$P9}kuR`(cU{ZeKiAH*yXls{C7^9dTdu^^qmH) z#s*RC|LW;b$&0uulP5S+Vtva6-j^TH6k#~O4I9b#6b$>#CEt#487`$LqCNVV5}D|~ zcPmKey%$K>cgSEyFNLzV0LA}4iqul2*>KQZJRK~aY0PARRb)QT5p(&kT=WV^YpKfO!;xsl^^U= zv05s^!T38fvu`0h=pEXbd%e_Q+YBcMJ0DI=hMKWkx8=}FNSOI$+Rpu*k$20mEyfmV z@4$Kox)0NqoY3Z?uC96<2~LB0@%jCXpqCb>m*e3`)d0%mCcj@4+jdk?4XCdQQX|LXHA0LR@84ccDFbO_Xxh>v#w4{?x z7CafN$5$Z}(ZIX-St48u@ZCv{f0KFz{#VjqLqM+%9`Av47%zalR)6u%1!LlK{kV9^ z(?V$EIpov5sDFj;mFk`osi2MUChStWa?o>=$@Z&1P9un9@`{) zuV|I4+WIc=ZdaG|*5L`}Ji5q&bs8i#%idO)O*pMzROCdFq;s?kg;WDHS`@Qt74A;` zfn|empZ>I2>0cHs!(9DQUvP&{d(y#NFjKSzC1fqEYM4)6m?(yG>6EUr&p<*$5#1`K zE8yFvP6ESpb1TUr0+#`@&}g2kcSO1iipF#EV3u|#SuDB~mmZqUjcF*|DvBDOGDT6& zjMx)vP?~3439+y-#I8sAts!3EwKF7kfGB^N2BSzWAEkRu_^P%SC0(=3-uZ*GtaL9# zwdRNMVD!&V#YGcwbdIgvH11Lc1L#^EWSU`EdH+|3F+W(%pDvqGk`g}&KF~6yv=2-L z>Gg^o?L_JdR}y+$!hcO8j4x)sNZ-DYU?z4Q>v$#+aBtnOYZCyFIFOaLIyE3xJ zn?%>LQ_Con$JGGq4|6rrEiI4r{0BU)JEPE~1~bk2kxSEI33qwj&mm5#^yMm5l7!=y`xS~ zvzKlen=*ysepfIiPqs2`wH2d&_{mZ3kzhf`PjUSDalMV)B#D zVzI*YM0K5@8(mGzYrm7pG!o_?B}c;b{emZk75F*(m!AJBC=q0J6bi=<<;Ul1v>1sz z^ct@7Biv1fg1fy)kN@Nn!{eo*%`88(ki6)uW)6{CtjmLT{I;$)O4h0{^n@3@C%DT0 zhHW^MIl7PkdPH#X@_{8=5i*MKuQ_>$_AHdh(G7y|vuu?3^+(nH%6?~*8F5dgvO2x; zT;H~8DRuhh@pOT!>sbcl89}@a)~ZCMKW6oZHQt)P&VyC?3i)P2kgb~FgVg9$ zaW5n6^-9bXzgV+VW~!s|Q5vM3is5pTU4>uGp z>%RmTF4P*veiw}FQILTe_&((4G>O!-SQu_@EaD3E-h}!L*0`Z-lF=y^AN`i6Gx{#z@t`^(3XR~S^JMO`B`m~(C9Up1zeWXwY#AGUL5~6{*>q!#~gHlz*HHc9-Lch`2p3rc&6n(GDD} zPgq(@O5cdC!*oS69z?mac#gy}AY4tg*6L*aaB&?=sobluTwS?qoQnKyEn251wA=>H zw05KA-~D!o=h4DUprN|28G{j*Jws@`>JrXwU^i}5L%va(_j;ko6$!e@%s{(D=Ha`1 zjP*Eamlm1g@A(!6Z5WfXMsyFzYE;ur&E&3TREGUBHh4Z7Q<7} zhB}vwTRs??05hG>qTZ<>(%pQsL38XkC;(LkhZ)m9Ixq|R6t`=Vu8H~1gf0W4W}1b+ zkgNVBYaPzgO{Mknh}!yI|LVF zNRrrk@h6(^&btlDZ?n8BV)F{kR^>*WF?UyKWO-A%hxQ7H236r@hin8S#jrM=BUQ9Y z@CQe^mC^IM<>(kkcM+)>@Hx{u+dHY+@;;o?WmtY1`px?u7L$+Yded>B2xBO$I5VC< zxT}cdOr8W`DQ2CG8xJ4i4v*j$Q+0C8;|Ka7XAJ#o=gQn3;U{G6&v~O5l)>{hoKzAh zE)r*0Q!AWQN!uqh}^ONxaGeHp1uEgJyDoS>ULnM_c(PknK%KmERqUvyuwK{od`j7#4admkx zrsKPyk@#32gm|7@J~diMN;mH3lbTJkY&6NvmIp=JL;_^aPFO^-ZjhHJx}=OnNS|}RDb81Yk!<&T zthZiG2l9Tfsvq3nX8I@FX~`C$Lg{M{+DdkpzR~*=9Bg<$jBkVX;M*V*XY{xR$%tEE)ts=L7a692PV0hi;tr%Nl^^*vFTWrq7HQ4#k{n zZHDUYkHL-KC6^zV%*C_7f9YJzeqp_OLDjhJFMUO4%(-%cTAd_NH)6AQzX!+9lMRr18N8?ddL@(_$C+`P?eWlD6Yd?sqy$^Knc%g2~ zRh;~XCjCxd_uDgJC|J6EJQ7DLhpwvh7aP*_?@IjOm6%CL3`~T#YnA64FEeNeU5XlWK07UbtgxV)@aLP3S1;aN`SW{?6~T;E z{3$d-w`47WN3L!-E4WN-D>~YQYuHrcScxWY7$YIh{!e|&dQLE`GOyGGhGHHiO;z`2 zvKwBS2Dp$p;nQH^K=mIJ@hCK({FMc09oH#iVETc0{r>9EZIC)?$$s*q$>G-=-{2{A)%WBN?N} zDKEdu59Fb+dJWdO=0y2L2|Neb&ObSFSP=$~3us(+ErRoiHY1xD7e3`5UUhroD{Udh zN>hI?jE5v?f2MiI{<8y_H=Z?U)fX}b#lB2Aa~Q~O!JOAU*!WS>si(EnPw88L1ann4 zRl~EEayc371o6Z7M)R0r67p}0PgG~of9fn|kLGK(k6Mlc!xkH?C6P8!P8-ZpHj#&!P2klgEZ}1WLk-t!-Fi6#b~qoe+n<-~EY#Z(7r>8T+wP?( zB{Lh0q;kL^MUUmsZ=OQ7}(kg)j zPLT5yfyV-4j0TqRn!y%<gL zq>%#NlZkw4d_6)W{8iU>;B`92@06UQ#lQn_uk*Be*P29zzJkz73C!e?^Ek%;K)~i6 z^FMKE2{m}M7#K~sa1|q8Esr&1Ym1W{%;zrDt2F#)XY|I1WuvBLq7gz`$Fqj|^S4}= zUX92mzu)9lKK7YdFUIHU?Gqz;MbU2V3ZULJEYzAYUyh5S zt>ffIT(5eO?)3pLH*llqg7`sGbWrgg&VDnY5pjE?bO)h<9ZGCg!IO?x(Rd(F(l{l6 zn3iCipuA}-^?Xc_)U`+8dTFxhyGb$uj~x{&i0^`#K*>c_fp+LVnji)KaNtGfto>$c z*C|$^+aZ_{i2~JDNX`c-4mkb+o2YW_21p37jC%7FXk7@?j8;PU-xB*|$&3Q`h?o}7 zJy4*XpnCU4s>Rs2t>df8n*r#9Xm>iFQW9nLpSw>+@0R0CBAy60XRXCpr52PRl90h5 zGFcv}1#@Mk96P$8I7Gru$I?Pk;z8V7*$>GAm&*F1Kbnci_9EB^Vx>Lj5n`s7jg&9P z`>Qj%P4+`~ul(bK?2}h~kN%-F6NnY`)cH{Nel)?Z>WpSQVji6TJUNZ7teumk^>3Xc z>zX&e0k0Z7p4C6nk0a>HLd|N6TI!X34xrH!ziTc#;ldX7_y`d!kX`;=~??pFT zQvGyBn`**nB+I|q(wG%?GwhH_pBu*%)M`u$M;a4n<5w~5lSR!LD7|^j>KnuzFfo{lGvOfBGLXaNjUbZKH+zbw4_=CLmF#feoL6~!GevZ@@Ci& zQDReWYqIgK&uz{vslfmy}e8|dXp$DnBSiXI9#$47yhlo96FC<&R6~W~` z_Mwx+|!i!TeP} z$IW0hCSI+-;vGn{ZZk8(fsq1hTw#rlt#Q3pj=)A~;I%G%aX;wb3qJic{}%&k0k!ye zwSD!j5o8a^GB)*C%!nku*DP;2Y6jn&BUp?Erf`y(jK5Aq{@=Go?qN=}NlnP2x;xD+ zCH`e1rO%+9ol4S<_LfJnl9C52MfH>TKr_b4aBtyAd>$4JoW_vw2Uf}VvOGo({Z;XP zzdR^auVh=Lta-DPMzNCI5x(&&aF`#0FZ2dN`2GHFAoi$XbuRX5629DggrQh; zFj4&xgb1(6e2z5&NT8?n!65{$bNn`ILSQKsK!C?ddBH<);T*5Df@DH06B}@>T|iJI zSX1q&qtD32qbg>6c~u1WHTPJ2O;UesEg3(sbSUA58b;3piK=B_ekut1SGt`vZNP310*#U zTZ{yhOx#`WgJO0KwsnCygf2+MTWGLGBL${ir(0>(*%DP;oB3&0@3iX=fXPq{SrMd-A(IJdD54XnR){HnD(rLdKgG?z_vJb#Ks2pVCJb(V%9<|-Qhz!?mrxQ+ z`TF3BFc^;*6`qB~70=bBQt(GOJnpT$k_a@<7w}g@#{jr!sz5#i7%T$&mTPeKQh~uD zsAE}VI^Rb)>_1f*bO*|RxP^V!CyuKt7LI~=I%EJ&BR=fS*|teyr@rah=U9eYYNlt~ z4E^#gAEUCyYMGx*66n95zAiO*tRxrCvRo{x{^&)Oq!Xvn^rdeXZbpJBA4c;$Gc86_ z+z{R4^jU-*GVx}icAQ&Ee!G?cp^26&V9itFBxJ{%00AzM z$5+nbEAkE$)WB#+m(K}Hf8JOxr2TP{)$&u!2k>r~8f!qSfb;Sr z9yCT;pRJXryE14(Xd@knHtD|);Hhdv_=zz7LlnIl{ig&u(*&u>pw$p9dj?YcX;%(9;#9R*Ng0RrcJep3sT%(~Fz}RL@pg}Ut9WVYXIwU$O$t9J z6mzEts|H}~?|-p0Rz(>bOr*>}8Pv8G!1?i!ncq5^I%GZG%lJyQJismFTj>12Z9=}* zmel3Udf|cJzCg8Am>Q!~m{M?iR`~*PD_e}ARPgc8!6D%RP3ZLezG@}CFvAIzDH+f!c)67B@QEEVVW$CDP7l$$&1Md`kWw*52J*eQ8f2+URScnB=7(Rg zYMH9MQWTB@a@0ZxmDCyxpYRc$^1DRd2}`+g;-9=Db04Dl5BmTi5^${raD8Jg|04WP zj^y*r->4P)n^gd(%d{Jm`g%ZhY`@#1mx+r6tpT24o#i6u5TXWX&4Eh^9c2!wUYsL6jfqFINT-=lV+EK@?;8v-?B=(n!OmCe-tk=GgSzoHz>w)>+N z0Pr>_KwAjvdl{g`z~VJs)<7Zy;rs2uxS$CxU=a(ePDix`dQ<@F6kcp*bpU2o*Rk!5 z;93d3c(IR2W?l!kPmRe6Xp8|KN&!*oTr1AqPp?ct+X+(Ig*$!W^uT_*H8uz&{=w_; zuGtLS=!kzonVkaL?|7pI()yB_zzEX@7$9wdCGcQ1P^uCzBLEcT2joe4j|FI*Kqrek z3qZroVZ$n?<$^zSc(VS*XOGM2c-R8~*?NL8!0PyXyO$Se3`$f>W(pN4%|@~5)quw( z8Y%%hI;AkwU!%K=UA(t&NLY=xTWP@gPSy+#h4Zwy1#iZdR{0IWZ7K{K%RVmQ`(0 zUuvU!XA|wZ&1Ri|s?wLTg;Ol@{)jB-$iVVzR&MEWr7FXH-zUv3vCe5}y~>XNFli`( zpgAN`IlMXjyEX>e&9N9t$%J>Rw6;(+uA4EEw97ewgv&a627M&xlrEbc_V>i0eC=;e z3|3tTPi&;fla(0t+I}SfkDde21W5x*%?sS^U?;utYQZ86@Cd+8ltHhJzn-gzx}MT* zYuM$u^_l{)#pxJWcWhOC>3{!++()R9#O;7(pAsvZTr^Nf2c7i(avxzGDmyS(aMN#d z*ED%O$d(=7cQlq-79N!#Ucl`NSOd+S{{U^Ad~^fXy2fgo$8qD|$ddAAB#0MqEK#Ud z5lGkgw%SHaOx2F7@mGp7w!jwguR_~V(>95R8b2|b;92EfeSAFp6gNe$G!VI&bvCnO zw(ABc;X5%$P*PXmZ>U77j9K=J7P3RCzkd@wYTUId$|RK&`tK z{7(fd+xxA(f=ym`i$j1^7H^E~E*X;Tr=Ufyinrwp%T^10@^XejvxszSty%oP{C=Pr zVN#({C_N&w{H&Vs{L<9jGD&cYS8ipMgnpIZH>Sdk69n;VJwZS zBCJPC$g_~pRExF=nyAryy4W<8+J?*+99?j#CD4D}dI3rcbSYopNql{!s%V}8#z!Qb zDc41|LbCvz-$(*IuDEA^I3d^33urYBpvk!}O`hX31jc8x0AK;%C;&Q*qF&{O0xz@Y z(|*#OVBmk2t#Z22Tei{wwcB|H%d=jfz9uv#f_wvdbabza(gCy#a#W#4;gUc1>Utgy zfS4K;e0Gn8uAMA~JM7BzO;iL7iMv=Mx&#U{tvy)X$=xwji9n18w7KoyK2zVu0Hqh3 zY&=X*oNHz|Jnk-l8C92duBhxgoyN+?BWjG`eER7k>l~8GXcw7H#c6f z2=L*~6ssc7UhdCIc7uu(@b17x<%1h(3b5>qLs>EXd%8Paf@|flI|1hycQe=Bc=OTP0G7Y_^t$dd z=(GcD2P!S41cZc$pTJ>%I>26PgD}UUVLROmBHtg~DXJ6EOgBNxj9^5_t?%v3`!2ZP zZ>;R-2V$AXBp6XIDg?MG?qJNuc*^0y9BJJlVaycKws=3oSRNHw9@^w#@(KaQly0Pg z*xMHLJ)kK0O_aNf*zLfN)I>#NmxOF9CAlXsQVnSd&Qda<%{L}K4JD4h%NH6f*V#t5781;cs%CS9zY)uWz?A{z^W49}_@eWdm4m{B3A- zB4}%n+HaoEGoHQQdUGmBb%+4kN9ZiWdE<3-Az;;g6$}g?iVD(U4tq8^#cB4)<-Dg_ zK{3~-V?6bx*dL4m{zhnRCtb)kc>YV;7^zT{`pXsVPrL(rz{41R4=?w9;Z{M0XXrO@ zN{`30Ufsmba+{8I1t5c8bXOThr=VbppD9lPw~2ZreVF-rtuIqPvDu&zR4yt3YNd^%9bUg!3Lz7s@iJ8JbdmZM@!m zrgXPCU>nZEN3F#(7aaY|30bDP4;u?^1OY=PXZ)< zED6hw8s1`bL)+8c{j`Sd?n9n#)5-e`}r5NFqb~Mp4Yw*XSTA{qaoU=#!)7 zeMTvDf@@|S+vO(N?R(Nj(zAZTigw8E`M;xOLJli_yB@^-xuhkRvsyrVtMiMFLQot` z8!K9`2+~&#M1w3B8VA!P+4lmT-PS*EqZ5y}3#LcD&2UBXVirjhPls^DsFJz1c@umh ziIlAcQL^fy5}L+lw4HgfE_dR^-nv@2XAwwTs!O&q;LO@7#9;5@V+}+ zQYW5XzfjPQD6-?c?1)6~mAfYHem&6@KUYUB7Ih+p|9aGq-#0u|tdkjKT9f9#(`m{a zm&eo^9%O*WcOouYd+iF&fENkBGdc=Vd#)lJ-roe^;5evX+90ga4b#G#DE5ZC}{u{}c8ScFc3Ol42ohHu~Scz+bIaG;C5hhIVyjiV^ zInk3Jnx6e1aS$$3q}{+s5hZ3SWQt5Pp@>B60cL(kf5hNSmCy-jbKdpD_%!6~w z$!f1)JF}6-dyCa8APs;e3E-~Ktm%`Dv1t(hjT0lG8!1s%BS6fMEsv1?VSg8~+;$+O zwzV-1w&h3C_V<5@*et5Omi`ei9pR+{h@&k^uj@YC`Q={0d*#FOo4<_)Rp_ zk&4;UOq68=L3;Z=msiffm0T6@NCv^8yEaKNm0y%6`9yZd>v)4FVrUG5in#EiR@U*L zS*Izde|M--5&aSmc!7oZDCd-!0}pJ@@nnl+C~rUgaK76uDEsD{bvR$sKe}(oy@v^_ zYD$a6lLke#3E1LF6R47Y6wYLxiOvaO{hmZvJ|T|&aQs(aVZ6u!3iIoPmwVw-;aT59 zC{Gx|zA2!VQevvWKB#0jBZK!C5po@2R*30p`Gx{MjF`(@P=_g912MAU9s1M|0%&Fis=0l-~zy8%XKdv z3Ki+0bh)hS8vjbG{1xfH zhYbHKOMb#ctVnGO6?%dvZ}y)@ZVsP@=qnmo|Caw;G{qT4_Fac_j^FosF6{lCEu3uK zxDGGJ4f=#xG9EBKs98M(bPQ|~m>PcJ#nlR*o7)I@e7+`=+V~P(?I;oIyMmIuAWl+g zSQY&0e6SLz6h2IJofQDn@(rT6Rj+4aipaf3%IrjNkGbkKlcQGbK-5X49}5Q5h?P24 zgo`&eq7@%3PX%xR*8}d%Y5=_qyND)l9A^lwJm>*jV|%iiug()-v0nK#as5Yz>vyo) z4y7N<*0O^k!jKlRPqlKvCsbOS%v%M~(kT6@asf$#4C$)@=d6%=Wc1k{VV-im?rJS1 zvlZN!f_&ABqT7ipfVn2G$*cD+o3|hHj(GFL??%}8G5u-&klBa8cHYP7Ijn^dS4-q+ zRA>7mtGCGUkS38u_X#uJ(dV^Qr^*HO31b~?Dk62n!>)1+CqvpV_}t?Ud5Th?HxMv7#Ho$_e-T-j4N zP9dsFK-?rSBY-`(Ei4RzPLJsl9PqbvY;O( z+pkp9RgC0A81?HN%93@gQp)#aVpu$EBJN^KrEz3BU<(3=UQMpI74$4&`35?|A>$V@ zOhN@1Cc$*&4(Klo!P&pxU|_N{!~~!47>#_lgC@wr7ETq=qYqI7_w{7cWPL93YU zUl-&{!C+@u{^y1Wz1S3)yaQ8VTy6@C|ERva!SdM9f_2_FU^w##{#CiehLv_cE%-3D zB7|-}=)3lnD3xPHi$bAtZ5~_xuAqB8XC}1|If!R0uQXYjSJ0?HQ7TN*8kT60juio4 zV$FoyAxA8i^us(PLDf#75{qBRWCRU&{~aT z2Db$&(P9X~*UuI8suIm<*o4P{~Rcp!F^}{<|Oo^hq zUu=x~4M4KWj*c@Vsi%pdh`F^@zd1cMWbf^$NJr2BT#K7`kAM;@){+pqqnID_u=PN1 zrfqP+7Zt!Nk)=br$LCLSekktc1qRW?>Ey6n8t3JvQq?PSM3Ozc4G8OZyfH;8$`@h{ z@|S8^I2CVYg)ssNQ7D%gBaa432d+^MbdGGL8S^vO7O0@O{Cp6BZ;AS?O< z#0NfG9_J%mpxNnT7$C(;F>D1?g0Q!6cE`_`=_44sg>8_%z4@~Weg&8wymZch1=P1_ zOIUH;FMC5H=Uq;|^M;mI9LEo|_ErGe>@!O19b{|2*L@5tU0mt}`e}UgpwiywoH^c3SXYLkIdUVyO%d7)QQV;)6lF#rz+^(|$hA{O6$4ROMTZlI z5--rd0HxFxuse9I=#mTdmiK?71d-BIi&Z+uxTQ$Il+HU;F!nJ%a`nLi&MdCHpGaF(esxIt*we{KKyA zs8t8cPOH5){$H+_oict;&9Qt*^O{`;WmqFUa6`)Xs<(FsDteDZf-($?}~m)S`Bm7L5_4AGT* zx~uXO+DA=FvjRJXp^e+`K+^s`#ODGqnT&5RT&|7>Lh)YD)ou)lNeiU=DNV)LFcZ0n zD(MmInRyonH?ib(8jIO-re7z% zTlS(7=nvb1&qbur)cBb>OsyA~#lHd1#4>?1bNuz93$UOMeT3GVLx+cOJdY?0nEpWz zCau+mKP4rE{yrs+u6Uk<|JcNh_g8*+$4s{IrB?8!(pGyG(>{Z7p&jAbL+@*$(TqvWlr6tSlf#>Y>z`Stoq_`r(JL&!5+Ch?4&qG*lkWnE_|@(5-sSx+(BjA5 zV{ux7U?S-WOzIsd%v7)?Ly0cjpORLsYZF%dhv$H&Y$d~g{`;&BU9$t`4%i6asR+T~ z6*M(OC4}3sGbk?ROvw{2*0;aRt2rRIyfDVSyivcFR2rm&7wah{AuZ{U3c73GS9KiA z1kfphiN=%W_Z|$Q>%)fx4*nc^jK0u9u*47Eg{?=Si);fW9gh4qUg_<;hf~=3n zmn;Yt>cy)!~ARYTEmg|C@~h)r{e60e;0BI>FVZ(esC!S8BFbqMjh zn65;Q+&SUb6MVnEQbTSvRFY?|3$i9Z@VANa`T*9t%MnYjk*iS|*URa8x!_I|xx|BbTm|1Sb(eYc_6Z_sz<$4eygky}!q<|{G6E$5gQ zF@j?AeEEi)@?i+`uY~zTI|b%!yrO=&FfcEfsNp_HmvRiF?XMnS;8Pd3m6LL3(vJBm zBOOgLCY;Ve5#xm##YV1=9`J6h#Vw$=RpN?+`)JAh)r2uImbr0I4NdO^gJiMR@p$<$ z*|X#vll4siNh(q;2c69Sy=~yA&@m8A5EKXNU}Wt-$bMSb5~{P0jtfJc7Hw^#zQC$;c@CFgsZ}c z9Z99$&?Z50JSo1+@=2956~ag=y}IH$4lI}c%=KTu2hL1*ctm(}WOumYUoxa$@uH&D z#tj~LMNu3rZ-qG^GHH24e|)nN?Q#c**2g?-28jZ#zuMKQQhb-HpuKVCDD4Nc&!78y)|yC)XA|@A~ka7&L_R!e%`=$qwD@bEU{c9ubzt z7W1&pu?D#t)Z=IB{S!S3i|8;$1U|3$jYJ+PlJD8c*JTRMd6a?b<+&tKF|c`lMlp${ z1yX@?>*o&(BUDPqWU&_L>o~6HG5Pf2nF+=2Hpa&Ps~_-wr}Exu69Y}FJd=QLCJ`ao z{y#3@*&KWJ@P-{?$MEk22*K315UX^1pXdVfh*p$H5G-GReYXm!il^LiDplApj#Nlf z!=(F;4sxwFh6*Nga|7u>MU5BA^CM!?Rg_rJOr#wk!9gN-^dEn7;2P9VY6h*m9e@?0 zD4-Ntw3mtJ4#kS`UoE;mo`CBG7`aH{qf17S98MKYf_9hA>;)c!L4!CxG?-R_;A~cE z{6b@({4DPQ7}()(A;Guv>^qnxSpPbO59n+nC^37ZjY=ixLfxKDENw060qOE7h2Kco zTWJLX#zl^lLzmL>YuB5>w;98CmN>E2EaxTQie;X^g=oR`<;aAbXsBolv^NU z04`(jC!Z65Y2cytfoUVyVLP5y*Spke0n-cFB*sS|Q}SiSrd8;Wd4WcZ?GI--3|ft6 zewghy8)1d18+`ybe!-O_aoLPn6aLZ@h0NHPDHej66*||y2l7!ns=$~`oct8vB?g^F zblem+lNbIU_=!`X`~k$i@?9XtP9Y2%SVlp#1`@vW&A~UKB(w2n)JZyqUfFO+IPa1< zwAE|{tXGzZ7oBQVt}}Nv`-O8qIL5mdc%eo#9Lar+4w-=h3o0y5869~<>G$ zMjm8sfxJ?+L``@N@pCSi?P6hqG3>AF6}Pb;JTD=zV58va*z4I(1dIS7OE2+2;B%hM z6!rtMZ&Cn@<<7v<@i?gJ&-7cfRs`z^f`D9c0h2dd)IgVwo)GU#-|?fB)-+6ifFXTI za6w=14ogZYK>#x(0={HX?5e0~ppEP=l@a2zm@51OR1*N{Gws+4g6VqA*aumBjQrT+ zTa$R`3G(>1lDJipk_2XoTo(G1uN2S7^p~CGNY%(}21+6---)>LwDTvPGu!O*d;i$bOD3$m1GG z`d_kh43!4wr^~Eekh7kgH*)Y|Gsk=NM4AY>n7Ie5*|LeV_UYcg`BodkQJfCj2 zXG=BT6hjYivirU$7k+S`0>T|*9%RqFrdtP`C_1UF695PU;bteGFV6?r-3ml2p7)2K zHM#&}4g8K3Feh@ALID&MP)b;Uhqymo;&fOy+iVV=Gjk|E7^`Aw7Omt;#{o@3g>DPZ zn}dl5A)sag$A!Ke(Av*?B^6*)d=9WjcP{T{0uVKdjG{a3PZhly{0r0*yDP0Ns<7Ur z`W{Pl7PxR7^_Gj^g$IN&>Nc-gGb^zOqD<#lueRAh)eJFc)m9EuaKD@}KL+)39k6$z zV}#ev1G)>HED^Zhs@NxS#-N`?qzu(@m{tV|f5KekFNtR_a>8n(*Uc4jDXi%B{zf^w z9R38X3ZVZ7QqCj*QNe_4;q#xJKRE`dkVV33Fm471JktxPHju^hLH4laVm&r0sAhP9 zBcMlH8^c!D@%d!y5~-C5^CH-wV7to$%@?4Q_&yj`g8d*TJO;9oVxiqv!JSvT1sNdf z{1%L!>M${A!)rm08WM%4zT@N$c1Uu$Doq!_c+>4-hnyk~6k$rqNV3rv9{8bDF{cky z>_+3?I8(^Q@3RdeR4=&WIdaLS@O%Ih&1@eE&V+vSe@L-rAmz&q)X=6X|3eA*u{X0e zP#(=+M+m}E-3tHi(!lNkh9sZ?FB@GA-D@hc-w||l&Kf82o!a~d{{HgN7uf<=m)OIsrX^D#x@{2vhJPRD6J&h51X&v!a z5dFvb25NfzM9R*{dS8_jZd`qPA~f;6i0L%>&n_S(B{aJ=I1y_;VABc3b0=)|(bap! z+YKzPyeu#W1vBe+Kt0?cXnKH=rO<2U_PhU#9y14A?U2Ll{LNkh@Fv>CP4P_jJvI3GHgoDV9SH1wT+GAyhwOgu&3m!Cq=#ZBP2fxVD6u+O3I3brv=H0W;)K%TdPkWhHQxV|J%%UiWx zs$F2^W`pUk3_C2;pyYQFpmH+hHIe>iBQ%$GL-O+vh{9U#agmu4mKwZJxCj z#uB4}Oy8&4u%i&>AE8hon*Ld4%axw>+8*8@kW6Edve0yj=j{dJu9f3SA#T0stBNtk zfW9|JJ8SiFcM{>J@t8ZSp7T1~(%x!&dxO-R6`OvcLRMxZm7O9xQFe+X{_p!8 z|KInV&goRA>v^90c|M=_`?cP7NJW1Y&D&hmY!^tE79L-|qWEWMY;@<;(Ec}5d0W~)5z9g(_bJ}K zvwOFK9OZgfw5|0%GyihKeS;NWL6%)!VGG^&k5*q)7=}z`BFIx*GGpJ!eiQkTNGs;8GCcX@M|d$!11+o01wRjcztqR_yJJ}FB?XIzpLz-oL;r;g~`NDYZd70P_O z@^Gd$EAp|v?9bIemzMd$Zkd#0xHXc3pZ!`R7A$VO)cmK~ZPB!*l(`A68Q|jePqpN_I`TR{>9Zw&fDk=H1j1pMHdW3D z=Uw*mnq$}x+hH+{g%d%9NSWdV$#p(8M`TpO^=@QF^Pow^6qMSZ6|Ol&#ubtnoQc%~ zTY>>oPmTY#$@>hgghe+-8_1687|uUZ)VgKN7N@o{%u6G_E9L040f{RQ+bhYlcX>mc zFAi|n1^MeRu4$~G;=A)y-73-E(_`^IHEr(G8A6<0L#OrEFhu9F#2MxBT`3UtT>hfx z+SRftiUM6`3^R6gxEAa^zBZ(&YzB4ygX7wT7yLM|s?|G1N!8Hb#2`d?d)SIQkx_Kz zvo^lH^?4U%L9-p&BuqnX&5KTsdcR1shh6Bj4IU6Q!Zx;_4}d<@z5M~wxt&n<-BfO* zq2I-$uxt~BqR*n z%kEka^&r0uSHwYqtnW(+!*KJjbBa9Su6Ml6<0+8W0Vj><8M_&JI$qOr>I8My-M*`w7n!=9-<(;lsQ^g?>4&`MniaDd$=oS9AqcES#_ z$NdN^ZU9e06s7o9|6-LO-wFdy-J&YFAK~%?YNdly>DTh){E;fDOpBC=UXC|}+_ypB zSP;Cz>Owxu34KTuhNq2XXp$VC!mgEUm*hBr)27v~hd86&Q$JF7twMV#o>6G;j2lDA zWl64?cO|Qn*T&z)aMEN(%L#`gFP)ucm&hk2%`1MIkSW z`M?^@+0HK_m2_ zT%5-;r=ryG%DXJo(uF%QHI`Ut|C6q6WJ4%WVKs8W3)$#9)Hac@A_uqW~!Du zU)9%tYx$bmijDr%Q?>vdE*qb=jK4QGcExZ7c45LOaCknD;QDpm1FB?i=TYq9Je*-#Wh^+we{-zNCfpO^&8ec=d7>SGWTHsgE*4a7-~ zGclAPp}f{i{&G$aACu&-lgPk!1xp}R%G)E)H8Zn**{bR5o!M4F@zS0L3L>$Ub#ixm z;wvU9j^!5Kn?A7;w`gm|l^OB}BERdd6qLQm{S-2NlUe@7;I@DCP(bcTd+7f78IC&$ zhh~l}HxBi9rQi&XFT^iW@elGQE{&-pKw>N^W4e#C)2^&-Egm0rQH==_6NyJ0+ z8b2zf06=2Y@-DTYFO;96kDjsQLk9VJ=z5<;(jwc7A5RDiP(0F@AqB>ooHEO0KgM}-rHr>(ty*n$QchUeCLFKJ@*GAoxtA zV^CduvCEIrdaNcdpCdM8EBuQNVw#KOMXu9j2pShva*g!^xgwpeo;B8IGO4O9Wx*EF zgED-7>EK7aBXiViig(Tz@4xzn+MtW(nQls-qL;IuZLvS#7`3~EIj$60O`IC1Vs>Yj zt-2{z_UVSvDB%;%EmZ0Q6v2l857!a0n(O-a4%6R$bi7MW#&qE?rq1pZzR`1^&(N+% zibwt2+nSp@JmFrM>NB7K-A=d{%6n10avYxMc}P+ia*L$kAz;b-(TG314#0a6smB7_ zjN0}F*ce{_Ebb@LuK}?@_f3>!g8m$UBlFNBu)|4hz6UzhlizbE)#i1JP$HJ8s|@!+ z9zm4R+BJzp7o^}od}9>0XrsFQ9MZsxdO<^uKO0_Q8IPH4e1fqbw$E3efv&d;|6`&) zMo%r*X6%%rz`z11JQV#__2-Ue?fCM}v;ahtM!$qNO00oTxe+oXqfpqXB5D8C zVZ6Yi!eCJ%BQT}O9=5~Vo*|92r|}5bn!W3d2qSM7e)}5-MF5}{V-c2gbl)a~jI*dd z+{F`=3_zj9T6#mX00|O`kTQVMNc+SJ63xnnQTq~a9EVc| zlAH zjGhqQRIZr0uF?}Rc%y;ZDfHiLHYN`c|DvOaah|;FfR_xVGAF?!8qCi;=Wc`DcP?z7 zv&;rOlTz+eC~u1K9iVTZH#lC1<3T^%<48ge~tB8Qokc%i4t>`vzcuzFOC*+M=(1j5L=Ar~r4R=k}HarEq0k zg~~5t8ogM4!mMKeyIS0O=hYtr02xYcsE4E}1z}^y&P#Te3$b$e$#EE7t-2Xd8S_#f z)Ee*SaN}wGtjG|v*aiNZj@=Q^-6+H#0426{;rkp639E`>X<18_-=tY09MlGP^LF^W zUGEaXn^gd3y5_somK7iOzNcp%wrKhkz|Ol3=T2pRlyHmx&ngSPl20GE55Jixj{{bE zXLYy;kAPT3xRZ}^;gn(SdG?od_6Jb1tNo;_iJ!ROdP17Xq66d&y_N8Gq&@icR!yX8gUMUU9U zr%gPA)lE7=a&jwSqV+m@=^oYwPI`L2gb~KiACjrIBn&c0y5C1x)9F&1g*Kl^0X{FIeo}3rMki;mAI`G;nwmqzLCunO&Sc zI}I95h^4R;m4fs#k|aAJ5B(BMT5ReO)BTMrr&({^xu>=kZaWTnHwn}_-L?-E>wd(1 zxfZm((+@DP&%UB~()=@hnnPL_`Sv1Fo*(^Y6b84`Ws#+yMnisBP;NP4jL!95)6`}7 z*wFmsJ9ijuu<0+A`=Our358mxxBYeVdPvu8!t+fuW8pK*uUoo29R{rcE&+K=am zZ_A3lFT8fcJM!PyE!&6e>$C0b=V)Y7G0+^In`e? zTFrDlRrju$d_dOzX5hBE@8RJ6tn>MkK2LZ3&Oc&m{o(b6-y$CN~h{+b?VHh{*ZMxAoMHXbwEv!Jm&$x5Qa`pgRCq_)=%{z z{}(?|&9@-4(L9lA*`d12IR2Wlsa>6oJk&#{Gk{S`>1-$6T?W37x|ip~Di;E(rbdl_ zpILSSet`$L8QmI-qHQQ|CwR3Sj2L-*4zlLMMAf}Lt>b5@Mxq zPQLE#h=>^w59qb;7LoWAq`DkNmH6A2<*9mtxVVn*^;F$zk~5W&*++}^r6z`>A7 zYo8@MUSJ|cBFs(n=@;`R=Q)qL;?j&uS?f+4I&rEmiwQ+Jmz64_MkA)sI_-RYW>%%3 z!qo0F%KN&|$S!2m8Df1omHU7aq4|=apsNegn)rWE;-GvCeb?J1(wtnv-iDjADVGUr zV(`zINHSIpGKk%Rp0Yn`9fo5s$ahjZlqUq=Ez;~U7ny&M{ELbc&uIQuv3Z7dL+Uv(^ghGk!#yI5O8(1#YnXQ z@%c8>mvltsc@8JxERh+;7^{~>o_-ili@$51zplm25qUQX%P*pIH|nA6;&ipD;oYc# zEn~#)gT^Z|i>G~A(yzc88cDH;OsP1blJ&6N(C1 zVe(VfAJJN^8Bs!6oTB{Sl`%uKZo(uMQd(FqN_hr_t&AV{WjRX6>tnw?b(|wN7_u@O zT2-zf;#9Y@X;Za(@0WUYt2~;=NsayC4T^o>V^@KOg{=z+z}KMEB0B#Jcn6?ITIfI*M1BL(4Fojl zXqvk3gJ43Z?MS(qM zgs=iCs&`@m+Zx^*uAMIZW_+)Nb%KM^38lv!Sn?SttiDqy%Jlu1Zdd1eJGE>#jXPyF zhabs^Qd;Z?gE8IRUKegsp}wUoMO|?VA7P1d_?Jrc8UkAc&}yKXD~?`_q>#W#V4Icv zR)+!d2KVJ;v$q543zXuF5?8aO1Se~*QP&aQ;gXWVh}e0l^pwj)E{> z8=qGK;Gv~?uPF!FEFFTvIhSOQ>T@dum|VS3Bm&oEv~-<)oBt$LEe~%Jd1vSduO`Rw zE0sMpw{NP(GTaURme3u?P|BCiG)>IZN}!JZS$kn@T~uCZ*+(?vUo1eymJ^>B|W@ zEP}V@7yu5ix5NvobjkXE9S1h8&|~$|=XV8nfSm@3dZQuDcR)1 zE1pZOPdC*7SKTxlBANsw8BxYo46}_&=h_V3Ef8~k%CJkT`|Ig2X=2POQ>A69xa#4% z374I2aCJCw!NzTfFks)5nJpm`_Kgt_9PmtYS<)?&mcgU6X0(C{olCgdR4dAvei|c9 zoreC%lXgyr#2|^?+)-)ZNviQW@)urd2C}{9MmMRiv@Rv;RzNeB-=)7Gw0uoU<6q|~ zUC7V%5C7vCDce963v1y?GwxmJiX!1t1V40PKeTW$lfzA(bFgq0H+$ph(+&W+vdRIy z=kfBrD_J5YI#^gRUvU0R#l2yOcLPDhT|+>2_aXRd0+hkHBq2CP;r#>i*2h}A9$+(f zfvL~G;B60CH}K+~I*sNPQve4fD1W;Yf@^HrapfD2ugY&0$lL&9(fm!QsUUz1SrHXh zPB#A#)D%EiKCEb;FLmjDn}|WJfal zdBn|rh1!Y$;gz>}6#h#QBwXd@b;X%1j%&R*l1@0#g=w;e)sKeG`qDnp_RNTD%@W`Yq;J=Q^??cgCIYvuMG8m{a?@vJV52jg$C6W!sM8B&G8X16dK80%ZYEK8IXkWN_F+mi2 z_Zc$5R%p?#%T`l#{Yex7D#nkJI7v>Oov!F>&a*?#WIOGWC=rtizv-JzJl^mDUIYdr z2E~NLR{$=dX@V3u9yYjTCZe4-zVIM(EOj4z=Vh1k5-y{+N+tlKRU%GN>;`!%Gp$m< z9!rQFc+W^la|;A6WnqHJo*hGsh%NaN7Ko13knBU~Fo(nJr41eR^dN}NqisiXhqi}o zq?2IuvC--A9}shcu{-BDKqF1EW~zlrj-?^{l9p`ff^bB;#JCjx&iV0PvinKv_O(G; z!==K8YlCR4T(;VzM3$@q(Ps)d?P5Rv5IA#-{e*U1_lO@)U#9+1w)YxXqA4^stmg)Y z*9S?%9g_jAqY=fOsEUk*iIh^Hc0Y&jX3CmeT6JX zEfp`7byx(YHmoolFK*R16Z|C)kbpepJ4)sx7{UYoffMlcb`s;9rC=piePG^kD;O|$ z{bjIZYMi?eZpBcvKi^W=-z}=&0X2Ea0LWSKLt_0<8!1YMq(9{Y)=3rZp|+jRK{+=vK#X zk8(4N`0dLJe7r|+EyWR66m|%MgoifgUU2gf=4N~n(_|nf=8f$Nzp%eG$K69L87(~0 zw@N{I*7&X}5+zMuL`uNme6(< zx>c!Km*Fiy&v>ye$msYh%6}LdPVz%H(WUcsn0S`cqXs$&Bv|u0dx;aC^PNh)Vh5D? z)QX#J-#PBt=woP_pp|E>Cwj@F$*0+|FRYRXFVQ-NmqlR}-SAc>X@qZ^`$-f^^7c`X zI^(QrW2(|qdu^2v6^(LEMs!g#h>K#nNq+|!BDc{!-DipT%9&fpqzaM5bbE=QZ>Q+E z$W3lcl|vjwa!8ug6Diu=EXqsyiLk!UAw}8r z3kj^s*w ze)U@TBTnG_+C*`d0DURz#_Uy`B_={C88JH$jxivKn$bggRgueQT6bxDS+GNdvXxrCB-ucUGF z=RN`%g`kupw8xWUmPGR!6C~YKXk+2`Vw7Xn6pJO-ZJq=@1Sc6FRzr;19!1VF#Hf(zR=Eun#{?Fs65Ex~PqBnJX76+#Xf_ zu&sP&Jcp^B@{+}yj9&p~&*kuyY9?^evHt3(q;WXk8o-=7O>;5*+}8@-c4r}>cGe%$ zK+1F#w1&X;{abbUj;kW{v*y6ZAjZOMrcR1l?W zIT)F(5r`KIWj{fJODXb!(5431o2nFrhiOsn`b_aIXEL4v7Hc6(&8;LRQRU%-=NAH5 z6onluFNaBmcu@w952yNL{oTlj8~rj8CWJ;wdWp)(=5Bp|1A8G^4rSGpJPP#^uhy-*i=i`J#&JyQyrNZLv~kI zkxxb!p5|&j_RDU(BR$KW?jUv4i>P^7c#DjJh)!NXCvJ?pRMvUTw<0h#;FIoPo`qpHbCe%iG z2R7ry3__|x`DT|5NJK*Q&(=F#I-n$G;SXQ6i0=M~ho>s^CMsdQrdC7Tp4@ud{4xF_ zelBakQI&OTBa}AtrNHGqYa&|zILn!JouNl4^N^Yh4D?*hd!v_%bU%#oIM1g&ho}Y+ zp~fUg*IbJ)s{2|0bJC8#^Wsju-NaeZSm}^CCz4N&VYu+oRVm7Ds3X+$!e2&s^Qvcs zQ^$yOcPm!%(Y$+F=15)K&MjG7Xw`++OlUxGtYRPzkVD`jI+43~BMM)gpS$G?QVx)1 zx?VYN|DoFc@NJ)tFU#SE$nhH=XG8Jlfy)$*vQx>7DKcXkWokDqy~nF(%!_mrZ7L!Q z(b#hHVcHh=ik^l2C#g4>iicF|KUi_IJ5?;pJK+)sFe|4Vv2BQ&$VN^K*y z8oQnx`dh;#(EX14{aEJ$5&`Bivrja9V|P$}cz>gWXVBQ_f3bj_E1e)Pg=o>va6IU~ ziJq(2-YACVz^%0CD(hhR^%66kOGQHXjLHX@LH1x=cZv* zxLNrhoG`E_Vg=un;P^@!73chA5c0-c0$HwE-fp2PyqM_9SYQDwVt+=CYF}YDm`LW$ zZf{CHU8`SXqv+Lla=iay`%4OGYCl&4j!(0T63qvHB8ip;?F5Amp*{_QuQA)W0J2IO zT*rEY69Q^zPT=1u%1$!~`6YbPhK?QjLt#X$N*6SSnMdI+wn}%PU5}&Y)Eb{N&7OO4lRg|4TV|*Rf~`$>vg$=&A-gSrJ>|i$YrcMYbDwON39L zedkS>_dWpR2vr-CUK(;8taT(K&d|~?N-}>5y9&)HOC8JUfutRRJ5?oKJS}#}g{)_+ z>Ob_G3lmV|(ilOUSwCTEt%o@|)cZDr^W0Cmc;dm!`O;+gS=u$0PuTKJT{vo&o-k7* z$LSB&|I{1e>Aegjol&`OXB(PH(Puz~ zjlGQ@!i!|RKV&D&X^^mfn&HCWM{DoaGJUyawul^;QngRSDWkNP*HJg^NmzOM5I^#2 z?c;=5HR;-ys+JePF(`a_;HxACU#TSIuUQG8qo+S*vj?C@v~CF_4qVjmJ!veu{xaoh7TC;dL@dqH$m$P`ZU)7y^YR z8#cG#m_$g^P5ev@JJRNGQB*rGUIWfzjiX13M=DH2wJot40B(VF)tdp-c|3B#jnlyZ!Vnuc6-mpeZcRs&GCJ1BYDjudp3(ulIte`;jt7T8a4xkN zKQ;txEsKkj&B{Ss#kY%wP5HJ0K4RaNoGZbq!kH-#!-W>z&IWy7AqZ{2FA{eWw9NAX z7uCl?8I)VK&rw1$62{Urxxkhi*G*&+?fK$QfRGqYjr;Li(CNxG^XOTu>pk%YO>I+8 ztg5d*N7l?tVdJ&?3H%|rPi6Rdjf0kFDZoD}iAsO=jI@J5G=x3X$@d-faPg%V!OE~D zboNpmO!e56&*a++VC24MSV=ZlXK$DtCA&`cl{j}dN_O>O-WytRkyS^rt&GeAchYBS zcZOn0Y;`^uXAZ38^(U=~aU7%~d_)WyD@t?lxhL$-7F(BOSdQjY$=9k`rzfXJolR$Ps6@5HD~QFCBN24{mE(k3><1pZ z2!qSC>ec2>J!#82k-MVz$8{hTo~RO|EdPbIr=^!6J1s@DZYH@z&Ph<9bVpc=+z79p zZSV@YvvE?6BZ5%aUHgnfVO}KnOJ6}%)44I6Gsu|7P_NBAiUlpQ~gAen2w@D#IHL zAF@h^9!Rfdo$CFP(9n<;&Ro@emX$1|KjDqyi>k|K)(etqff0;;vVsf?Vx*yvt8(2o z=ckK%_qY-nx%Y@iF)^dB%Y;=D^E}DpYL>66D>K8j{o zFGuWJO%`pd=UMRtr0R9m%~3uHEU(Rc5Ru7!Wl;u{x*){<2Xpvu|VpP z&Hdw#X2`ovh*26!ZnGmr8C8=l#vn%Tw{UXG9OS-G}W;x9jPNjB0lvsplf9bB|JRG2$&ed}|h`2Fc#fzS8P z4u|@y*%sa_J~OGCHY1t$W+pw4B!yS&5#+7)J)5L`ZOuJS8rKtr4urX|mD(P;^)D z6KW3G3{MPJsLoM}pZ=SQz(F7|qv`^%(G(P~B4}T~DmiWvUI9zxYqm7s02Ik9Cqyxp z1FeJSgwPp?On!QqKLD+DXec*ew(ceNtq^IiRXg&zj%jWYkzlcf44%0C+}>0XgD-Ko z{pOn(^P7RcqbC1)qS^y8i1hkezZ&nT+WbtU2@ z+NR?D5G|!nHr?dYXs*iSmuj_!X?C=Zsi%n>Mem4zLz1_|#o$-1BMrqjNyP$EQ)bZ@ zGZlo=D&orMG;Z$t{LeqiLKge*`U8<;#_JJUqx0Vb73N*R5>a5@; zhOG}A>Rs0wRk83udweLTBN0V9{OW7kin)AVX8aIeCB)z%+(R+8mgr$Y5^NTrkLlw? zn+2r4kSnrWMDJYUQbz|AX`4_dgb5w~(~+S@X^E-1Zb3mp!W9&r9*=XK!SMpPsLOC+ z7chgkn9Eu_H3;b@i+Ok@muEa_zV}Q7=7&IigolwK_>F~fdfGFhMlY{mPTUbXsY7T6 zxUREq5)AN^p3=I2H(;t%Ch%(Nu0Pt7~92 z^$2tS#KY`3H)Y59_APxMYNM^xwQn`jars{fYu!HIGtWBnpy><3t$^X>QJ7@=(-S&1 za%J6DoR5i&Ji4>4zqX5ZNqYZ~^+St2%g&l7Ep<})CE>R@gRw$lQ|!EF9yi=icVk(u zUlOkfRpFGSWcMS|wK5_&e|AwgCO&lxZKNcn=Pcxhw-cuCX8Uh`(~_lNQmr*F7w%x z_Ur{cxjm@s<3={hagko$eo~%JG`QL4`ChUufneKq(HZ7uC+9zXHLUh1DF$4QNTNgw z2A3Dv1=u+Kk0F?PvRRU%RIdaV_ycTJU436CDnTvT3=9TNUfgv5%r&lfAeM6F{M{hu zPBV!Gm*@_%K<@Zv8%UsotdrLn!ec`9IAv!*JNI)l}c_sG-96At+n*;P9#Wf3F0)^*rz049< zNo#;!$_1(jEJW52WmEmJk12EP(Ul(Oqu6rOCm+ERNhlD>+ud7kP*RA-P1%6vAnua3 zLdc#A$nbE&{RjdQ-f`UgB)AcrWw0uk?UT)I*n{#F#?5$+V&mUi z{k8lZN1ih3$QvPl4#88!x$&ZAIz8F1uo!@o0oe4#mgW@hxzO*O& z>kNUSAPUYgOMDWS;3Cx+uR5?I!upx>c`XjUUXlWwyAh1Kw5WiYj{rzOxDS#r{5$A3 zS?Nxd-^Td|8H7$rE&>Y{E1e2bDl>6jD=@bBEDzwJ?SP4+xv-avmQi^N{W1D{#HkU8 zb9SN66g9gIN>HSt0)%_w`0IfjajSC}HZN*?@_O702j!;34fx#!tf^4pQFSdspQn`u z3uQs82S{K@&0YJFABxc11n)8Q>ZzC0=I9BCDOCt*QsIxq12CG4++UN8Rp`vH7R^g= z6Y8%D;y3>cc?}yo1s_2NIt%Mzg`&SB5_jcpu07r^EBY;WX<@{;gvXhkYn>A(9|=g9?4`Ev*#2jY4WNBE{Pf>z=l&{g1i zK!5`xFl_^7MhqB1d%B7br0sd1MiAOU%Krdx!!Y*;JfS`oO1FtBD1(9gK8&q zG$CLJxON8+L_^T`1Hj<)E}v5gT#&diN23pi5vh0xB(>i?AdiE;FTc%WA0*oCyjgzM z>j7`28Az6p=MSK2EF*tU6Fo z4gM7N3H~>4;txJJf5wvPZd5}WWV(H!+4bkZ<|=2MH6fQ|e1KxPXf=pV(EN2q2qoMI z9L-YlOoRjHJ2%Apy_S{;sd|ws80H6uU}Xe z2-VCp8FRS2@8h%^Ce6V`cy%Vtax^JR_t^r$^JB*E)l8XDvDa)q_)zC4FZ?yq0r*Z9 zFcHGwAA#TJ5@6E=b?~RzhlPieCw-+9l0eC>pOCcrt??c!=feIEmQ*9h7qOYA0jc2c zfioBTKGzTe;z2on1y&O-gLFU?ZuQ^58mR!&HHq)uT=BSq!iBauxa%xE|3Vca2f28#fF9LSJXBWJr-sMp1G**IQ~}M7_vpee`51A zyMaW99W>;%c$2v>2s(twV;JiCqIl0R z(Qa`R4Y(odhSfUdQj88x-I(kLNY%U&rN0KNq!(avByE(Gis*e2wQD2R<2|#FvyG^!$@RP6>^er0MK{N zZCtypGKTld)dtTwE+*XE1Q=5QFrOm~C|WdyguW|zax$QP0V^`zaRXpL0BbCoLaPi& zG|uFJ6WNE5H>5z(dVMhCG3>aoFQpxR0GJRkn80t7IR8F;jG6|6g;m6L)q?{tY|JZ1 zXTzj}RHiOIhpx>HP`OYE2y~crz$j=Hl+?h+>myX+3w2ScC zOoCZ{?Vn7Qn~EXEagQppxw4`au(KTT?5~uZx``+CZ(}IP&(j&<{S&B#YqQ=eQm(n) zXLT0V;y7bo6#jG~_K~5^lp!Ke>CG--;AY`7{_8R%eJ=`oQiC+mmbtl_@z%q%GQK}+ z2&>l$rOhYNXw!hyuab@H={8&2=gZC(Sn5$;Qds6*)Q`tVx4IsM%)C(gpG-B$507tV z+C*sPE`N`6E4dSfQSc3YB_G}qrLhu~* z_D%lnoAt{!7Bs&j5o-2tYOvpuzh569;>>rPezf=4Z-B#BY(-yG?U-1&VG51jLxdvk zw5vIrT+@{Z)(_{Bo+zL%4J)7b$^H)jpsJ~iOO8UlW#q^5E_<8+@?fr?&U>c`U)CeK zV!X+*_7?g|6)F8;`I}BB%7SR~S+gzRahopkSRWj;$S!DXMB0Y5hMGQ zZ}p~G8AKocX+^GV@Gt0{UZhC;Umg_88m--IwJO8cs$}ViUE5?8KsOpEIF8}BpsPdk z@}}A#Eyg#=D4*b9c-O&LJau`su;F8is%w2&)Y0W{h2~}fSj(Rlopx@U7o~nxZ*5!t zsN1kgm%gDWq*E_!7qgsyPFV5E z4A1WWlZ(2v-;w@;me*5C>m!Gf&WUAC)UD(%$P*&Y*tcS@GLu=D{q*ZI5b$e<2R+;t zt+5+nU#BZl7iB*#Kfo7EINPP}Ew+aojroMKzvHXZZ28~sUZ%UvTA+;mRsZu$*`vTJ znmp`asg+kip1!4%q^6o>645hFs*>*3 zWaD0$Ma#mR7pFQ^b?Tzl#dp#K@u#mm>ArJQL%+QzZ#s2Vs;-=iVfubz$2AV7C~cnq zpaQJKJ1^9KKD@z!Rc7wgS}NS*_f5?Rcb&TJ7O^cwQaaMXm|*u!=YWVa!&ImL!yT%# z!XVT-8bs)1%%VHe<6iJC#$5Wb&7`fipyp{{f%hNuMl&+4>k3yXQr>hRurCbRDEO&P zmF%AEa^`BK*A(Idy=${)%&6Np^0mI~UJ=fW1}9FeAuorh{D8#m-k+yLejs$(mhA>B z_L;s@J77%M>Uf^v|L3Kqj7>eABElRr{5(tc)?0bqw=f7ZFCr$Qk=1Vjg z4E|fwg-LaY?(bB2cmnniB$`1XCzLu$`#KV#b6EwC{CSBy2rKlTui&NT(diUJx#0^!KTA0xd{)1+l7&8~k_b$;H7I@|Thmtsex*UdtIu zjOnfy(u9yak4@qe9`R_-E6UbLLKc1ZB>dl#eW^@-lEhr4p9=V(dKamPa=EFmK20fK zZyBRb_z;XF8%B2&N4&2eBrN>}r%Cl1Qm1wl{pIpH^6o6pkA(U6u;tG zjuF4&GX8tz7?ix}EyrYd@AdH=lM7Yv3KpcrYNZ;Mhlw&+u4#`)B6fv1ztWWMY%+e( zdvusKMJ1NcrW@By<0zv)Gps|I_Cm2oe4*7cvRS?3UHx0$Uy=X$^n!TQnY16z(I+7} ze6m`pgT8U*!|0;c)73tXbc2q!~;1&P3=buVN4T;kHTMFd5Q30J~h@!j!g zRe@XP80-ITtq0D0JaCNr)xv&#cL*IK_>k5S@#qh#&ylgTeH6`VyzaH(7=Pqny@%NS zFqLko{rT!{2h}Y`8mj-e7>*ZEbo-HvgEUfcCtVy2DFE^XL64bqUn)>ciX`s=>_4$O zTBnwH_G?pjI}tj$Q|JuMGleERogY|zCZaH-HV34Ux_?cde))nLX5$<~=_SOs1Qx})bX`mBJEbwmkH}aXgw_>F+gqd{6|3m+2h;A(oYs^d-OF`?* zAt|ZnoW#*J`svcY;JeiSCVPSQo!Q#Y0JOM(8>n53E#HHQ&X2FX=urFv)?1iUJOO}- zb$f`v4oB5F^1{-sw(rU3ry&~6uuuihQ|>kwC`3Hgr|PaK{xF6h0qa1hWN{+jVjgfW z_0_yH2_d!r!8 zv$?g?R~-o-507XM8dPXc4B*oNL|q%(V7cHm56##9#x%fs9fqYkT}l$hJoZB&`?_)e zVgV1%Q@ekVCuTu=0r?HmzjJW20DeHkg#~!4LDUXNgn7As5vAm8_nFUyduOMw|A$0ECY(wvO6dWmUwE$l{+WD zx$TqlO-iRb_rM3#hgg7epk z%Ie}Ig3qr?_TItSi$<&zVhi4&T}6DcS(GD6fs70R-^gFJU#=cSUu2Z0)MK5qT#VSK z^JhmV3K4awrQ&eJdMC~xATiXhf1>9yc(H!8pX zKN}O~W*f;kc1)iw>}Ydt(@jLFNIhCKZ|; z-Un#97Bm4DR*$C50!h-FJb7kLV;F#SA&O@Yrg`Z*PQ1#Nbb#2M@EgNsBW`x&c)!5c zjI3h>)NcbM|Iy{NPCRpk3DCMC$Kkz>o#$0RufokyQ$7GoLX+Esj@(t;(0PbhJ6KEj z3X21ySN+hshJnQ zgS@xZ@j|AI$t@e0`nN|acgeBGf%M`x0InHEZo}57kIN*1Oc$iI7^US@#7yklZlhR+ zIrHF+`Gq{CFuB7&pqcLuhscr~vz;`hk)Q-S!a$HmSgg8~FGBc7-~8J6WBQk;xfEN8 z7_=;=EFfS&3C2uQ^{7byf~m^~D`+jGDT}qsZSa8!t6b~(h-><-f|7-lgdp)$7{NX9 zQ7jPCHo>mL(1 z-^%?TRc{$qb^3h|)6F5Ir4gl+mhMJCx}-s*I~0&^X%I1x?ruaW2|Ltk^jK>wkfYJiG1v zpblBIf@C{JT_A=0V5$nv-7t|LV*dOv)8Ln3LjW4#TaW?B=4;e3a0OE&()YrtP?>LB zXrYN&m>i?iNym5ST8`~wq+NbE7!1=yJN5?x3})J>JR+O%d(m9ZgRcGrfq)OC;3YWx zM{WcH#o3q*E#Wb^Q{B9Q4+fR>Hre-9xbX2}jqqH(ha-Ib;`GHMZ` ztA7HtPI;8ey)cBdwo4A1WKz{9A=VhSD3$`M3;n2dV>So+BXA?nIYG*v|L%P@8B51-evF@luj1QebNu0J8n$t&a{{)K_ZpN^<& zV7F0G@SgvP;*P)2a`#?R
+ ZCode Desktop +

{t("zcodeDesktop.intro")}

+
{status?.connected ? t("zcodeDesktop.connected") : t("zcodeDesktop.notConnected")}
+
+ {t("zcodeDesktop.open")} + + {t("zcodeDesktop.download")} +
+ {runtime && {runtime}} +
+ {t("zcodeDesktop.location")} + + {status?.runtimes.map(path => )} +
+ + + {folders &&
+ {folders.current} + {folders.parent && } + {folders.folders.map(folder =>
+ + +
)} +
} + {(!status?.connected || changed) && } + {(problem || parentError) &&

{parentError || problem}

} + {tested &&

{t("zcodeDesktop.testPassed")}

} +
+ {onBack && } + {(!status?.connected || changed) && } + {status?.connected && } + {status?.connected && !changed && onConnected && } +
+ {status?.connected && !changed && <> +

{t("zcodeDesktop.testHint")}

+ + + } +
; +} diff --git a/gui/src/components/ZcodeUsageNotices.tsx b/gui/src/components/ZcodeUsageNotices.tsx new file mode 100644 index 0000000000..ac56a5ecce --- /dev/null +++ b/gui/src/components/ZcodeUsageNotices.tsx @@ -0,0 +1,39 @@ +import { useEffect, useState } from "react"; +import { useI18n, useT } from "../i18n/shared"; +import { browserTimeZone, formatZcodeLocalTime, zcodeUsageSchedule, ZCODE_CAMPAIGN_SOURCE, ZCODE_USAGE_SOURCE } from "../zcode-usage-schedule"; + +export default function ZcodeUsageNotices({ viaZcode = true }: { viaZcode?: boolean }) { + const t = useT(); + const { locale } = useI18n(); + const [now, setNow] = useState(Date.now); + const schedule = zcodeUsageSchedule(now); + const zone = browserTimeZone(); + const local = (timestamp: number) => formatZcodeLocalTime(timestamp, locale, zone); + useEffect(() => { + const update = () => setNow(Date.now()); + const interval = setInterval(update, 1000); + window.addEventListener("focus", update); + document.addEventListener("visibilitychange", update); + return () => { clearInterval(interval); window.removeEventListener("focus", update); document.removeEventListener("visibilitychange", update); }; + }, []); + return
+

{t("zcodeUsage.title")}

+

{t("zcodeUsage.zone", { zone })}

+
+

+ {t(schedule.peak ? "zcodeUsage.peak" : "zcodeUsage.offPeak", { until: local(schedule.rateChangesAt) })} +

+ {viaZcode && schedule.campaign && <> +

+ {t(schedule.flashActive ? "zcodeUsage.flashActive" : "zcodeUsage.flashNext", { + start: local(schedule.flashStart), end: local(schedule.flashEnd), + })} +

+

{t("zcodeUsage.eligibility")}

+ } +
+

{t("zcodeUsage.rules")}

+ {t("zcodeUsage.source")} + {viaZcode && <> · {t("zcodeUsage.campaignSource")}} +
; +} diff --git a/gui/src/components/provider-workspace/ProviderCurrentQuota.tsx b/gui/src/components/provider-workspace/ProviderCurrentQuota.tsx index 157ad4e8e4..b548c04e1f 100644 --- a/gui/src/components/provider-workspace/ProviderCurrentQuota.tsx +++ b/gui/src/components/provider-workspace/ProviderCurrentQuota.tsx @@ -1,3 +1,6 @@ +import ZcodeUsageNotices from "../ZcodeUsageNotices"; +import { zcodeUsageScope } from "../../zcode-usage-schedule"; +import type { WorkspaceItem } from "../../provider-workspace/catalog"; import { useState } from "react"; import { useT } from "../../i18n/shared"; import { IconRefresh } from "../../icons"; @@ -6,7 +9,8 @@ import { formatRelativeTime, relativeTimeLabelsFromT } from "../../provider-work import ProviderAccountQuota from "./ProviderAccountQuota"; import type { AccountQuotaReading } from "./types"; -export default function ProviderCurrentQuota({ report, reading, onRefreshQuota }: { +export default function ProviderCurrentQuota({ report, reading, onRefreshQuota, item }: { + item?: WorkspaceItem; report?: ProviderQuotaReportView; reading?: AccountQuotaReading; onRefreshQuota?: () => Promise; @@ -46,6 +50,7 @@ export default function ProviderCurrentQuota({ report, reading, onRefreshQuota } } + {item && zcodeUsageScope(item) && } {quota && !effective.quotaUnavailable && effective.quotaMode !== "unsupported" &&
{!rowOwnsReading && current?.source?.trim() &&
{t("pws.stats.source")}
{formatQuotaSourceLabel(current.source)}
diff --git a/gui/src/components/provider-workspace/ProviderOverview.tsx b/gui/src/components/provider-workspace/ProviderOverview.tsx index 60efdb3950..7ad644a91c 100644 --- a/gui/src/components/provider-workspace/ProviderOverview.tsx +++ b/gui/src/components/provider-workspace/ProviderOverview.tsx @@ -279,7 +279,7 @@ export default function ProviderOverview({ )} - +
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx index 33c5dcd2f0..6a2db87e02 100644 --- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx +++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx @@ -1,3 +1,5 @@ +import ZcodeUsageNotices from "../ZcodeUsageNotices"; +import { zcodeUsageScope } from "../../zcode-usage-schedule"; /** * ProviderOverviewDashboard — aggregate overview when no provider is selected. * Shows summary cards, attention list, per-provider rate limits (QuotaBars stacked), @@ -204,6 +206,7 @@ export default function ProviderOverviewDashboard({ aria-busy={quotasLoading || undefined} >

{t("pws.dashboard.rateLimits")}

+ {allItems.some(item => zcodeUsageScope(item)) && zcodeUsageScope(item) === "zcode")} />} {quotaProviders.length > 0 ? (
{quotaProviders.map(({ item, report }) => ( diff --git a/gui/src/components/provider-workspace/ProviderSettings.tsx b/gui/src/components/provider-workspace/ProviderSettings.tsx index 847fe7e712..91dea28914 100644 --- a/gui/src/components/provider-workspace/ProviderSettings.tsx +++ b/gui/src/components/provider-workspace/ProviderSettings.tsx @@ -7,6 +7,7 @@ * Parent should remount on provider change (`key={item.name}`) so choice-loading * state resets cleanly without sync setState-in-effect. */ +import ZcodeDesktopPane from "../ZcodeDesktopPane"; import { useEffect, useMemo, useRef, useState } from "react"; import { baseUrlForChoice, matchChoiceId, resolvedBaseUrlForChoice } from "../../base-url-choice"; import { readJsonIfOk } from "../../fetch-json"; @@ -326,6 +327,7 @@ export default function ProviderSettings({ return (
+ {item.adapter === "zcode" && apiBase !== undefined && }
)} - +
); } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 68363152b5..70a1e2e0fe 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -5,6 +5,46 @@ import type { TKey } from "./en"; * German i18n catalog, generated from en.ts. Must match the `TKey` set (compile-checked). */ export const de: Record = { + "zcodeUsage.title": "GLM Coding Plan · zeitabhängige Vorteile", + "zcodeUsage.zone": "Zeiten in {zone}. Referenz: Singapur (UTC+8).", + "zcodeUsage.peak": "Spitzenzeit: regulärer Modell-Credittarif bis {until}.", + "zcodeUsage.offPeak": "Nebenzeit: 50 % Rabatt auf den regulären Modell-Credittarif bis {until}.", + "zcodeUsage.flashActive": "Kostenloses GLM-5.3-Flash-Zeitfenster über ZCode aktiv: kein Kontingentverbrauch bis {end}, sofern die Bedingungen unten erfüllt sind.", + "zcodeUsage.flashNext": "Nächstes kostenloses GLM-5.3-Flash-Zeitfenster über ZCode: {start} – {end}.", + "zcodeUsage.eligibility": "Erfordert einen kostenpflichtigen GLM Coding Plan, ZCode ab 3.10 und Restkontingent in BEIDEN Limits (5 Stunden und Woche). Guthaben und Berechtigung werden hier nicht geprüft. GLM-5.3 und andere Agenten sind von diesem Nullverbrauch ausgeschlossen.", + "zcodeUsage.rules": "Spitzenzeit: Mo–Fr 14–18 Uhr Singapur; sonst Nebenzeit. Flash-Aktion: 3.–20. September 2026, täglich 23–9 Uhr Singapur. Der Hinweis endet vorsorglich am 20. September; eine Verlängerung der letzten Nacht ist nicht angegeben. Hinweise ändern keine Verbrauchs- oder Kostenberichte.", + "zcodeUsage.source": "Offizielle Tarife · geprüft am 11. September 2026", + "zcodeUsage.campaignSource": "Offizielle Aktionsbedingungen", + + "zcodeDesktop.intro": "Verbinden Sie die auf diesem Computer in ZCode Desktop eingerichteten Z.AI-Modelle ohne API-Schlüssel-Eingabe. OpenCodex startet die offizielle Laufzeit, keine vorhandene Desktop-Unterhaltung.", + "zcodeDesktop.connected": "Desktop verbunden · lokales Protokoll geprüft", + "zcodeDesktop.notConnected": "Desktop nicht verbunden", + "zcodeDesktop.open": "ZCode Desktop öffnen", + "zcodeDesktop.detect": "Erneut erkennen", + "zcodeDesktop.download": "ZCode herunterladen", + "zcodeDesktop.location": "Desktop-Installationsordner (erweitert)", + "zcodeDesktop.workspace": "Arbeitsordner", + "zcodeDesktop.browse": "Ordner durchsuchen", + "zcodeDesktop.up": "Übergeordneter Ordner", + "zcodeDesktop.select": "Auswählen", + "zcodeDesktop.consent": "Ich erlaube ZCode, Dateien in diesem Ordner zu ändern und Befehle auszuführen. Desktop-Einstellungen bleiben schreibgeschützt. Befehle haben Netzwerkzugriff. Nur eine vertrauenswürdige offizielle Installation verbinden.", + "zcodeDesktop.back": "Zurück", + "zcodeDesktop.connect": "Desktop verbinden", + "zcodeDesktop.disconnect": "Trennen", + "zcodeDesktop.use": "Diesen Anbieter verwenden", + "zcodeDesktop.testHint": "Optional: Eine kurze Anfrage über ZCode prüft den Modellzugriff und verbraucht Kontingent. Die Verbindung allein verbraucht keines.", + "zcodeDesktop.model": "Modell", + "zcodeDesktop.test": "Mit einer Anfrage testen", + "zcodeDesktop.testPassed": "ZCode hat erfolgreich geantwortet.", + "zcodeDesktop.missing": "Öffnen Sie ZCode Desktop auf dem Proxy-Computer und starten Sie die Erkennung erneut, oder wählen Sie den Installationsordner.", + "zcodeDesktop.sandboxMissing": "Installieren Sie bubblewrap mit dem Linux-Paketmanager für die Isolation und starten Sie die Erkennung erneut.", + "zcodeDesktop.nodeMissing": "Installieren Sie eine mit ZCode kompatible Node.js-Version und starten Sie die Erkennung erneut.", + "zcodeDesktop.platformUnsupported": "Die verwaltete Desktop-Verbindung unterstützt derzeit nur Linux. Andere Plattformen benötigen den erweiterten isolierten Starter.", + "zcodeDesktop.loginNeeded": "Melden Sie sich in ZCode Desktop an und richten Sie ein Z.AI-Modell ein. Verbinden Sie danach erneut.", + "zcodeDesktop.workspaceInvalid": "Wählen Sie einen vorhandenen Projektordner, nicht den Home-, System- oder Zugangsdatenordner.", + "zcodeDesktop.inferenceFailed": "Der ZCode-Test ist fehlgeschlagen. Prüfen Sie Anmeldung und Modellzugriff in Desktop. Keine direkte API wurde als Ersatz verwendet.", + "zcodeDesktop.failed": "Die offizielle Laufzeit konnte nicht verbunden werden. Prüfen Sie Desktop, den Ordner und die Isolationsvoraussetzungen.", + "models.pickerOrder.label": "Modellreihenfolge", "models.pickerOrder.default": "Standard", "models.pickerOrder.alphabetical": "A–Z nach Modell", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 89fc5ec0d7..2e7abb6fa9 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -6,6 +6,46 @@ * `{var}` are plain interpolations. */ export const en = { + "zcodeUsage.title": "GLM Coding Plan · time-based benefits", + "zcodeUsage.zone": "Times shown in {zone}. Schedule reference: Singapore (UTC+8).", + "zcodeUsage.peak": "Peak hours: standard model credit rate until {until}.", + "zcodeUsage.offPeak": "Off-peak: 50% off the standard model credit rate until {until}.", + "zcodeUsage.flashActive": "GLM-5.3-Flash free window active through ZCode: zero quota consumption until {end}, subject to eligibility below.", + "zcodeUsage.flashNext": "Next GLM-5.3-Flash free window through ZCode: {start} – {end}.", + "zcodeUsage.eligibility": "Requires a paid GLM Coding Plan, ZCode 3.10 or later, and remaining quota in BOTH the 5-hour and weekly limits. Balance and eligibility are not verified here. GLM-5.3 and other agents are not covered by this zero-quota benefit.", + "zcodeUsage.rules": "Peak: Mon–Fri 14:00–18:00 Singapore; all other hours are off-peak. Flash campaign: September 3–20, 2026, daily 23:00–09:00 Singapore. We conservatively stop the notice at the end of September 20; the final overnight extension is not specified. Advisories do not modify usage or cost reports.", + "zcodeUsage.source": "Official rates · verified September 11, 2026", + "zcodeUsage.campaignSource": "Official campaign conditions", + + "zcodeDesktop.intro": "Connect the Z.AI models configured in ZCode Desktop on this computer. No API key to paste. OpenCodex starts the official runtime; it does not attach to an existing Desktop conversation.", + "zcodeDesktop.connected": "Desktop connected · local protocol verified", + "zcodeDesktop.notConnected": "Desktop not connected", + "zcodeDesktop.open": "Open ZCode Desktop", + "zcodeDesktop.detect": "Detect again", + "zcodeDesktop.download": "Download ZCode", + "zcodeDesktop.location": "Desktop installation folder (advanced)", + "zcodeDesktop.workspace": "Workspace folder", + "zcodeDesktop.browse": "Browse folders", + "zcodeDesktop.up": "Parent folder", + "zcodeDesktop.select": "Select", + "zcodeDesktop.consent": "I allow ZCode to edit files and run commands in this folder. Desktop settings are read-only. Commands have network access. Only connect a trusted official ZCode installation.", + "zcodeDesktop.back": "Back", + "zcodeDesktop.connect": "Connect Desktop", + "zcodeDesktop.disconnect": "Disconnect", + "zcodeDesktop.use": "Use this provider", + "zcodeDesktop.testHint": "Optional: send one short request through ZCode to verify model access. This consumes account quota; connecting alone does not.", + "zcodeDesktop.model": "Model", + "zcodeDesktop.test": "Test with one request", + "zcodeDesktop.testPassed": "ZCode answered successfully.", + "zcodeDesktop.missing": "Open ZCode Desktop on the proxy computer, then detect again. You can also select its installed or extracted application folder.", + "zcodeDesktop.sandboxMissing": "Bubblewrap is required for isolation. Install bubblewrap using your Linux package manager, then detect again.", + "zcodeDesktop.nodeMissing": "Node.js is required by the ZCode runtime. Install a compatible Node.js version, then detect again.", + "zcodeDesktop.platformUnsupported": "Managed Desktop connection currently supports Linux only. Other platforms can use the advanced isolated-launcher setup.", + "zcodeDesktop.loginNeeded": "Sign in and configure a Z.AI model in ZCode Desktop, then detect and connect again.", + "zcodeDesktop.workspaceInvalid": "Choose an existing project folder, not your home, a system folder or a credentials/configuration directory.", + "zcodeDesktop.inferenceFailed": "ZCode did not complete the test. Check login and model access in Desktop, then reconnect. No direct API fallback was used.", + "zcodeDesktop.failed": "Could not connect to the official runtime. Check Desktop, the selected folder and the isolation prerequisites, then retry.", + "models.pickerOrder.label": "Picker order", "models.pickerOrder.default": "Default", "models.pickerOrder.alphabetical": "A–Z by model", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 4cf6818072..f72853f2e9 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -4,6 +4,46 @@ import type { TKey } from "./en"; * French i18n catalog. Must match the `TKey` set. */ export const fr: Record = { + "zcodeUsage.title": "GLM Coding Plan · avantages horaires", + "zcodeUsage.zone": "Heures affichées dans {zone}. Référence : Singapour (UTC+8).", + "zcodeUsage.peak": "Heures de pointe : tarif standard en crédits jusqu’à {until}.", + "zcodeUsage.offPeak": "Heures creuses : réduction de 50 % du tarif standard en crédits jusqu’à {until}.", + "zcodeUsage.flashActive": "Créneau gratuit GLM-5.3-Flash via ZCode actif : aucun quota consommé jusqu’à {end}, sous réserve des conditions ci-dessous.", + "zcodeUsage.flashNext": "Prochain créneau gratuit GLM-5.3-Flash via ZCode : {start} – {end}.", + "zcodeUsage.eligibility": "Nécessite un GLM Coding Plan payant, ZCode 3.10 ou ultérieur et du quota restant dans LES DEUX limites (5 heures et semaine). Solde et éligibilité non vérifiés ici. GLM-5.3 et les autres agents sont exclus de cet avantage sans consommation.", + "zcodeUsage.rules": "Pointe : lun–ven 14 h–18 h à Singapour ; heures creuses le reste du temps. Promotion Flash : du 3 au 20 septembre 2026, chaque jour de 23 h à 9 h à Singapour. Par prudence, l’avis cesse à la fin du 20 septembre ; la prolongation de la dernière nuit n’est pas précisée. Aucun rapport de consommation ou de coût n’est modifié.", + "zcodeUsage.source": "Tarifs officiels · vérifiés le 11 septembre 2026", + "zcodeUsage.campaignSource": "Conditions officielles de la promotion", + + "zcodeDesktop.intro": "Connectez les modèles Z.AI configurés dans ZCode Desktop sur cet ordinateur, sans coller de clé API. OpenCodex lance le runtime officiel, sans reprendre une conversation Desktop.", + "zcodeDesktop.connected": "Desktop connecté · protocole local vérifié", + "zcodeDesktop.notConnected": "Desktop non connecté", + "zcodeDesktop.open": "Ouvrir ZCode Desktop", + "zcodeDesktop.detect": "Détecter à nouveau", + "zcodeDesktop.download": "Télécharger ZCode", + "zcodeDesktop.location": "Dossier d’installation de Desktop (avancé)", + "zcodeDesktop.workspace": "Dossier de travail", + "zcodeDesktop.browse": "Parcourir les dossiers", + "zcodeDesktop.up": "Dossier parent", + "zcodeDesktop.select": "Sélectionner", + "zcodeDesktop.consent": "J’autorise ZCode à modifier les fichiers et exécuter des commandes dans ce dossier. Les paramètres Desktop restent en lecture seule. Les commandes ont accès au réseau. Utilisez uniquement une installation officielle de confiance.", + "zcodeDesktop.back": "Retour", + "zcodeDesktop.connect": "Connecter Desktop", + "zcodeDesktop.disconnect": "Déconnecter", + "zcodeDesktop.use": "Utiliser ce fournisseur", + "zcodeDesktop.testHint": "Facultatif : une courte requête via ZCode vérifie l’accès au modèle et consomme du quota. La connexion seule ne consomme pas de quota.", + "zcodeDesktop.model": "Modèle", + "zcodeDesktop.test": "Tester avec une requête", + "zcodeDesktop.testPassed": "ZCode a répondu correctement.", + "zcodeDesktop.missing": "Ouvrez ZCode Desktop sur l’ordinateur du proxy, puis relancez la détection, ou indiquez son dossier installé ou extrait.", + "zcodeDesktop.sandboxMissing": "Installez bubblewrap avec le gestionnaire de paquets Linux pour activer l’isolation, puis relancez la détection.", + "zcodeDesktop.nodeMissing": "Installez une version de Node.js compatible avec ZCode, puis relancez la détection.", + "zcodeDesktop.platformUnsupported": "La connexion Desktop gérée prend actuellement en charge Linux uniquement. Les autres plateformes nécessitent le lanceur isolé avancé.", + "zcodeDesktop.loginNeeded": "Connectez-vous et configurez un modèle Z.AI dans ZCode Desktop, puis reconnectez le fournisseur.", + "zcodeDesktop.workspaceInvalid": "Choisissez un dossier de projet existant, pas le dossier personnel, système ou des identifiants.", + "zcodeDesktop.inferenceFailed": "Le test ZCode a échoué. Vérifiez la connexion et l’accès au modèle dans Desktop, puis reconnectez. Aucun repli vers une API directe.", + "zcodeDesktop.failed": "Connexion au runtime officiel impossible. Vérifiez Desktop, le dossier et les prérequis d’isolation.", + "models.pickerOrder.label": "Ordre des modèles", "models.pickerOrder.default": "Par défaut", "models.pickerOrder.alphabetical": "A–Z par modèle", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index c77edbe2c7..b48c488712 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -4,6 +4,46 @@ import type { TKey } from "./en"; * Japanese i18n catalog; must match the `TKey` set (compile-checked). */ export const ja: Record = { + "zcodeUsage.title": "GLM Coding Plan · 時間帯別特典", + "zcodeUsage.zone": "表示時刻:{zone}。基準:シンガポール(UTC+8)。", + "zcodeUsage.peak": "ピーク時間:{until}まで通常のモデルクレジット料金。", + "zcodeUsage.offPeak": "オフピーク:{until}まで通常のモデルクレジット料金が50%割引。", + "zcodeUsage.flashActive": "ZCode経由のGLM-5.3-Flash無料時間帯が有効:以下の条件を満たす場合、{end}まで枠の消費なし。", + "zcodeUsage.flashNext": "次のZCode経由GLM-5.3-Flash無料時間帯:{start} – {end}。", + "zcodeUsage.eligibility": "有料GLM Coding Plan、ZCode 3.10以降、5時間枠と週間枠の両方に残量が必要です。残量・適用資格はここでは確認していません。GLM-5.3と他のエージェントは消費ゼロ特典の対象外です。", + "zcodeUsage.rules": "ピーク:シンガポール時間の月~金14~18時。それ以外はオフピーク。Flashキャンペーン:2026年9月3~20日、毎日23~翌9時(シンガポール)。最終夜の延長は明記されていないため、安全側で9月20日末に表示を終了します。使用量・料金レポートは変更しません。", + "zcodeUsage.source": "公式料金 · 2026年9月11日確認", + "zcodeUsage.campaignSource": "公式キャンペーン条件", + + "zcodeDesktop.intro": "このコンピューターの ZCode Desktop で設定した Z.AI モデルを接続します。API キーの貼り付けは不要です。公式ランタイムを起動し、既存の Desktop 会話には接続しません。", + "zcodeDesktop.connected": "Desktop 接続済み・ローカルプロトコル確認済み", + "zcodeDesktop.notConnected": "Desktop 未接続", + "zcodeDesktop.open": "ZCode Desktop を開く", + "zcodeDesktop.detect": "再検出", + "zcodeDesktop.download": "ZCode をダウンロード", + "zcodeDesktop.location": "Desktop インストール先(詳細)", + "zcodeDesktop.workspace": "作業フォルダー", + "zcodeDesktop.browse": "フォルダーを参照", + "zcodeDesktop.up": "親フォルダー", + "zcodeDesktop.select": "選択", + "zcodeDesktop.consent": "このフォルダーでのファイル編集とコマンド実行を ZCode に許可します。Desktop 設定は読み取り専用です。コマンドはネットワークにアクセスできます。信頼できる公式インストールのみ接続してください。", + "zcodeDesktop.back": "戻る", + "zcodeDesktop.connect": "Desktop に接続", + "zcodeDesktop.disconnect": "切断", + "zcodeDesktop.use": "このプロバイダーを使用", + "zcodeDesktop.testHint": "任意:ZCode 経由で短いリクエストを1回送信し、モデルへのアクセスを確認します。割り当てを消費します。接続だけでは消費しません。", + "zcodeDesktop.model": "モデル", + "zcodeDesktop.test": "1回のリクエストでテスト", + "zcodeDesktop.testPassed": "ZCode が正常に応答しました。", + "zcodeDesktop.missing": "プロキシのコンピューターで ZCode Desktop を開いて再検出するか、インストールまたは展開先を選択してください。", + "zcodeDesktop.sandboxMissing": "隔離には Bubblewrap が必要です。Linux のパッケージマネージャーでインストールして再検出してください。", + "zcodeDesktop.nodeMissing": "ZCode 対応の Node.js をインストールして再検出してください。", + "zcodeDesktop.platformUnsupported": "管理された Desktop 接続は現在 Linux のみ対応です。他の環境では詳細な隔離ランチャー設定を使用してください。", + "zcodeDesktop.loginNeeded": "ZCode Desktop にサインインして Z.AI モデルを設定し、再接続してください。", + "zcodeDesktop.workspaceInvalid": "ホーム、システム、認証情報のフォルダーではなく、既存のプロジェクトフォルダーを選択してください。", + "zcodeDesktop.inferenceFailed": "ZCode のテストが完了しませんでした。Desktop でログインとモデルの権限を確認して再接続してください。直接 API へのフォールバックはありません。", + "zcodeDesktop.failed": "公式ランタイムに接続できません。Desktop、フォルダー、隔離の前提条件を確認してください。", + "models.pickerOrder.label": "モデル選択順", "models.pickerOrder.default": "デフォルト", "models.pickerOrder.alphabetical": "モデル名のA–Z順", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 34d5ceae87..a9c7f94a19 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -4,6 +4,46 @@ import type { TKey } from "./en"; * Korean i18n catalog; must match the `TKey` set (compile-checked). */ export const ko: Record = { + "zcodeUsage.title": "GLM Coding Plan · 시간대별 혜택", + "zcodeUsage.zone": "{zone} 시간으로 표시합니다. 기준: 싱가포르(UTC+8).", + "zcodeUsage.peak": "피크 시간: {until}까지 표준 모델 크레딧 요금입니다.", + "zcodeUsage.offPeak": "오프피크: {until}까지 표준 모델 크레딧 요금 50% 할인.", + "zcodeUsage.flashActive": "ZCode 경유 GLM-5.3-Flash 무료 시간 활성: 아래 조건 충족 시 {end}까지 할당량 차감 없음.", + "zcodeUsage.flashNext": "다음 ZCode 경유 GLM-5.3-Flash 무료 시간: {start} – {end}.", + "zcodeUsage.eligibility": "유료 GLM Coding Plan, ZCode 3.10 이상, 5시간 및 주간 한도 모두에 잔여 할당량이 필요합니다. 잔액과 자격은 여기서 확인하지 않습니다. GLM-5.3 및 다른 에이전트에는 이 무차감 혜택이 적용되지 않습니다.", + "zcodeUsage.rules": "피크: 싱가포르 월–금 14–18시, 나머지는 오프피크. Flash 행사: 2026년 9월 3–20일, 매일 싱가포르 23–다음 날 9시. 마지막 밤 연장이 명시되지 않아 9월 20일 종료 시 안내를 보수적으로 중단합니다. 사용량 및 비용 보고서는 변경하지 않습니다.", + "zcodeUsage.source": "공식 요금 · 2026년 9월 11일 확인", + "zcodeUsage.campaignSource": "공식 행사 조건", + + "zcodeDesktop.intro": "이 컴퓨터의 ZCode Desktop에 설정된 Z.AI 모델을 연결합니다. API 키를 붙여 넣을 필요가 없습니다. 공식 런타임을 시작하며 기존 Desktop 대화에는 연결하지 않습니다.", + "zcodeDesktop.connected": "Desktop 연결됨 · 로컬 프로토콜 확인됨", + "zcodeDesktop.notConnected": "Desktop 연결 안 됨", + "zcodeDesktop.open": "ZCode Desktop 열기", + "zcodeDesktop.detect": "다시 감지", + "zcodeDesktop.download": "ZCode 다운로드", + "zcodeDesktop.location": "Desktop 설치 폴더(고급)", + "zcodeDesktop.workspace": "작업 폴더", + "zcodeDesktop.browse": "폴더 찾아보기", + "zcodeDesktop.up": "상위 폴더", + "zcodeDesktop.select": "선택", + "zcodeDesktop.consent": "ZCode가 이 폴더에서 파일을 수정하고 명령을 실행하도록 허용합니다. Desktop 설정은 읽기 전용입니다. 명령은 네트워크에 접근할 수 있습니다. 신뢰할 수 있는 공식 설치만 연결하세요.", + "zcodeDesktop.back": "뒤로", + "zcodeDesktop.connect": "Desktop 연결", + "zcodeDesktop.disconnect": "연결 해제", + "zcodeDesktop.use": "이 제공자 사용", + "zcodeDesktop.testHint": "선택 사항: ZCode를 통해 짧은 요청을 한 번 보내 모델 접근을 확인합니다. 계정 할당량을 사용합니다. 연결만으로는 사용하지 않습니다.", + "zcodeDesktop.model": "모델", + "zcodeDesktop.test": "요청 한 번으로 테스트", + "zcodeDesktop.testPassed": "ZCode가 정상적으로 응답했습니다.", + "zcodeDesktop.missing": "프록시 컴퓨터에서 ZCode Desktop을 열고 다시 감지하거나 설치 또는 압축 해제 폴더를 선택하세요.", + "zcodeDesktop.sandboxMissing": "격리를 위해 Linux 패키지 관리자로 bubblewrap을 설치한 뒤 다시 감지하세요.", + "zcodeDesktop.nodeMissing": "ZCode와 호환되는 Node.js를 설치한 뒤 다시 감지하세요.", + "zcodeDesktop.platformUnsupported": "관리형 Desktop 연결은 현재 Linux만 지원합니다. 다른 플랫폼에서는 고급 격리 실행기 설정을 사용하세요.", + "zcodeDesktop.loginNeeded": "ZCode Desktop에서 로그인하고 Z.AI 모델을 설정한 뒤 다시 연결하세요.", + "zcodeDesktop.workspaceInvalid": "홈, 시스템 또는 인증 정보 폴더가 아닌 기존 프로젝트 폴더를 선택하세요.", + "zcodeDesktop.inferenceFailed": "ZCode 테스트가 완료되지 않았습니다. Desktop 로그인과 모델 권한을 확인하고 다시 연결하세요. 직접 API로 대체하지 않았습니다.", + "zcodeDesktop.failed": "공식 런타임에 연결할 수 없습니다. Desktop, 폴더 및 격리 요구 사항을 확인하세요.", + "models.pickerOrder.label": "모델 선택 순서", "models.pickerOrder.default": "기본값", "models.pickerOrder.alphabetical": "모델 이름순", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 488f87d55b..478f777721 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -4,6 +4,46 @@ import type { TKey } from "./en"; * Russian i18n catalog; must match the `TKey` set (compile-checked). */ export const ru: Record = { + "zcodeUsage.title": "GLM Coding Plan · льготы по времени", + "zcodeUsage.zone": "Время в {zone}. Основа расписания: Сингапур (UTC+8).", + "zcodeUsage.peak": "Пиковые часы: стандартная ставка кредитов модели до {until}.", + "zcodeUsage.offPeak": "Вне пика: скидка 50% от стандартной ставки кредитов модели до {until}.", + "zcodeUsage.flashActive": "Бесплатное окно GLM-5.3-Flash через ZCode активно: квота не расходуется до {end} при выполнении условий ниже.", + "zcodeUsage.flashNext": "Следующее бесплатное окно GLM-5.3-Flash через ZCode: {start} – {end}.", + "zcodeUsage.eligibility": "Нужны платный GLM Coding Plan, ZCode 3.10 или новее и остаток ОБОИХ лимитов: 5-часового и недельного. Баланс и право на акцию здесь не проверяются. GLM-5.3 и другие агенты не получают эту льготу без расхода квоты.", + "zcodeUsage.rules": "Пик: пн–пт 14–18 по Сингапуру; остальное время — вне пика. Акция Flash: 3–20 сентября 2026, ежедневно 23–9 по Сингапуру. Уведомление консервативно прекращается в конце 20 сентября: продление последней ночи не уточнено. Отчёты о расходе и стоимости не меняются.", + "zcodeUsage.source": "Официальные тарифы · проверено 11 сентября 2026", + "zcodeUsage.campaignSource": "Официальные условия акции", + + "zcodeDesktop.intro": "Подключите модели Z.AI, настроенные в ZCode Desktop на этом компьютере, без ввода API-ключа. Запускается официальный runtime, а не существующий диалог Desktop.", + "zcodeDesktop.connected": "Desktop подключён · локальный протокол проверен", + "zcodeDesktop.notConnected": "Desktop не подключён", + "zcodeDesktop.open": "Открыть ZCode Desktop", + "zcodeDesktop.detect": "Найти снова", + "zcodeDesktop.download": "Скачать ZCode", + "zcodeDesktop.location": "Папка установки Desktop (дополнительно)", + "zcodeDesktop.workspace": "Рабочая папка", + "zcodeDesktop.browse": "Обзор папок", + "zcodeDesktop.up": "Родительская папка", + "zcodeDesktop.select": "Выбрать", + "zcodeDesktop.consent": "Разрешаю ZCode изменять файлы и выполнять команды в этой папке. Настройки Desktop доступны только для чтения. Команды имеют доступ к сети. Подключайте только доверенную официальную установку.", + "zcodeDesktop.back": "Назад", + "zcodeDesktop.connect": "Подключить Desktop", + "zcodeDesktop.disconnect": "Отключить", + "zcodeDesktop.use": "Использовать провайдера", + "zcodeDesktop.testHint": "Необязательно: один короткий запрос через ZCode проверит доступ к модели и потратит квоту. Само подключение квоту не расходует.", + "zcodeDesktop.model": "Модель", + "zcodeDesktop.test": "Проверить одним запросом", + "zcodeDesktop.testPassed": "ZCode успешно ответил.", + "zcodeDesktop.missing": "Откройте ZCode Desktop на компьютере прокси и повторите поиск либо выберите папку установки или распаковки.", + "zcodeDesktop.sandboxMissing": "Для изоляции установите bubblewrap через менеджер пакетов Linux и повторите поиск.", + "zcodeDesktop.nodeMissing": "Установите совместимую с ZCode версию Node.js и повторите поиск.", + "zcodeDesktop.platformUnsupported": "Управляемое подключение Desktop пока поддерживает только Linux. Для других платформ используйте расширенную настройку изолированного запуска.", + "zcodeDesktop.loginNeeded": "Войдите и настройте модель Z.AI в ZCode Desktop, затем подключитесь снова.", + "zcodeDesktop.workspaceInvalid": "Выберите существующую папку проекта, а не домашнюю, системную или папку с учётными данными.", + "zcodeDesktop.inferenceFailed": "ZCode не завершил проверку. Проверьте вход и доступ к модели в Desktop. Прямая API-подмена не использовалась.", + "zcodeDesktop.failed": "Не удалось подключить официальный runtime. Проверьте Desktop, папку и требования изоляции.", + "models.pickerOrder.label": "Порядок моделей", "models.pickerOrder.default": "По умолчанию", "models.pickerOrder.alphabetical": "По имени A–Z", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 2e46e2792f..ad3ffd471c 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -5,6 +5,46 @@ import type { TKey } from "./en"; * Turkish i18n catalog. Must match the `TKey` set (compile-checked). */ export const tr: Record = { + "zcodeUsage.title": "GLM Coding Plan · saat bazlı avantajlar", + "zcodeUsage.zone": "Saatler {zone} diliminde gösterilir. Referans: Singapur (UTC+8).", + "zcodeUsage.peak": "Yoğun saatler: {until} tarihine kadar standart model kredi tarifesi.", + "zcodeUsage.offPeak": "Yoğun olmayan saatler: {until} tarihine kadar standart model kredi tarifesinde %50 indirim.", + "zcodeUsage.flashActive": "ZCode üzerinden GLM-5.3-Flash ücretsiz zaman aralığı etkin: aşağıdaki koşullarla {end} tarihine kadar kota tüketimi yok.", + "zcodeUsage.flashNext": "ZCode üzerinden sonraki GLM-5.3-Flash ücretsiz aralığı: {start} – {end}.", + "zcodeUsage.eligibility": "Ücretli GLM Coding Plan, ZCode 3.10 veya üstü ve HEM 5 saatlik HEM haftalık limitte kalan kota gerektirir. Bakiye ve uygunluk burada doğrulanmaz. GLM-5.3 ve diğer ajanlar bu sıfır kota avantajına dahil değildir.", + "zcodeUsage.rules": "Yoğun: Singapur saatine göre Pzt–Cum 14–18; diğer tüm saatler yoğun değildir. Flash kampanyası: 3–20 Eylül 2026, her gün Singapur 23–ertesi gün 9. Son gece uzatması belirtilmediğinden bildirim tedbiren 20 Eylül sonunda durur. Kullanım veya maliyet raporlarını değiştirmez.", + "zcodeUsage.source": "Resmî tarifeler · 11 Eylül 2026 tarihinde doğrulandı", + "zcodeUsage.campaignSource": "Resmî kampanya koşulları", + + "zcodeDesktop.intro": "Bu bilgisayardaki ZCode Desktop içinde yapılandırılmış Z.AI modellerini API anahtarı yapıştırmadan bağlayın. Resmî çalışma zamanı başlatılır; mevcut Desktop konuşmasına bağlanılmaz.", + "zcodeDesktop.connected": "Desktop bağlı · yerel protokol doğrulandı", + "zcodeDesktop.notConnected": "Desktop bağlı değil", + "zcodeDesktop.open": "ZCode Desktop aç", + "zcodeDesktop.detect": "Yeniden algıla", + "zcodeDesktop.download": "ZCode indir", + "zcodeDesktop.location": "Desktop kurulum klasörü (gelişmiş)", + "zcodeDesktop.workspace": "Çalışma klasörü", + "zcodeDesktop.browse": "Klasörlere göz at", + "zcodeDesktop.up": "Üst klasör", + "zcodeDesktop.select": "Seç", + "zcodeDesktop.consent": "ZCode’un bu klasörde dosyaları düzenlemesine ve komut çalıştırmasına izin veriyorum. Desktop ayarları salt okunurdur. Komutlar ağa erişebilir. Yalnızca güvenilir resmî kurulumu bağlayın.", + "zcodeDesktop.back": "Geri", + "zcodeDesktop.connect": "Desktop bağla", + "zcodeDesktop.disconnect": "Bağlantıyı kes", + "zcodeDesktop.use": "Bu sağlayıcıyı kullan", + "zcodeDesktop.testHint": "İsteğe bağlı: ZCode üzerinden kısa bir istek model erişimini doğrular ve hesap kotasını kullanır. Yalnızca bağlantı kurmak kota kullanmaz.", + "zcodeDesktop.model": "Model", + "zcodeDesktop.test": "Bir istekle test et", + "zcodeDesktop.testPassed": "ZCode başarıyla yanıt verdi.", + "zcodeDesktop.missing": "Proxy bilgisayarında ZCode Desktop açıp yeniden algılayın veya kurulum ya da çıkarılmış uygulama klasörünü seçin.", + "zcodeDesktop.sandboxMissing": "Yalıtım için Linux paket yöneticisiyle bubblewrap kurup yeniden algılayın.", + "zcodeDesktop.nodeMissing": "ZCode ile uyumlu Node.js sürümünü kurup yeniden algılayın.", + "zcodeDesktop.platformUnsupported": "Yönetilen Desktop bağlantısı şu anda yalnızca Linux destekler. Diğer platformlarda gelişmiş yalıtılmış başlatıcıyı kullanın.", + "zcodeDesktop.loginNeeded": "ZCode Desktop içinde oturum açıp bir Z.AI modeli yapılandırın, ardından yeniden bağlanın.", + "zcodeDesktop.workspaceInvalid": "Ev, sistem veya kimlik bilgileri klasörü yerine mevcut bir proje klasörü seçin.", + "zcodeDesktop.inferenceFailed": "ZCode testi tamamlayamadı. Desktop oturumunu ve model erişimini kontrol edip yeniden bağlanın. Doğrudan API yedeği kullanılmadı.", + "zcodeDesktop.failed": "Resmî çalışma zamanına bağlanılamadı. Desktop, klasör ve yalıtım gereksinimlerini kontrol edin.", + "models.pickerOrder.label": "Model sırası", "models.pickerOrder.default": "Varsayılan", "models.pickerOrder.alphabetical": "Model adına göre A–Z", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index b9a26da41c..f280b74d1c 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2,6 +2,46 @@ import type { TKey } from "./en"; /** Traditional Chinese (Taiwan) UI strings — keys must match `en.ts` 1:1. */ export const zhTW: Record = { + "zcodeUsage.title": "GLM Coding Plan · 時段優惠", + "zcodeUsage.zone": "時間顯示為 {zone}。時段基準:新加坡(UTC+8)。", + "zcodeUsage.peak": "尖峰時段:截至 {until} 按標準模型點數費率計費。", + "zcodeUsage.offPeak": "離峰時段:截至 {until},標準模型點數費率優惠 50%。", + "zcodeUsage.flashActive": "透過 ZCode 使用 GLM-5.3-Flash 的免費時段已生效:符合以下條件時,截至 {end} 不消耗額度。", + "zcodeUsage.flashNext": "下一個透過 ZCode 使用 GLM-5.3-Flash 的免費時段:{start} – {end}。", + "zcodeUsage.eligibility": "需要付費 GLM Coding Plan、ZCode 3.10 或更新版本,且 5 小時和每週額度皆須有剩餘。此處未驗證餘額及資格。GLM-5.3 和其他 Agent 不享有此零額度消耗優惠。", + "zcodeUsage.rules": "尖峰:新加坡時間週一至週五 14:00–18:00,其餘皆為離峰。Flash 活動:2026 年 9 月 3–20 日,每天新加坡時間 23:00–翌日 09:00。末夜是否延長未明確說明,因此提示保守地於 9 月 20 日結束時停止。不修改用量或費用報告。", + "zcodeUsage.source": "官方費率 · 2026 年 9 月 11 日驗證", + "zcodeUsage.campaignSource": "官方活動條件", + + "zcodeDesktop.intro": "連接此電腦上 ZCode Desktop 中設定的 Z.AI 模型,無須貼上 API 金鑰。OpenCodex 啟動官方執行環境,不接管現有 Desktop 對話。", + "zcodeDesktop.connected": "Desktop 已連接 · 本機協定已驗證", + "zcodeDesktop.notConnected": "Desktop 未連接", + "zcodeDesktop.open": "開啟 ZCode Desktop", + "zcodeDesktop.detect": "重新偵測", + "zcodeDesktop.download": "下載 ZCode", + "zcodeDesktop.location": "Desktop 安裝目錄(進階)", + "zcodeDesktop.workspace": "工作目錄", + "zcodeDesktop.browse": "瀏覽目錄", + "zcodeDesktop.up": "上層目錄", + "zcodeDesktop.select": "選取", + "zcodeDesktop.consent": "我允許 ZCode 在此目錄修改檔案並執行命令。Desktop 設定維持唯讀。命令可存取網路。請僅連接可信任的官方 ZCode 安裝。", + "zcodeDesktop.back": "返回", + "zcodeDesktop.connect": "連接 Desktop", + "zcodeDesktop.disconnect": "中斷連接", + "zcodeDesktop.use": "使用此供應商", + "zcodeDesktop.testHint": "選用:透過 ZCode 傳送一次簡短請求以驗證模型存取權限。這會消耗帳戶配額;僅連接不會消耗。", + "zcodeDesktop.model": "模型", + "zcodeDesktop.test": "傳送一次測試請求", + "zcodeDesktop.testPassed": "ZCode 已成功回應。", + "zcodeDesktop.missing": "請在代理所在電腦開啟 ZCode Desktop 後重新偵測,或選取安裝或解壓縮目錄。", + "zcodeDesktop.sandboxMissing": "隔離需要 Bubblewrap。請透過 Linux 套件管理員安裝 bubblewrap 後重新偵測。", + "zcodeDesktop.nodeMissing": "請安裝與 ZCode 相容的 Node.js 版本後重新偵測。", + "zcodeDesktop.platformUnsupported": "受管理的 Desktop 連接目前僅支援 Linux。其他平台可使用進階隔離啟動器設定。", + "zcodeDesktop.loginNeeded": "請在 ZCode Desktop 登入並設定 Z.AI 模型,然後重新連接。", + "zcodeDesktop.workspaceInvalid": "請選取現有專案目錄,而非家目錄、系統目錄或憑證設定目錄。", + "zcodeDesktop.inferenceFailed": "ZCode 未完成測試。請在 Desktop 檢查登入狀態及模型權限後重新連接。未使用直接 API 備援。", + "zcodeDesktop.failed": "無法連接官方執行環境。請檢查 Desktop、目錄與隔離依賴後重試。", + "models.pickerOrder.label": "模型選擇順序", "models.pickerOrder.default": "預設", "models.pickerOrder.alphabetical": "依模型名稱 A–Z", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 46866680b0..a190049d9b 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -4,6 +4,46 @@ import type { TKey } from "./en"; * Chinese i18n catalog; must match the `TKey` set (compile-checked). */ export const zh: Record = { + "zcodeUsage.title": "GLM Coding Plan · 时段优惠", + "zcodeUsage.zone": "时间显示为 {zone}。时段基准:新加坡(UTC+8)。", + "zcodeUsage.peak": "高峰时段:截至 {until} 按标准模型积分费率计费。", + "zcodeUsage.offPeak": "非高峰时段:截至 {until},标准模型积分费率优惠 50%。", + "zcodeUsage.flashActive": "通过 ZCode 使用 GLM-5.3-Flash 的免费时段已生效:符合以下条件时,截至 {end} 不消耗额度。", + "zcodeUsage.flashNext": "下一个通过 ZCode 使用 GLM-5.3-Flash 的免费时段:{start} – {end}。", + "zcodeUsage.eligibility": "需要付费 GLM Coding Plan、ZCode 3.10 或更高版本,且 5 小时和每周额度都必须有剩余。此处未核验余额及资格。GLM-5.3 和其他 Agent 不享受此零额度消耗优惠。", + "zcodeUsage.rules": "高峰:新加坡时间周一至周五 14:00–18:00,其余均为非高峰。Flash 活动:2026 年 9 月 3–20 日,每天新加坡时间 23:00–次日 09:00。末夜是否延长未明确说明,因此提示保守地于 9 月 20 日结束时停止。不修改用量或费用报告。", + "zcodeUsage.source": "官方费率 · 2026 年 9 月 11 日核验", + "zcodeUsage.campaignSource": "官方活动条件", + + "zcodeDesktop.intro": "连接此计算机上 ZCode Desktop 中配置的 Z.AI 模型,无需粘贴 API 密钥。OpenCodex 启动官方运行时,不接管现有 Desktop 对话。", + "zcodeDesktop.connected": "Desktop 已连接 · 本地协议已验证", + "zcodeDesktop.notConnected": "Desktop 未连接", + "zcodeDesktop.open": "打开 ZCode Desktop", + "zcodeDesktop.detect": "重新检测", + "zcodeDesktop.download": "下载 ZCode", + "zcodeDesktop.location": "Desktop 安装目录(高级)", + "zcodeDesktop.workspace": "工作目录", + "zcodeDesktop.browse": "浏览目录", + "zcodeDesktop.up": "上级目录", + "zcodeDesktop.select": "选择", + "zcodeDesktop.consent": "我允许 ZCode 在此目录中修改文件并执行命令。Desktop 配置保持只读。命令可以访问网络。请仅连接可信的官方 ZCode 安装。", + "zcodeDesktop.back": "返回", + "zcodeDesktop.connect": "连接 Desktop", + "zcodeDesktop.disconnect": "断开连接", + "zcodeDesktop.use": "使用此提供商", + "zcodeDesktop.testHint": "可选:通过 ZCode 发送一次简短请求以验证模型访问权限。这会消耗账户配额;仅连接不会消耗。", + "zcodeDesktop.model": "模型", + "zcodeDesktop.test": "发送一次测试请求", + "zcodeDesktop.testPassed": "ZCode 已成功响应。", + "zcodeDesktop.missing": "请在代理所在计算机上打开 ZCode Desktop 后重新检测,或选择其安装或解压目录。", + "zcodeDesktop.sandboxMissing": "隔离需要 Bubblewrap。请通过 Linux 包管理器安装 bubblewrap 后重新检测。", + "zcodeDesktop.nodeMissing": "请安装与 ZCode 兼容的 Node.js 版本后重新检测。", + "zcodeDesktop.platformUnsupported": "托管 Desktop 连接目前仅支持 Linux。其他平台可以使用高级隔离启动器配置。", + "zcodeDesktop.loginNeeded": "请在 ZCode Desktop 中登录并配置 Z.AI 模型,然后重新连接。", + "zcodeDesktop.workspaceInvalid": "请选择现有项目目录,而非主目录、系统目录或凭据配置目录。", + "zcodeDesktop.inferenceFailed": "ZCode 未完成测试。请在 Desktop 中检查登录状态及模型权限后重新连接。未使用直接 API 回退。", + "zcodeDesktop.failed": "无法连接官方运行时。请检查 Desktop、所选目录和隔离依赖后重试。", + "models.pickerOrder.label": "模型选择顺序", "models.pickerOrder.default": "默认", "models.pickerOrder.alphabetical": "按模型名 A–Z", diff --git a/gui/src/provider-payload.ts b/gui/src/provider-payload.ts index bc42c9e941..4f4686f9db 100644 --- a/gui/src/provider-payload.ts +++ b/gui/src/provider-payload.ts @@ -75,7 +75,7 @@ export interface ProviderPayload { apiKey?: string; apiKeyTransport?: "x-api-key" | "bearer"; defaultModel?: string; - authMode?: "key" | "forward" | "oauth"; + authMode?: "key" | "forward" | "oauth" | "local"; codexAccountMode?: "pool" | "direct"; allowPrivateNetwork?: boolean; } @@ -89,7 +89,7 @@ export function buildProviderPayload(form: ProviderPayloadForm): ProviderPayload if (form.responsesPath?.trim()) { provider.responsesPath = form.responsesPath.trim(); } - if (form.authMode === "key" || form.authMode === "forward") { + if (form.authMode === "key" || form.authMode === "forward" || (form.authMode === "local" && form.adapter.trim() === "zcode")) { provider.authMode = form.authMode; } if (form.authMode === "key" && form.apiKey.trim()) { diff --git a/gui/src/zcode-usage-schedule.ts b/gui/src/zcode-usage-schedule.ts new file mode 100644 index 0000000000..334f66cfc9 --- /dev/null +++ b/gui/src/zcode-usage-schedule.ts @@ -0,0 +1,48 @@ +/** Advisory only: never changes routing, measured usage or billing estimates. + * Official rules verified 2026-09-11; Singapore has a fixed UTC+8 offset. + */ +export const ZCODE_USAGE_SOURCE = "https://docs.z.ai/devpack/overview"; +export const ZCODE_CAMPAIGN_SOURCE = "https://docs.z.ai/devpack/notice/event-glm-5.3-flash"; +const HOUR = 3_600_000; +const DAY = 24 * HOUR; +const OFFSET = 8 * HOUR; +export const CAMPAIGN_START = Date.parse("2026-09-03T00:00:00+08:00"); +// The notice does not explicitly extend the final night into September 21. +// Conservatively stop advertising free usage at the end of September 20 SGT. +export const CAMPAIGN_END = Date.parse("2026-09-21T00:00:00+08:00"); + +export function zcodeUsageScope(provider: { adapter: string; baseUrl: string; disabled?: boolean }): "zcode" | "coding-plan" | undefined { + if (provider.disabled) return; + if (provider.adapter === "zcode") return "zcode"; + try { + const url = new URL(provider.baseUrl); + if (url.protocol === "https:" && url.hostname === "api.z.ai" && !url.username && !url.password + && /^\/api\/coding\/paas\/v4\/?$/.test(url.pathname)) return "coding-plan"; + } catch { /* Non-HTTP local adapters are not Coding Plan endpoints. */ } +} + +export function zcodeUsageSchedule(now: number) { + if (!Number.isFinite(now)) throw new RangeError("Invalid schedule timestamp"); + const shifted = new Date(now + OFFSET); + const dayStart = Math.floor((now + OFFSET) / DAY) * DAY - OFFSET; + const weekday = shifted.getUTCDay(); + const hour = shifted.getUTCHours(); + const peak = weekday >= 1 && weekday <= 5 && hour >= 14 && hour < 18; + let nextPeakStart = dayStart + 14 * HOUR; + while (nextPeakStart <= now || [0, 6].includes(new Date(nextPeakStart + OFFSET).getUTCDay())) nextPeakStart += DAY; + const rateChangesAt = peak ? dayStart + 18 * HOUR : nextPeakStart; + let flashStart = hour < 9 ? dayStart - HOUR : dayStart + 23 * HOUR; + flashStart = Math.max(flashStart, CAMPAIGN_START); + const flashEnd = Math.min(hour < 9 ? dayStart + 9 * HOUR : dayStart + DAY + 9 * HOUR, CAMPAIGN_END); + const campaign = now >= CAMPAIGN_START && now < CAMPAIGN_END; + return { peak, rateChangesAt, campaign, flashActive: campaign && now >= flashStart && now < flashEnd, + flashStart, flashEnd }; +} + +export function formatZcodeLocalTime(timestamp: number, locale: string, timeZone: string): string { + return new Intl.DateTimeFormat(locale, { timeZone, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", timeZoneName: "short" }).format(timestamp); +} + +export function browserTimeZone(): string { + return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; +} diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx new file mode 100644 index 0000000000..555a09fa40 --- /dev/null +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -0,0 +1,95 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import { LanguageProvider } from "../src/i18n/provider"; +import ZcodeDesktopPane from "../src/components/ZcodeDesktopPane"; + + + +const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; +let previous: Record<(typeof globals)[number], unknown>; +let win: Window; +let host: HTMLElement; +let root: Root | null = null; +let originalFetch: typeof globalThis.fetch; +let closeCalls: number; +let requests: Array<{ path: string; body?: Record }>; + +beforeEach(() => { + previous = Object.fromEntries(globals.map((k) => [k, Reflect.get(globalThis, k)])) as typeof previous; + originalFetch = globalThis.fetch; + win = new Window({ url: "http://localhost/" }); + Object.defineProperty(win.navigator, "language", { configurable: true, value: "en-US" }); + Object.defineProperties(globalThis, { + document: { configurable: true, value: win.document }, + window: { configurable: true, value: win }, + navigator: { configurable: true, value: win.navigator }, + localStorage: { configurable: true, value: win.localStorage }, + }); + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + + closeCalls = 0; requests = []; + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: async (input: RequestInfo | URL, options?: RequestInit) => { + const url = new URL(String(input), "http://localhost"); + requests.push({ path: url.pathname, body: options?.body ? JSON.parse(String(options.body)) : undefined }); + if (url.pathname.endsWith("/test")) return Response.json({ ok: true }); + return Response.json({ connected: url.pathname.endsWith("/connect"), runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [{ id: "builtin:zai/model", label: "Model" }] }); + }, + }); + + host = win.document.createElement("div") as unknown as HTMLElement; + win.document.body.appendChild(host as never); +}); + +afterEach(async () => { + if (root) { + const current = root; + await act(async () => { current.unmount(); }); + root = null; + } + for (const key of globals) { + Object.defineProperty(globalThis, key, { configurable: true, value: previous[key] }); + } + Object.defineProperty(globalThis, "fetch", { configurable: true, value: originalFetch }); + await win.happyDOM?.close?.(); +}); + +async function mountPane() { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( { closeCalls++; }} />); + }); + await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); }); +} +function button(text: string): HTMLButtonElement { + const found = [...host.querySelectorAll("button")].find(b => b.textContent === text); + expect(found).toBeTruthy(); return found!; +} +async function click(element: HTMLElement) { + await act(async () => { element.dispatchEvent(new win.MouseEvent("click", { bubbles: true })); }); +} +test("Desktop connect requires explicit consent and does not automatically spend model quota", async () => { + await mountPane(); + expect(button("Connect Desktop").disabled).toBe(true); + const consent = host.querySelector('input[type="checkbox"]')!; + await click(consent); + expect(button("Connect Desktop").disabled).toBe(false); + await click(button("Connect Desktop")); + expect(requests.find(r => r.path.endsWith("/connect"))?.body).toEqual({ runtime: "/installed/ZCode", workspace: "/project", consent: true }); + expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); + expect(closeCalls).toBe(0); + await click(button("Use this provider")); + expect(closeCalls).toBe(1); +}); +test("one-request test is a separate explicit quota-spending action", async () => { + await mountPane(); await click(host.querySelector('input[type="checkbox"]')!); + await click(button("Connect Desktop")); + expect(host.textContent).toContain("consumes account quota"); + await click(button("Test with one request")); + expect(requests.find(r => r.path.endsWith("/test"))?.body).toEqual({ model: "builtin:zai/model", consent: true }); + expect(host.textContent).toContain("ZCode answered successfully."); +}); diff --git a/gui/tests/zcode-usage-schedule.test.ts b/gui/tests/zcode-usage-schedule.test.ts new file mode 100644 index 0000000000..a8fda89f12 --- /dev/null +++ b/gui/tests/zcode-usage-schedule.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { createElement } from "react"; +import { buildProviderPayload } from "../src/provider-payload"; +import ZcodeUsageNotices from "../src/components/ZcodeUsageNotices"; +import { LanguageProvider } from "../src/i18n/provider"; +import { CAMPAIGN_END, CAMPAIGN_START, formatZcodeLocalTime, zcodeUsageSchedule, zcodeUsageScope } from "../src/zcode-usage-schedule"; +const at = (value: string) => zcodeUsageSchedule(Date.parse(value + "+08:00")); + +describe("ZCode official schedule advisories", () => { + test("Desktop provider creation preserves local auth instead of requesting an API key", () => { + expect(buildProviderPayload({ name: "zcode", adapter: "zcode", baseUrl: "https://zcode.z.ai", authMode: "local", apiKey: "", defaultModel: "" })).toEqual({ adapter: "zcode", baseUrl: "https://zcode.z.ai", authMode: "local" }); + }); + test("peak boundaries are half-open Singapore weekdays", () => { + expect(at("2026-09-11T13:59:59").peak).toBe(false); + expect(at("2026-09-11T14:00:00").peak).toBe(true); + expect(at("2026-09-11T17:59:59").peak).toBe(true); + expect(at("2026-09-11T18:00:00").peak).toBe(false); + expect(at("2026-09-12T15:00:00").peak).toBe(false); + expect(at("2026-09-13T15:00:00").peak).toBe(false); + expect(at("2026-09-11T18:00:00").rateChangesAt).toBe(Date.parse("2026-09-14T14:00:00+08:00")); + }); + test("daily Flash window crosses midnight and includes weekends", () => { + expect(at("2026-09-11T22:59:59").flashActive).toBe(false); + expect(at("2026-09-11T23:00:00").flashActive).toBe(true); + expect(at("2026-09-12T08:59:59").flashActive).toBe(true); + expect(at("2026-09-12T09:00:00").flashActive).toBe(false); + expect(at("2026-09-12T09:00:00").flashStart).toBe(Date.parse("2026-09-12T23:00:00+08:00")); + }); + test("campaign expires without extending the undocumented final night", () => { + expect(zcodeUsageSchedule(CAMPAIGN_START - 1).campaign).toBe(false); + expect(zcodeUsageSchedule(CAMPAIGN_START).campaign).toBe(true); + expect(at("2026-09-20T23:30:00").flashEnd).toBe(CAMPAIGN_END); + expect(zcodeUsageSchedule(CAMPAIGN_END - 1).flashActive).toBe(true); + expect(zcodeUsageSchedule(CAMPAIGN_END).flashActive).toBe(false); + expect(at("2027-09-11T23:30:00").campaign).toBe(false); + expect(() => zcodeUsageSchedule(NaN)).toThrow(); + }); + test("local rendering converts date rollover and DST without changing eligibility", () => { + const instant = Date.parse("2026-09-11T23:00:00+08:00"); + expect(formatZcodeLocalTime(instant, "en-GB", "America/Argentina/Buenos_Aires")).toContain("12:00"); + expect(formatZcodeLocalTime(instant, "en-GB", "America/Los_Angeles")).toContain("08:00"); + expect(formatZcodeLocalTime(Date.parse("2026-11-01T09:00:00Z"), "en-GB", "America/Los_Angeles")).toContain("01:00"); + expect(formatZcodeLocalTime(Date.parse("2026-09-11T09:00:00+08:00"), "en-GB", "America/Argentina/Buenos_Aires")).toContain("10 Sept"); + }); + test("never treats provider names, general API or lookalike endpoints as ZCode", () => { + expect(zcodeUsageScope({ adapter: "zcode", baseUrl: "" })).toBe("zcode"); + expect(zcodeUsageScope({ adapter: "zcode", baseUrl: "", disabled: true })).toBeUndefined(); + expect(zcodeUsageScope({ adapter: "openai", baseUrl: "https://api.z.ai/api/coding/paas/v4" })).toBe("coding-plan"); + for (const baseUrl of ["https://api.z.ai/api/paas/v4", "https://api.z.ai.evil/api/coding/paas/v4", "http://api.z.ai/api/coding/paas/v4", "https://api.z.ai@evil/api/coding/paas/v4"]) + expect(zcodeUsageScope({ adapter: "openai", baseUrl })).toBeUndefined(); + }); + test("free window is conditional, not a verified balance or billing result", () => { + const original = Date.now; + Date.now = () => Date.parse("2026-09-11T23:30:00+08:00"); + try { + const render = (viaZcode: boolean) => renderToStaticMarkup(createElement(LanguageProvider, null, createElement(ZcodeUsageNotices, { viaZcode }))); + expect(render(true)).toContain("free window active"); + expect(render(true)).toContain("Balance and eligibility are not verified"); + expect(render(false)).not.toContain("free window active"); + Date.now = () => CAMPAIGN_END; + expect(render(true)).not.toContain("free window active"); + expect(render(true)).not.toContain("Next GLM-5.3-Flash free window"); + } finally { Date.now = original; } + }); +}); diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index ad80d1ac98..ec651af43b 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1336,15 +1336,17 @@ "yaml-fragment-source.test.ts": "config", "z-fulfill.test.ts": "images", "z-handler-activation.test.ts": "images", + "zcode-adapter.test.ts": "providers", "zcode-client.test.ts": "providers", + "zcode-desktop-routes.test.ts": "providers", + "zcode-desktop.test.ts": "providers", + "zcode-routing.test.ts": "providers", + "zcode-transport.test.ts": "providers", "zhipu-bigmodel-provider.test.ts": "providers", "zhipu-bigmodel-responses-quota.test.ts": "providers", "zz-ci-api-usage-isolation.test.ts": "ci-workflows", "zz-ci-storage-policy-isolation.test.ts": "ci-workflows", - "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", - "zcode-adapter.test.ts": "providers", - "zcode-transport.test.ts": "providers", - "zcode-routing.test.ts": "providers" + "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows" }, "migrated": [ "adapters", diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 65141259de..368b893915 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -87,6 +87,10 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt throw new Error("ZCode owns its tools and does not support client tool_choice constraints."); } release = await lock(settings.scope, incoming.abortSignal); + // A queued turn must not resurrect a revoked Desktop connection or old login. + if (settings.desktopModels && (deps.settings ?? loadZcodeSettings)().scope !== settings.scope) { + throw new Error("ZCode Desktop connection changed while this turn was queued."); + } const scope = createHash("sha256").update(JSON.stringify([ settings.scope, provider.baseUrl, parsed._reasoningReplayScope?.current?.providerName, parsed._cursorIdentityScope, parsed.modelId, @@ -100,6 +104,11 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt const content = textInput(parsed, Boolean(sessionId)); client = (deps.client ?? (s => new ZcodeClient(s)))(settings); const active = client; + // The Desktop bootstrap resolves credentials INSIDE the sandbox. The parent only sends + // public model identity; it never receives Desktop API keys or synthesizes vendor auth. + const modelParams = settings.desktopModels + ? { _zcodeModel: { providerId: model.providerId, modelId: model.modelId } } + : { runtimeModel: model.runtimeModel }; const controller = Promise.withResolvers(); // Attach a handler immediately: failures can happen during session materialization. void controller.promise.catch(() => {}); @@ -135,11 +144,11 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt } }; if (sessionId) { - await active.request("session/resume", { sessionId, runtimeModel: model.runtimeModel }); + await active.request("session/resume", { sessionId, ...modelParams }); } else { const result = await active.request("session/create", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace }, - mode: "edit", runtimeModel: model.runtimeModel, titleGenerationEnabled: false, + mode: "edit", ...modelParams, titleGenerationEnabled: false, mcpServers: [], toolDenylist: ["Task", "TaskOutput", "TaskStop"], }); const id = record(result.session).sessionId; @@ -152,7 +161,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt // accepted task that already changed files. Post-send failure is non-retryable incomplete. emit({ type: "text_delta", text: "[ZCode: running in the configured isolated workspace]\n", phase: "commentary" }); sent = true; - await active.request("session/send", { sessionId, content, runtimeModel: model.runtimeModel }); + await active.request("session/send", { sessionId, content, ...modelParams }); await controller.promise; if (sessionKey) { if (sessions.size >= 128) sessions.delete(sessions.keys().next().value!); diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index 36404bab30..e88738fb06 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -5,6 +5,10 @@ import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks" export type ZcodeSpawn = typeof spawn; type Pending = { resolve: (value: JsonObject) => void; reject: (error: Error) => void; timer: ReturnType }; +const desktopClients = new Set(); +export async function closeZcodeDesktopClients(): Promise { + await Promise.all([...desktopClients].map(client => client.disconnect())); +} /** ZCode 0.16.5 uses request/result NDJSON, without a jsonrpc field. */ export class ZcodeClient { @@ -37,6 +41,12 @@ export class ZcodeClient { this.child.once("exit", () => this.fail(new Error("ZCode app server exited before completing the turn."))); this.reading = this.read(); this.detachShutdown = registerOptionalShutdownHook(`zcode-${crypto.randomUUID()}`, () => { void this.close(); }); + if (settings.desktopModels) desktopClients.add(this); + } + + async disconnect(): Promise { + this.fail(new Error("ZCode Desktop was disconnected or reconnected.")); + await this.close(); } private fail(error: Error): void { @@ -106,6 +116,7 @@ export class ZcodeClient { async close(): Promise { if (this.closed) return; this.closed = true; + desktopClients.delete(this); this.detachShutdown(); for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(new Error("ZCode client closed.")); } this.pending.clear(); diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs new file mode 100644 index 0000000000..f267f6fdee --- /dev/null +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -0,0 +1,94 @@ +// Runs INSIDE the managed OS sandbox. Desktop credentials/configuration are mounted read-only. +// Only the official runtime consumes provider keys. The compatible settings file lives in tmpfs, +// never in OpenCodex's data directory, management responses, logs or the Desktop profile. +const fs = require("node:fs"); +const { spawn } = require("node:child_process"); + +function normalizeDesktopConfig(input) { + const provider = {}; + for (const [id, raw] of Object.entries(input?.provider ?? {})) { + if (!raw || typeof raw !== "object" || raw.enabled === false || id === "opencodex") continue; + // Desktop's built-in Z.AI profiles only. Do not import arbitrary proxy chains/custom providers. + if (!id.startsWith("builtin:zai")) continue; + const options = raw.options ?? {}; + if (options.apiKeyRequired !== false && !(typeof options.apiKey === "string" && options.apiKey.trim())) continue; + let url; + try { url = new URL(options.baseURL); } catch { continue; } + if (url.protocol !== "https:" || url.hostname !== "api.z.ai" || url.username || url.password) continue; + const cleanOptions = { baseURL: url.href }; + if (typeof options.apiKey === "string" && options.apiKey.trim()) cleanOptions.apiKey = options.apiKey; + if (typeof options.apiKeyRequired === "boolean") cleanOptions.apiKeyRequired = options.apiKeyRequired; + provider[id] = { + name: typeof raw.name === "string" ? raw.name : id, + kind: raw.kind === "anthropic" ? "anthropic" : "openai-compatible", + source: "custom", enabled: true, options: cleanOptions, + models: raw.models && typeof raw.models === "object" && !Array.isArray(raw.models) ? raw.models : {}, + }; + } + const first = Object.entries(provider).find(([, p]) => Object.keys(p.models).length); + const model = first ? `${first[0]}/${Object.keys(first[1].models)[0]}` : undefined; + return { provider, ...(model ? { model: { main: model, lite: model } } : {}) }; +} + +function desktopModelCatalog(config) { + return Object.entries(config.provider).flatMap(([providerId, p]) => + Object.entries(p.models).slice(0, 200).filter(([id, m]) => id && !/[\x00-\x20]/.test(id) && m && typeof m === "object").map(([modelId, m]) => ({ + id: `${providerId}/${modelId}`, providerId, modelId, + label: `${p.name} / ${typeof m.name === "string" ? m.name : modelId}`.slice(0, 240), + ...(Number.isFinite(m.limit?.context) && m.limit.context > 0 ? { contextWindow: m.limit.context } : {}), + }))); +} +module.exports = { normalizeDesktopConfig, desktopModelCatalog }; +if (require.main === module) { + try { + const path = "/desktop/config.json"; + if (fs.statSync(path).size > 4 * 1024 * 1024) throw new Error("oversized"); + const input = JSON.parse(fs.readFileSync(path, "utf8")); + const config = normalizeDesktopConfig(input); + fs.writeFileSync(`${process.env.HOME}/.zcode/cli/config.json`, JSON.stringify(config), { mode: 0o600, flag: "wx" }); + const args = process.argv.slice(2); + if (args.length !== 1 || args[0] !== "app-server") throw new Error("unsupported command"); + const child = spawn("/usr/bin/node", ["/runtime/zcode.cjs", "app-server"], { stdio: ["pipe", "inherit", "inherit"], shell: false }); + const lines = require("node:readline").createInterface({ input: process.stdin }); + lines.on("line", line => { + try { + if (line.length > 1024 * 1024) throw new Error("oversized request"); + const frame = JSON.parse(line); + if (frame.method === "opencodex/desktopModels") { + const models = desktopModelCatalog(config); + process.stdout.write(JSON.stringify({ id: frame.id, result: { models } }) + "\n"); + return; + } + if (["workspace/readState", "session/create", "session/resume", "session/send"].includes(frame.method)) { + const params = frame.params ?? {}; + const ref = params._zcodeModel ?? params.model; + const providerId = ref?.providerId ?? Object.keys(config.provider)[0]; + const provider = config.provider[providerId]; + const modelId = ref?.modelId ?? Object.keys(provider?.models ?? {})[0]; + if (!provider || !Object.hasOwn(provider.models, modelId)) throw new Error("missing model"); + delete params._zcodeModel; + params.runtimeModel = { + revision: "opencodex-desktop-v1", generatedAt: Date.now(), model: { providerId, modelId }, + provider: { providerId, kind: provider.kind, label: provider.name, source: provider.source, + baseURL: provider.options.baseURL, apiKeyRequired: provider.options.apiKeyRequired, + ...(provider.options.apiKey ? { apiKey: { source: "inline", value: provider.options.apiKey } } : {}), + models: Object.entries(provider.models).map(([id, m]) => ({ modelId: id, + contextWindow: m.limit?.context, maxOutputTokens: m.limit?.output })), + }, + }; + frame.params = params; + } + child.stdin.write(JSON.stringify(frame) + "\n"); + } catch { child.kill("SIGTERM"); } + }); + lines.on("close", () => child.stdin.end()); + child.stdin.on("error", () => child.kill("SIGTERM")); + process.on("SIGTERM", () => child.kill("SIGTERM")); + child.on("error", () => process.exit(1)); + child.on("exit", code => process.exit(code ?? 1)); + } catch { + // Never expose parser excerpts, provider keys or paths from Desktop's configuration. + process.stderr.write("ZCode Desktop configuration is unavailable or incompatible.\n"); + process.exitCode = 1; + } +} diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts new file mode 100644 index 0000000000..d1e0947363 --- /dev/null +++ b/src/adapters/zcode/desktop.ts @@ -0,0 +1,212 @@ +import { constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs"; +import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path"; +import { homedir } from "node:os"; +import { createHash, randomUUID } from "node:crypto"; +import { fileURLToPath } from "node:url"; +import { getConfigDir } from "../../config/paths"; +import { closeZcodeDesktopClients, ZcodeClient } from "./client"; +import type { ZcodeSettings } from "./settings"; + +export interface DesktopModel { id: string; providerId: string; modelId: string; label: string; contextWindow?: number } +interface Connection { + version: 1; connected: boolean; generation: string; + runtime: string; workspace: string; models: DesktopModel[]; +} +export class DesktopSetupError extends Error { + constructor(public code: string) { super(code); } +} +const fail = (code: string): never => { throw new DesktopSetupError(code); }; +const root = () => join(getConfigDir(), "zcode-desktop"); +const connectionPath = () => join(root(), "connection.json"); +export const defaultDesktopWorkspace = () => join(root(), "workspace"); + +function readConnection(): Connection | undefined { + const path = connectionPath(); + if (!existsSync(path)) return undefined; + try { + const st = lstatSync(path); + if (!st.isFile() || st.size > 256 * 1024 || (st.mode & 0o077) !== 0 || st.uid !== process.getuid?.()) fail("connection_invalid"); + const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + const c = JSON.parse(readFileSync(fd, "utf8")) as Connection; + if (c.version !== 1 || typeof c.connected !== "boolean" || typeof c.generation !== "string" || !/^[a-f0-9-]{36}$/.test(c.generation) || typeof c.runtime !== "string" || typeof c.workspace !== "string" || !Array.isArray(c.models)) fail("connection_invalid"); + return c; + } finally { closeSync(fd); } + } catch { return fail("connection_invalid"); } +} + +function persist(connection: Connection): void { + mkdirSync(root(), { recursive: true, mode: 0o700 }); + const temp = join(root(), `${randomUUID()}.tmp`); + writeFileSync(temp, JSON.stringify(connection), { mode: 0o600, flag: "wx" }); + renameSync(temp, connectionPath()); +} + +/** Only an installed Desktop layout, never an arbitrary executable or shell command. */ +export function resolveDesktopRuntime(path: string): string { + if (typeof path !== "string" || !isAbsolute(path) || path.length > 4096 || path.includes("\0")) return fail("desktop_missing"); + for (const candidate of [path, join(path, "resources/glm/zcode.cjs"), join(path, "Contents/Resources/glm/zcode.cjs")]) { + try { + const real = realpathSync(candidate); + if (basename(real) !== "zcode.cjs" || basename(dirname(real)) !== "glm") continue; + const resources = dirname(dirname(real)); + if (!existsSync(join(resources, "app.asar")) || !statSync(real).isFile()) continue; + return real; + } catch { /* try the next installed layout */ } + } + return fail("desktop_missing"); +} + +export function detectDesktopRuntimes(): string[] { + const candidates = ["/opt/ZCode", "/opt/zcode", "/Applications/ZCode.app", join(homedir(), "Applications/ZCode.app")]; + if (process.platform === "linux") { + try { + for (const pid of readdirSync("/proc").filter(p => /^\d+$/.test(p)).slice(0, 4096)) { + try { + if (statSync(`/proc/${pid}`).uid !== process.getuid?.()) continue; + const exe = readlinkSync(`/proc/${pid}/exe`); + if (/^zcode(?:-bin)?$/i.test(basename(exe))) candidates.push(dirname(exe)); + } catch { /* process exited or belongs to another user */ } + } + } catch { /* no process discovery */ } + } + const found = new Set(); + for (const candidate of candidates) { try { found.add(resolveDesktopRuntime(candidate)); } catch { /* absent */ } } + return [...found].slice(0, 8); +} + +export function validateDesktopWorkspace(path: string): string { + if (!path || !isAbsolute(path) || path.includes("\0") || path.length > 4096) return fail("workspace_invalid"); + if (resolve(path) === resolve(defaultDesktopWorkspace())) mkdirSync(path, { recursive: true, mode: 0o700 }); + let workspace: string; + try { workspace = realpathSync(path); if (!statSync(workspace).isDirectory()) return fail("workspace_invalid"); } + catch { return fail("workspace_invalid"); } + const home = realpathSync(homedir()); + if (workspace === home || home.startsWith(workspace.endsWith(sep) ? workspace : workspace + sep)) return fail("workspace_invalid"); + if (workspace !== resolve(defaultDesktopWorkspace())) { + for (const protectedPath of [join(home, ".zcode"), join(home, ".codex"), join(home, ".ssh"), join(home, ".config"), getConfigDir(), root()]) { + if (workspace === protectedPath || workspace.startsWith(protectedPath + sep) || protectedPath.startsWith(workspace + sep)) return fail("workspace_invalid"); + } + } + return workspace; +} + +export function desktopFolders(path?: string) { + const home = realpathSync(homedir()); + const current = path ? realpathSync(path) : home; + if (current !== home && !current.startsWith(home + sep)) return fail("workspace_invalid"); + if (current !== home) validateDesktopWorkspace(current); + const folders = readdirSync(current, { withFileTypes: true }).filter(entry => entry.isDirectory() && !entry.name.startsWith(".")) + .slice(0, 200).map(entry => ({ name: entry.name, path: join(current, entry.name) })); + return { current, parent: current === home ? null : dirname(current), folders }; +} + +function prerequisites(): { bwrap: string; node: string } { + if (process.platform !== "linux") return fail("platform_unsupported"); + const bwrap = Bun.which("bwrap"); if (!bwrap) return fail("sandbox_missing"); + const node = Bun.which("node"); if (!node) return fail("node_missing"); + return { bwrap: realpathSync(bwrap), node: realpathSync(node) }; +} + +function desktopProfile(): { config: string; credentials?: string } { + const base = join(homedir(), ".zcode/v2"); + const config = join(base, "config.json"); + try { if (!lstatSync(config).isFile() || statSync(config).size > 4 * 1024 * 1024) return fail("profile_missing"); } + catch { return fail("profile_missing"); } + const credentials = join(base, "credentials.json"); + return { config, ...(existsSync(credentials) && lstatSync(credentials).isFile() ? { credentials } : {}) }; +} + +function settingsFor(connection: Connection): ZcodeSettings { + const { bwrap, node } = prerequisites(); + const runtime = resolveDesktopRuntime(connection.runtime); + const workspace = validateDesktopWorkspace(connection.workspace); + const profile = desktopProfile(); + // Do not carry a native conversation across Desktop login/profile changes. Metadata-only + // fencing is conservative (a refresh may start a new session) and never reads credential bytes. + const profileStamp = createHash("sha256").update(JSON.stringify([profile.config, profile.credentials].filter(Boolean).map(path => { + const st = statSync(path!); return [st.dev, st.ino, st.size, st.mtimeMs, st.ctimeMs]; + }))).digest("hex"); + const privateHome = join(root(), "home", connection.generation, profileStamp); + const db = join(privateHome, ".zcode/cli/db"); + mkdirSync(db, { recursive: true, mode: 0o700 }); + const sandboxHome = homedir(); // preserve the official credential cipher's HOME/username identity + const args = ["--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--ro-bind", "/usr", "/usr"]; + for (const path of ["/bin", "/lib", "/lib64", "/sbin"]) { + if (!existsSync(path)) continue; + const st = lstatSync(path); + args.push(...(st.isSymbolicLink() ? ["--symlink", readlinkSync(path), path] : ["--ro-bind", path, path])); + } + args.push("--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/etc"); + for (const path of ["/etc/passwd", "/etc/resolv.conf", "/etc/ssl", "/etc/pki"]) if (existsSync(path)) args.push("--ro-bind", path, path); + args.push("--ro-bind", dirname(runtime), "/runtime", "--ro-bind", node, "/usr/bin/node", + "--ro-bind", fileURLToPath(new URL("./desktop-bootstrap.cjs", import.meta.url)), "/bridge/desktop-bootstrap.cjs", + "--bind", privateHome, sandboxHome, "--bind", workspace, "/workspace", + "--ro-bind", profile.config, "/desktop/config.json", + "--tmpfs", `${sandboxHome}/.zcode/cli`, "--bind", db, `${sandboxHome}/.zcode/cli/db`); + if (profile.credentials) args.push("--ro-bind", profile.credentials, "/desktop/.zcode/v2/credentials.json"); + args.push("--clearenv", "--setenv", "HOME", sandboxHome, "--setenv", "PATH", "/usr/bin:/bin", + "--setenv", "ZCODE_DATA_BASE_DIR", "/desktop", "--chdir", "/workspace", + "/usr/bin/node", "/bridge/desktop-bootstrap.cjs"); + return { command: [bwrap, ...args], home: privateHome, workspace: "/workspace", settingsPath: "", + scope: `desktop:${connection.generation}:${profileStamp}`, desktopModels: connection.models }; +} + +/** Persisted GUI consent is separate from provider config; data-plane requests cannot set it. */ +export function loadDesktopSettings(): ZcodeSettings | undefined { + if (connecting) return fail("busy"); + const connection = readConnection(); + if (!connection) return undefined; + if (!connection.connected) return fail("disconnected"); + return settingsFor(connection); +} + +export function desktopStatus() { + let issue: string | undefined; + try { prerequisites(); desktopProfile(); } catch (e) { issue = e instanceof DesktopSetupError ? e.code : "connection_invalid"; } + let connection: Connection | undefined; + try { connection = readConnection(); } catch { issue = "connection_invalid"; } + const runtimes = detectDesktopRuntimes(); + if (connection?.connected) { + try { resolveDesktopRuntime(connection.runtime); validateDesktopWorkspace(connection.workspace); } + catch (e) { issue = e instanceof DesktopSetupError ? e.code : "connection_invalid"; } + } + return { connected: connection?.connected === true && !issue, issue, runtimes, + runtime: connection?.runtime ?? runtimes[0] ?? "", workspace: connection?.workspace ?? defaultDesktopWorkspace(), + models: connection?.models ?? [], platform: process.platform }; +} + +let connecting = false; +export async function connectDesktop(runtime: string, workspace: string): Promise> { + if (connecting) return fail("busy"); + connecting = true; + try { + const connection: Connection = { version: 1, connected: true, generation: randomUUID(), + runtime: resolveDesktopRuntime(runtime), workspace: validateDesktopWorkspace(workspace), models: [] }; + const client = new ZcodeClient(settingsFor(connection)); + try { + const result = await client.request("opencodex/desktopModels", {}, 15_000); + if (!Array.isArray(result.models) || !result.models.length || result.models.length > 1000) return fail("models_missing"); + connection.models = result.models.filter((m: DesktopModel) => typeof m.id === "string" && typeof m.providerId === "string" + && m.providerId.startsWith("builtin:zai") && typeof m.modelId === "string" && !/[\x00-\x20]/.test(m.id) + && m.id === `${m.providerId}/${m.modelId}`).map((m: DesktopModel) => ({ + id: m.id, providerId: m.providerId, modelId: m.modelId, label: String(m.label).slice(0, 240), + ...(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow) && m.contextWindow > 0 ? { contextWindow: m.contextWindow } : {}), + })); + if (!connection.models.length) return fail("models_missing"); + // Official protocol readiness only. This does not send a prompt or spend inference quota. + await client.request("workspace/readState", { workspace: { workspacePath: "/workspace", workspaceKey: "/workspace" } }, 15_000); + } finally { await client.close(); } + await closeZcodeDesktopClients(); + persist(connection); + return desktopStatus(); + } catch (e) { if (e instanceof DesktopSetupError) throw e; return fail("runtime_failed"); } + finally { connecting = false; } +} + +export async function disconnectDesktop(): Promise { + if (connecting) return fail("busy"); + const previous = readConnection(); + persist({ version: 1, connected: false, generation: randomUUID(), runtime: previous?.runtime ?? "", workspace: previous?.workspace ?? defaultDesktopWorkspace(), models: [] }); + await closeZcodeDesktopClients(); +} diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index 93d87dc0c0..611b2b13f3 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -1,6 +1,7 @@ import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from "node:fs"; import { isAbsolute, join, sep } from "node:path"; import { createHash } from "node:crypto"; +import { loadDesktopSettings, type DesktopModel } from "./desktop"; export type JsonObject = Record; export function record(value: unknown): JsonObject { @@ -13,10 +14,16 @@ export interface ZcodeSettings { workspace: string; settingsPath: string; scope: string; + desktopModels?: DesktopModel[]; } -/** Execution authority is operator environment, never a request header or model configuration. */ +/** Execution authority is operator environment or persisted GUI consent, never data-plane input. */ export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env): ZcodeSettings { + // Explicit env fixtures remain hermetic. Production prefers the GUI's persisted connection. + if (env === process.env) { + const desktop = loadDesktopSettings(); + if (desktop) return desktop; + } if (env.OCX_ZCODE_NATIVE_TOOLS !== "1") { throw new Error("ZCode native execution is disabled. Configure an isolated launcher and opt in with OCX_ZCODE_NATIVE_TOOLS=1."); } @@ -53,6 +60,7 @@ export interface ZcodeModel { /** Local settings only: no credential import, cloud discovery or writes to ZCode Desktop. */ export function readZcodeModels(settings: ZcodeSettings): ZcodeModel[] { + if (settings.desktopModels) return settings.desktopModels.map(model => ({ ...model, runtimeModel: {} })); try { return readModels(settings); } catch { throw new Error("ZCode isolated model settings are unavailable, invalid or exceed the size limit."); } } diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 4ba7010727..4be89edcc9 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -2000,7 +2000,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ id: "zcode", label: "ZCode (local agent)", adapter: "zcode", baseUrl: "https://zcode.z.ai", authKind: "local", liveModels: true, dashboardPreset: true, allowBaseUrlOverride: false, - note: "Opt-in local ZCode app-server agent, not the ZCode client integration. Requires an isolated launcher (OCX_ZCODE_COMMAND JSON argv), separate OCX_ZCODE_HOME, OCX_ZCODE_WORKSPACE and OCX_ZCODE_NATIVE_TOOLS=1. Log in using that isolated launcher. ZCode owns its tools; text input only. No automatic credential import or installation.", + note: "Connect the official runtime and Z.AI models configured in ZCode Desktop from the provider setup panel. On Linux, OpenCodex manages an isolated workspace after explicit consent; no API key or environment variables to paste. Desktop settings stay read-only. ZCode owns its tools; text input only. Advanced isolated launchers remain supported.", }, { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 2cd813264c..79f424c561 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -692,6 +692,10 @@ function canonicalOpenAiBudgetPatchError( export async function handleProviderRoutes(ctx: ManagementContext): Promise { const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx; + if (url.pathname === "/api/zcode-desktop" || url.pathname.startsWith("/api/zcode-desktop/")) { + const { handleZcodeDesktopRoutes } = await import("./zcode-desktop-routes"); + return handleZcodeDesktopRoutes(ctx); + } if (url.pathname === "/api/provider-quotas" && req.method === "GET") { const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true"; diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts new file mode 100644 index 0000000000..d116934632 --- /dev/null +++ b/src/server/management/zcode-desktop-routes.ts @@ -0,0 +1,63 @@ +import { jsonResponse } from "../auth-cors"; +import type { ManagementContext } from "./context"; +import { connectDesktop, desktopStatus, desktopFolders, DesktopSetupError, disconnectDesktop } from "../../adapters/zcode/desktop"; +import { createZcodeAdapter } from "../../adapters/zcode/adapter"; +import { createTranslatorBudget } from "../../lib/translator-budget"; +import { invalidateCodexModelsCache } from "../../codex/catalog"; +import { clearGatherRoutedModelsInflight } from "../../codex/catalog/provider-fetch"; +import { readBoundedJsonRequestBody } from "../request-decompress"; + +const services = { connectDesktop, desktopStatus, disconnectDesktop }; +let testing = false; +export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = services): Promise { + const { req, url } = ctx; + if (!url.pathname.startsWith("/api/zcode-desktop")) return null; + if (url.pathname === "/api/zcode-desktop" && req.method === "GET") return jsonResponse(deps.desktopStatus()); + if (url.pathname === "/api/zcode-desktop/folders" && req.method === "GET") { + try { return jsonResponse(desktopFolders(url.searchParams.get("path") ?? undefined)); } + catch { return jsonResponse({ error: "workspace_invalid" }, 400); } + } + if (req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405); + // Executable/workspace selection and optional quota-spending tests require an actual GUI + // principal, not forgeable Origin headers or a data/admin token. + if (ctx.principal !== "gui-session") return jsonResponse({ error: "dashboard_required" }, 403); + try { + const raw = await readBoundedJsonRequestBody(req, 12_000, undefined, { signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]) }); + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return jsonResponse({ error: "invalid_request" }, 400); + const body = raw as Record; + if (url.pathname === "/api/zcode-desktop/connect") { + if (body.consent !== true || typeof body.runtime !== "string" || typeof body.workspace !== "string") return jsonResponse({ error: "consent_required" }, 400); + const status = await deps.connectDesktop(body.runtime, body.workspace); + invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); + return jsonResponse(status); + } + if (url.pathname === "/api/zcode-desktop/disconnect") { + await deps.disconnectDesktop(); invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); + return jsonResponse(deps.desktopStatus()); + } + if (url.pathname === "/api/zcode-desktop/test") { + if (testing) return jsonResponse({ error: "busy" }, 409); + const status = deps.desktopStatus(); + if (body.consent !== true || !status.connected || !status.models.some(m => m.id === body.model)) return jsonResponse({ error: "invalid_request" }, 400); + testing = true; + const budget = createTranslatorBudget(); + const deadline = AbortSignal.timeout(60_000); + let completed = false; + let textSeen = false; + try { + await createZcodeAdapter({ adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai" }).runTurn!({ + modelId: String(body.model), stream: false, options: {}, context: { messages: [ + { role: "user", timestamp: Date.now(), content: "Reply with exactly ZCODE_CONNECTION_OK. Do not use any tools." }, + ] }, + }, { headers: new Headers(), abortSignal: AbortSignal.any([req.signal, deadline]), translatorBudget: budget }, event => { + if (event.type === "text_delta" && event.phase !== "commentary" && event.text.trim()) textSeen = true; + if (event.type === "done") completed = true; + }); + return jsonResponse({ ok: completed && textSeen, ...(!completed || !textSeen ? { error: "inference_failed" } : {}) }); + } finally { budget.dispose(); testing = false; } + } + return jsonResponse({ error: "not_found" }, 404); + } catch (e) { + return jsonResponse({ error: e instanceof DesktopSetupError ? e.code : "runtime_failed" }, 400); + } +} diff --git a/structure/10_adapter-registry.md b/structure/10_adapter-registry.md index d5aa34b126..55a2fb9a19 100644 --- a/structure/10_adapter-registry.md +++ b/structure/10_adapter-registry.md @@ -25,7 +25,10 @@ The registry records those relationships with `contractParent`. A parent relatio `zcode` uses the direct `zcode` wire and `agent-owned-with-explicit-opt-in` mutation contract. Unlike routed function tools, native ZCode actions are informational output only. Its `runTurn` sets `replaySafe: false`; accepted failures terminate incomplete rather than becoming automatic -failover candidates. Launcher authority comes only from the operator environment, never requests. +failover candidates. Launcher authority comes from either the operator environment or a separately persisted, +GUI-consented Desktop connection. Data-plane requests and ordinary provider configuration cannot +set command/workspace paths. The managed Desktop bootstrap keeps credential-bearing runtime +descriptors inside its OS sandbox; the parent sees public model identities only. Adding a production adapter requires: diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 392f217154..f84e0a501b 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1171,13 +1171,15 @@ "yaml-fragment-source.test.ts": "config", "z-fulfill.test.ts": "images", "z-handler-activation.test.ts": "images", + "zcode-adapter.test.ts": "providers", "zcode-client.test.ts": "providers", + "zcode-desktop-routes.test.ts": "providers", + "zcode-desktop.test.ts": "providers", + "zcode-routing.test.ts": "providers", + "zcode-transport.test.ts": "providers", "zhipu-bigmodel-provider.test.ts": "providers", "zhipu-bigmodel-responses-quota.test.ts": "providers", "zz-ci-api-usage-isolation.test.ts": "ci-workflows", "zz-ci-storage-policy-isolation.test.ts": "ci-workflows", - "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", - "zcode-adapter.test.ts": "providers", - "zcode-transport.test.ts": "providers", - "zcode-routing.test.ts": "providers" + "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows" } diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index dd912206ff..03e9c133af 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -63,6 +63,24 @@ async function run(settings: ZcodeSettings, client: FakeClient, parsed = request } describe("ZCode local agent", () => { + test("managed requests select a public model without carrying Desktop secrets", async () => { + const settings = { ...fixture(), desktopModels: [{ id: "test/model", providerId: "test", modelId: "model", label: "Model" }] }; + const client = new FakeClient(); + expect((await run(settings, client)).at(-1)?.type).toBe("done"); + for (const call of client.calls.filter(c => ["session/create", "session/send"].includes(c.method))) { + expect(call.params._zcodeModel).toEqual({ providerId: "test", modelId: "model" }); + expect(call.params.runtimeModel).toBeUndefined(); + } + }); + test("a revoked or changed managed connection cannot start a queued child", async () => { + const settings = { ...fixture(), desktopModels: [{ id: "test/model", providerId: "test", modelId: "model", label: "Model" }] }; + let reads = 0; let children = 0; + const adapter = createZcodeAdapter(provider, { settings: () => ({ ...settings, scope: ++reads === 1 ? "before" : "after" }), client: () => { children++; return new FakeClient(); } }); + const events: AdapterEvent[] = []; + await adapter.runTurn!(request(), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, e => events.push(e)); + expect(children).toBe(0); + expect(events.at(-1)?.type).toBe("error"); + }); test("has no direct HTTP inference path or client-tool capability", () => { const adapter = createZcodeAdapter(provider); expect(adapter.fetchResponse).toBeUndefined(); diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts new file mode 100644 index 0000000000..3cedc6823c --- /dev/null +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test"; +import { handleZcodeDesktopRoutes } from "../../src/server/management/zcode-desktop-routes"; +import type { ManagementContext } from "../../src/server/management/context"; + +const status = { connected: false, issue: undefined, runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [], platform: "linux" as const }; +function context(path: string, body: unknown, principal?: ManagementContext["principal"]): ManagementContext { + const req = new Request(`http://localhost${path}`, { method: "POST", headers: { "content-type": "application/json", origin: "http://localhost", "x-opencodex-gui-session": "forged" }, body: JSON.stringify(body) }); + return { req, url: new URL(req.url), principal } as ManagementContext; +} +function fixture() { + let calls = 0; + const deps = { desktopStatus: () => status, connectDesktop: async () => { calls++; return { ...status, connected: true }; }, disconnectDesktop: async () => { calls++; } }; + return { deps, calls: () => calls }; +} +describe("ZCode Desktop management consent", () => { + for (const principal of [undefined, "admin-token"] as const) { + test(`rejects native execution configuration from ${principal ?? "missing"} principal`, async () => { + const f = fixture(); + for (const action of ["connect", "disconnect", "test"]) { + const response = await handleZcodeDesktopRoutes(context(`/api/zcode-desktop/${action}`, { consent: true, runtime: "/installed/ZCode", workspace: "/project" }, principal), f.deps); + expect(response?.status).toBe(403); + } + expect(f.calls()).toBe(0); + }); + } + test("requires an explicit consent checkbox, not just a GUI session", async () => { + const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { runtime: "/installed/ZCode", workspace: "/project" }, "gui-session"), f.deps); + expect(response?.status).toBe(400); expect(f.calls()).toBe(0); + }); + test("connection does not send inference or mutate provider settings", async () => { + const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { consent: true, runtime: "/installed/ZCode", workspace: "/project" }, "gui-session"), f.deps); + expect(await response?.json()).toMatchObject({ connected: true }); expect(f.calls()).toBe(1); + }); + test("oversized setup payload is rejected before runtime execution", async () => { + const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { consent: true, runtime: "x".repeat(14000), workspace: "/project" }, "gui-session"), f.deps); + expect(response?.status).toBe(400); expect(f.calls()).toBe(0); + }); +}); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts new file mode 100644 index 0000000000..0efa48a88a --- /dev/null +++ b/tests/providers/zcode-desktop.test.ts @@ -0,0 +1,74 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { disconnectDesktop, loadDesktopSettings, resolveDesktopRuntime, validateDesktopWorkspace } from "../../src/adapters/zcode/desktop"; +import { readZcodeModels } from "../../src/adapters/zcode/settings"; + +const { normalizeDesktopConfig, desktopModelCatalog } = createRequire(import.meta.url)("../../src/adapters/zcode/desktop-bootstrap.cjs"); +let root: string; +let previousHome: string | undefined; +beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "ocx-desktop-test-")); + previousHome = process.env.OPENCODEX_HOME; process.env.OPENCODEX_HOME = join(root, "proxy"); +}); +afterEach(() => { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; + rmSync(root, { recursive: true, force: true }); +}); +const provider = () => ({ enabled: true, name: "Z.AI", kind: "anthropic", options: { + baseURL: "https://api.z.ai/api/anthropic", apiKey: "fixture-private-value", apiKeyRequired: true, +}, models: { model: { name: "Model", limit: { context: 10000 } } } }); + +describe("managed ZCode Desktop", () => { + test("normalizes only configured official Z.AI profiles, not custom/direct proxy routes", () => { + const config = normalizeDesktopConfig({ provider: { + "builtin:zai-coding-plan": provider(), + "builtin:zai-empty": { ...provider(), options: { ...provider().options, apiKey: "" } }, + "builtin:zai-disabled": { ...provider(), enabled: false }, + "builtin:zai-local": { ...provider(), options: { baseURL: "https://localhost/v1" } }, + "builtin:zai-alias": { ...provider(), options: { baseURL: "https://api.z.ai.evil.invalid/v1" } }, + custom: provider(), opencodex: provider(), + } }); + expect(Object.keys(config.provider)).toEqual(["builtin:zai-coding-plan"]); + const catalog = desktopModelCatalog(config); + expect(catalog).toEqual([{ id: "builtin:zai-coding-plan/model", providerId: "builtin:zai-coding-plan", modelId: "model", label: "Z.AI / Model", contextWindow: 10000 }]); + expect(JSON.stringify(catalog)).not.toContain("fixture-private-value"); + expect(JSON.stringify(catalog)).not.toContain("apiKey"); + }); + test("runtime selection requires a Desktop resources layout, not arbitrary commands", () => { + expect(() => resolveDesktopRuntime("node -e malicious")).toThrow("desktop_missing"); + const glm = join(root, "app/resources/glm"); mkdirSync(glm, { recursive: true }); + writeFileSync(join(glm, "zcode.cjs"), ""); + expect(() => resolveDesktopRuntime(join(root, "app"))).toThrow("desktop_missing"); + writeFileSync(join(root, "app/resources/app.asar"), "fixture"); + expect(resolveDesktopRuntime(join(root, "app"))).toBe(join(glm, "zcode.cjs")); + }); + test("workspace rejects system roots and secret stores, follows aliases before checking", () => { + expect(() => validateDesktopWorkspace("/")).toThrow("workspace_invalid"); + expect(() => validateDesktopWorkspace("relative")).toThrow("workspace_invalid"); + mkdirSync(process.env.OPENCODEX_HOME!, { recursive: true }); + expect(() => validateDesktopWorkspace(process.env.OPENCODEX_HOME!)).toThrow("workspace_invalid"); + const project = join(root, "project"); mkdirSync(project); + expect(validateDesktopWorkspace(project)).toBe(project); + if (process.platform !== "win32") { + symlinkSync(process.env.OPENCODEX_HOME!, join(root, "alias")); + expect(() => validateDesktopWorkspace(join(root, "alias"))).toThrow("workspace_invalid"); + } + }); + test("disconnect persists revocation instead of falling back to operator env", async () => { + expect(loadDesktopSettings()).toBeUndefined(); + await disconnectDesktop(); + const path = join(process.env.OPENCODEX_HOME!, "zcode-desktop/connection.json"); + expect(JSON.parse(readFileSync(path, "utf8")).connected).toBe(false); + if (process.platform !== "win32") expect(statSync(path).mode & 0o777).toBe(0o600); + expect(() => loadDesktopSettings()).toThrow("disconnected"); + }); + test("managed model discovery reads only the public cache, not Desktop credentials", () => { + const result = readZcodeModels({ command: [], home: root, workspace: "/workspace", settingsPath: "/does/not/exist", + scope: "desktop:test", desktopModels: [{ id: "builtin:zai/model", providerId: "builtin:zai", modelId: "model", label: "Model" }] }); + expect(result[0]?.id).toBe("builtin:zai/model"); + expect(result[0]?.runtimeModel).toEqual({}); + }); +}); diff --git a/tests/providers/zcode-transport.test.ts b/tests/providers/zcode-transport.test.ts index 7ca0d5206d..8e94ac61e9 100644 --- a/tests/providers/zcode-transport.test.ts +++ b/tests/providers/zcode-transport.test.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { PassThrough, Writable } from "node:stream"; -import { ZcodeClient, type ZcodeSpawn } from "../../src/adapters/zcode/client"; +import { ZcodeClient, closeZcodeDesktopClients, type ZcodeSpawn } from "../../src/adapters/zcode/client"; import type { JsonObject, ZcodeSettings } from "../../src/adapters/zcode/settings"; const settings: ZcodeSettings = { command: ["/isolated/launcher", "argument with spaces"], home: "/isolated/home", workspace: "/workspace", settingsPath: "/isolated/config.json", scope: "test" }; -function fixture() { +function fixture(managed = false) { const child = new EventEmitter() as EventEmitter & { stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: () => boolean }; const writes: JsonObject[] = []; child.stdin = new Writable({ write(chunk, _encoding, callback) { writes.push(JSON.parse(chunk.toString())); callback(); } }); @@ -14,12 +14,23 @@ function fixture() { child.kill = () => { child.stdout.end(); child.stderr.end(); queueMicrotask(() => child.emit("exit", 0)); return true; }; let invocation: unknown[] = []; const spawn = ((...args: unknown[]) => { invocation = args; return child; }) as ZcodeSpawn; - const client = new ZcodeClient(settings, spawn); + const client = new ZcodeClient(managed ? { ...settings, desktopModels: [] } : settings, spawn); return { child, client, writes, invocation: () => invocation }; } const tick = () => new Promise(resolve => setTimeout(resolve, 5)); describe("ZCode NDJSON transport", () => { + test("Desktop disconnect notifies an active turn even without pending RPCs", async () => { + const managed = fixture(true); const advanced = fixture(); + let failures = 0; let advancedFailures = 0; + managed.client.onFailure = () => { failures++; }; + advanced.client.onFailure = () => { advancedFailures++; }; + await closeZcodeDesktopClients(); + expect(failures).toBe(1); + expect(advancedFailures).toBe(0); + await expect(managed.client.request("session/create", {})).rejects.toThrow("disconnected"); + await advanced.client.close(); + }); test("uses argv without shell and does not inherit provider secrets", async () => { const f = fixture(); const invocation = f.invocation(); expect(invocation[0]).toBe("/isolated/launcher"); From c29591497b9562e9d565e57cd52252b59a42f341 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 04:55:11 -0300 Subject: [PATCH 03/43] feat(zcode): show remaining subscription quota through Desktop --- docs-site/public/images/zcode-quota-bars.png | Bin 0 -> 292927 bytes .../src/content/docs/guides/zcode-agent.md | 35 +++++ gui/src/components/QuotaBars.tsx | 16 ++- .../ProviderAccountQuota.tsx | 4 +- .../ProviderCapacityQuota.tsx | 1 + .../ProviderCurrentQuota.tsx | 2 +- .../ProviderOverviewDashboard.tsx | 2 +- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/tests/zcode-quota-bars.test.tsx | 41 ++++++ scripts/test-layout/layout.json | 3 +- src/adapters/zcode/quota-bootstrap.cjs | 37 ++++++ src/adapters/zcode/quota.ts | 124 ++++++++++++++++++ src/providers/quota-routing-cache.ts | 2 + src/providers/quota.ts | 12 +- structure/10_adapter-registry.md | 4 + tests/fixtures/test-layout-expected.json | 3 +- tests/providers/zcode-quota.test.ts | 55 ++++++++ 25 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 docs-site/public/images/zcode-quota-bars.png create mode 100644 gui/tests/zcode-quota-bars.test.tsx create mode 100644 src/adapters/zcode/quota-bootstrap.cjs create mode 100644 src/adapters/zcode/quota.ts create mode 100644 tests/providers/zcode-quota.test.ts diff --git a/docs-site/public/images/zcode-quota-bars.png b/docs-site/public/images/zcode-quota-bars.png new file mode 100644 index 0000000000000000000000000000000000000000..984e56ceff02e84fcb6701aeeeca4333f4cad1eb GIT binary patch literal 292927 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

86oL1^Z(^C^ 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
)evB_eudT4_YdSl|-Yu`EJS|jS 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 UL^dSC#Q*>R07*qoM6N<$g7f3tbpQYW literal 0 HcmV?d00001 diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index f97001bb3a..7f8c30ed7c 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -214,3 +214,38 @@ conservatively stops advertising it at midnight ending September 20 Singapore. Source links and this boundary caveat are visible in the notice. Exact Z.AI Coding Plan HTTP endpoints also show peak/off-peak notices, but never the ZCode-only free-window alert; ordinary pay-as-you-go endpoints are excluded. + +## Remaining subscription quota + +The provider overview and Usage tab reuse the same quota-bar component as OpenAI. +For ZCode, the fill and label show **remaining** model credits in the 5-hour and +weekly windows, with localized reset times. Warning colors still reflect high +consumption. Unknown or failed readings do not appear as a full balance. + +Quota comes from the **official ZCode Desktop host service**, not from locally +counted tokens or a proxy-made Z.AI API request. OpenCodex starts the trusted +Desktop executable in its Node runtime mode and calls its existing read-only +entitlement RPC. Native ZCode owns authentication and network requests. This +separate short-lived Bubblewrap sandbox has no writable project folder: Desktop +configuration is mounted read-only and copied only into disposable tmpfs so the +native service can perform its normal configuration normalization. Credential +material never leaves that sandbox. No model prompt, quota-reset action, or +purchase is requested. Tested with Desktop 3.10.2 on Linux; other private host +protocol versions can return unavailable instead of a fabricated quota. + +Managed Desktop connections need no extra quota configuration. For the advanced +operator-launcher setup, explicitly set `OCX_ZCODE_DESKTOP_RUNTIME` to the trusted +installed/extracted Desktop directory (the directory containing `resources/`). +The native quota host then uses the Z.AI key from the **same isolated CLI config**, +not the user's unrelated Desktop login. The source provider is the configured +default model's provider prefix, or `zai` when none is set; an absent key or +unsupported profile is reported as unavailable. This opt-in does not change +inference routing or migrate your model IDs. + +The dashboard's existing quota cache and Refresh quotas controls apply. Concurrent +reads share one native probe. Account/configuration changes invalidate snapshots; +errors clear the ZCode report rather than leaving a misleading full balance. +These bars are informational and do not enable quota-based automatic rerouting. +The native session app-server's token accounting remains unavailable; subscription +quota is a distinct measurement. See [ZCode Usage Stats](https://zcode.z.ai/en/docs/usage-stats) +for the distinction between App Usage and Coding Plan statistics. diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx index a1a32e1cfa..0327d64fed 100644 --- a/gui/src/components/QuotaBars.tsx +++ b/gui/src/components/QuotaBars.tsx @@ -237,6 +237,7 @@ export default function QuotaBars({ incompleteWindowKeys, incompleteCustomWindowLabels, observedAt, + showRemaining = false, }: { quota: AccountQuota | null; plan?: string | null; @@ -262,6 +263,8 @@ export default function QuotaBars({ * look live. */ observedAt?: number; + /** Display remaining balance for native subscription snapshots; consumption stays canonical. */ + showRemaining?: boolean; }) { const { locale } = useI18n(); const rows = buildQuotaRows(quota, plan, t); @@ -320,6 +323,7 @@ export default function QuotaBars({ {observedLine} {rows.map(row => (
@@ -419,11 +426,12 @@ function StackedQuotaRow({ row, threshold, t, locale, incomplete }: { {resetText}
-
-
+
+
- {t("quota.usedPercent", { pct: Math.round(row.percent) })} + {showRemaining ? remainingText : t("quota.usedPercent", { pct: Math.round(row.percent) })}
{exhausted && ( diff --git a/gui/src/components/provider-workspace/ProviderAccountQuota.tsx b/gui/src/components/provider-workspace/ProviderAccountQuota.tsx index 42f4d12af2..ad49ec1ff0 100644 --- a/gui/src/components/provider-workspace/ProviderAccountQuota.tsx +++ b/gui/src/components/provider-workspace/ProviderAccountQuota.tsx @@ -5,7 +5,7 @@ import { ProviderCapacityQuota } from "./ProviderCapacityQuota"; import type { AccountQuotaReading } from "./types"; /** The same reading states and credit/window renderer for current and all-account views. */ -export default function ProviderAccountQuota({ quota: rawQuota, quotaMode, quotaUnavailable, quotaPending }: AccountQuotaReading) { +export default function ProviderAccountQuota({ quota: rawQuota, quotaMode, quotaUnavailable, quotaPending, source }: AccountQuotaReading & { source?: string }) { const t = useT(); const quota = accountQuotaFromReport({ quota: rawQuota }); if (quotaMode === "unsupported") { @@ -17,7 +17,7 @@ export default function ProviderAccountQuota({ quota: rawQuota, quotaMode, quota {quotaUnavailable &&

{t("pws.accountQuotaUnavailable")}

} {quota || pending ? ( ) : !quotaUnavailable && ( diff --git a/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx b/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx index f651e25821..e5df4c311b 100644 --- a/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx +++ b/gui/src/components/provider-workspace/ProviderCapacityQuota.tsx @@ -90,6 +90,7 @@ export function ProviderCapacityQuota({ report, pending }: { report: ProviderQuo {showsAggregate &&
{t("pws.capacity.estimate")}
} {(primaryQuota || pending) && (
}
- + {item && zcodeUsageScope(item) && } {quota && !effective.quotaUnavailable && effective.quotaMode !== "unsupported" &&
{!rowOwnsReading && current?.source?.trim() &&
diff --git a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx index 6a2db87e02..28549c2c3a 100644 --- a/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx +++ b/gui/src/components/provider-workspace/ProviderOverviewDashboard.tsx @@ -206,7 +206,6 @@ export default function ProviderOverviewDashboard({ aria-busy={quotasLoading || undefined} >

{t("pws.dashboard.rateLimits")}

- {allItems.some(item => zcodeUsageScope(item)) && zcodeUsageScope(item) === "zcode")} />} {quotaProviders.length > 0 ? (
{quotaProviders.map(({ item, report }) => ( @@ -248,6 +247,7 @@ export default function ProviderOverviewDashboard({ ) : (

{t("pws.dashboard.noRateLimits")}

)} + {allItems.some(item => zcodeUsageScope(item)) && zcodeUsageScope(item) === "zcode")} />}
= { + "quota.remainingPercent": "{pct}% verbleibend", "zcodeUsage.title": "GLM Coding Plan · zeitabhängige Vorteile", "zcodeUsage.zone": "Zeiten in {zone}. Referenz: Singapur (UTC+8).", "zcodeUsage.peak": "Spitzenzeit: regulärer Modell-Credittarif bis {until}.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 2e7abb6fa9..bc8cb1a789 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -6,6 +6,7 @@ * `{var}` are plain interpolations. */ export const en = { + "quota.remainingPercent": "{pct}% remaining", "zcodeUsage.title": "GLM Coding Plan · time-based benefits", "zcodeUsage.zone": "Times shown in {zone}. Schedule reference: Singapore (UTC+8).", "zcodeUsage.peak": "Peak hours: standard model credit rate until {until}.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index f72853f2e9..cdaeacef11 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * French i18n catalog. Must match the `TKey` set. */ export const fr: Record = { + "quota.remainingPercent": "{pct}% restant", "zcodeUsage.title": "GLM Coding Plan · avantages horaires", "zcodeUsage.zone": "Heures affichées dans {zone}. Référence : Singapour (UTC+8).", "zcodeUsage.peak": "Heures de pointe : tarif standard en crédits jusqu’à {until}.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index b48c488712..1a40378f06 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Japanese i18n catalog; must match the `TKey` set (compile-checked). */ export const ja: Record = { + "quota.remainingPercent": "残り {pct}%", "zcodeUsage.title": "GLM Coding Plan · 時間帯別特典", "zcodeUsage.zone": "表示時刻:{zone}。基準:シンガポール(UTC+8)。", "zcodeUsage.peak": "ピーク時間:{until}まで通常のモデルクレジット料金。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index a9c7f94a19..9b0c8cab43 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Korean i18n catalog; must match the `TKey` set (compile-checked). */ export const ko: Record = { + "quota.remainingPercent": "{pct}% 남음", "zcodeUsage.title": "GLM Coding Plan · 시간대별 혜택", "zcodeUsage.zone": "{zone} 시간으로 표시합니다. 기준: 싱가포르(UTC+8).", "zcodeUsage.peak": "피크 시간: {until}까지 표준 모델 크레딧 요금입니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 478f777721..f16183cb7f 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Russian i18n catalog; must match the `TKey` set (compile-checked). */ export const ru: Record = { + "quota.remainingPercent": "Осталось {pct}%", "zcodeUsage.title": "GLM Coding Plan · льготы по времени", "zcodeUsage.zone": "Время в {zone}. Основа расписания: Сингапур (UTC+8).", "zcodeUsage.peak": "Пиковые часы: стандартная ставка кредитов модели до {until}.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index ad3ffd471c..e0dd89fae9 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -5,6 +5,7 @@ import type { TKey } from "./en"; * Turkish i18n catalog. Must match the `TKey` set (compile-checked). */ export const tr: Record = { + "quota.remainingPercent": "%{pct} kaldı", "zcodeUsage.title": "GLM Coding Plan · saat bazlı avantajlar", "zcodeUsage.zone": "Saatler {zone} diliminde gösterilir. Referans: Singapur (UTC+8).", "zcodeUsage.peak": "Yoğun saatler: {until} tarihine kadar standart model kredi tarifesi.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index f280b74d1c..d8fd6aacac 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -2,6 +2,7 @@ import type { TKey } from "./en"; /** Traditional Chinese (Taiwan) UI strings — keys must match `en.ts` 1:1. */ export const zhTW: Record = { + "quota.remainingPercent": "剩餘 {pct}%", "zcodeUsage.title": "GLM Coding Plan · 時段優惠", "zcodeUsage.zone": "時間顯示為 {zone}。時段基準:新加坡(UTC+8)。", "zcodeUsage.peak": "尖峰時段:截至 {until} 按標準模型點數費率計費。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index a190049d9b..d61f28cbb2 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -4,6 +4,7 @@ import type { TKey } from "./en"; * Chinese i18n catalog; must match the `TKey` set (compile-checked). */ export const zh: Record = { + "quota.remainingPercent": "剩余 {pct}%", "zcodeUsage.title": "GLM Coding Plan · 时段优惠", "zcodeUsage.zone": "时间显示为 {zone}。时段基准:新加坡(UTC+8)。", "zcodeUsage.peak": "高峰时段:截至 {until} 按标准模型积分费率计费。", diff --git a/gui/tests/zcode-quota-bars.test.tsx b/gui/tests/zcode-quota-bars.test.tsx new file mode 100644 index 0000000000..9ba6404b44 --- /dev/null +++ b/gui/tests/zcode-quota-bars.test.tsx @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { LanguageProvider } from "../src/i18n/provider"; +import { ProviderCapacityQuota } from "../src/components/provider-workspace/ProviderCapacityQuota"; +import ProviderCurrentQuota from "../src/components/provider-workspace/ProviderCurrentQuota"; + +const report = () => ({ source: "zcode-desktop", updatedAt: Date.now(), quota: { + updatedAt: Date.now(), fiveHourPercent: 25, weeklyPercent: 90, + fiveHourResetAt: Date.now() + 3600000, weeklyResetAt: Date.now() + 86400000, +} }); + +test("ZCode uses shared bars with remaining balances and reset labels", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("75% remaining"); + expect(html).toContain("10% remaining"); + expect(html).toContain('aria-valuenow="75"'); + expect(html).toContain('--bar-scale:0.75'); + expect(html).toContain('quota-stacked-reset'); +}); + +test("current account Usage retains the native source through the shared renderer", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("75% remaining"); + expect(html).toContain("10% remaining"); +}); + +test("OpenAI keeps its existing consumption display and width", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("25% used"); + expect(html).toContain("90% used"); + expect(html).not.toContain("remaining"); + expect(html).toContain('--bar-scale:0.25'); +}); + +test("an exhausted native quota stays exhausted and missing quota has no progress bar", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("0% remaining"); + expect(html).toContain("Limit reached"); + const missing = renderToStaticMarkup(); + expect(missing).not.toContain('role="progressbar"'); +}); diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index ec651af43b..9b20c655eb 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1346,7 +1346,8 @@ "zhipu-bigmodel-responses-quota.test.ts": "providers", "zz-ci-api-usage-isolation.test.ts": "ci-workflows", "zz-ci-storage-policy-isolation.test.ts": "ci-workflows", - "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows" + "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", + "zcode-quota.test.ts": "providers" }, "migrated": [ "adapters", diff --git a/src/adapters/zcode/quota-bootstrap.cjs b/src/adapters/zcode/quota-bootstrap.cjs new file mode 100644 index 0000000000..597aa10e93 --- /dev/null +++ b/src/adapters/zcode/quota-bootstrap.cjs @@ -0,0 +1,37 @@ +// Runs only inside the quota sandbox. Native host and RPC modules are unmodified official ZCode. +// No HTTP implementation, quota reset or model inference here. All diagnostics stay off stdout. +const {EventEmitter}=require('node:events'); +const fs=require('node:fs'); +const parent=new EventEmitter(); parent.postMessage=()=>{}; process.parentPort=parent; +const output=process.stdout.write.bind(process.stdout); process.stdout.write=process.stderr.write.bind(process.stderr); const originalLog=value=>output(value+'\n'); +fs.mkdirSync(process.env.HOME+'/.zcode/v2',{recursive:true}); +const input=JSON.parse(fs.readFileSync('/desktop/config.json','utf8')); +if(process.argv[2]==='advanced') { + const raw=input.provider?.[process.argv[3]]; + const url=new URL(raw?.options?.baseURL); + if(url.protocol!=='https:' || url.hostname!=='api.z.ai' || url.username || url.password || !raw.options.apiKey) throw new Error('unsupported profile'); + fs.writeFileSync(process.env.HOME+'/.zcode/v2/config.json',JSON.stringify({provider:{'builtin:zai-coding-plan':{...raw,name:'Z.ai - Coding Plan',enabled:true}}}),{mode:0o600}); +} else { + fs.copyFileSync('/desktop/config.json',process.env.HOME+'/.zcode/v2/config.json'); + if(fs.existsSync('/desktop/credentials.json')) fs.copyFileSync('/desktop/credentials.json',process.env.HOME+'/.zcode/v2/credentials.json'); +} +setTimeout(()=>{originalLog(JSON.stringify({error:'timeout'}));process.exit(1)},40000); +(async()=>{ + const dir='/zcode/resources/app.asar/out/host'; + let classes; + for(const name of fs.readdirSync(dir).filter(n=>n.startsWith('chunk-')&&n.endsWith('.js'))){ + const source=fs.readFileSync(dir+'/'+name,'utf8');if(source.includes('"ChannelClient"')&&source.includes('"MessagePortProtocol"')){classes=Object.values(await import(dir+'/'+name));break;} + } + const Client=classes.find(v=>v?.name==='ChannelClient');const Protocol=classes.find(v=>v?.name==='MessagePortProtocol'); + const left=new EventEmitter(),right=new EventEmitter(); + for(const [a,b]of [[left,right],[right,left]]){a.postMessage=data=>queueMicrotask(()=>b.emit('message',{data,ports:[]}));a.start=()=>{};a.close=()=>a.emit('close');a.addEventListener=a.on.bind(a);a.removeEventListener=a.off.bind(a);} + const client=new Client(new Protocol(left)); + await import(dir+'/index.js'); + parent.emit('message',{data:{type:'init-local'},ports:[right]}); + const result=await client.getChannel('usage-stats').call('getEntitlementSnapshot',[{preferredProviderId:'builtin:zai-coding-plan',requirePreferredProvider:true,allowEnvApiKey:false,includeSubscription:false}]); + const limits=Array.isArray(result.quota?.limits)?result.quota.limits.slice(0,16).map(row=>({ + type: ['CREDIT_LIMIT','TOKENS_LIMIT','TIME_LIMIT'].includes(row.type)?row.type:'unknown', + ...Object.fromEntries(['unit','number','usage','currentValue','remaining','percentage','nextResetTime'].filter(k=>typeof row[k]==='number'&&Number.isFinite(row[k])).map(k=>[k,row[k]])), +})):[]; + originalLog(JSON.stringify({generatedAt:result.generatedAt,limits}));process.exit(0); +})().catch(()=>{originalLog(JSON.stringify({error:'failed'}));process.exit(1)}); diff --git a/src/adapters/zcode/quota.ts b/src/adapters/zcode/quota.ts new file mode 100644 index 0000000000..18038ffe92 --- /dev/null +++ b/src/adapters/zcode/quota.ts @@ -0,0 +1,124 @@ +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs"; +import { dirname, join, sep } from "node:path"; +import { homedir } from "node:os"; +import { fileURLToPath } from "node:url"; +import type { OcxProviderConfig } from "../../types"; +import type { ProviderQuota } from "../../providers/quota-types"; +import { desktopStatus, resolveDesktopRuntime } from "./desktop"; +import { loadZcodeSettings } from "./settings"; + +export interface QuotaContext { identity: string; runtimeRoot: string; config: string; credentials?: string; sourceProvider: string; managed: boolean } +const finite = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v); + +/** Translate consumption into the existing shared quota contract. Missing is never zero. */ +export function parseZcodeQuota(value: unknown, now = Date.now()): ProviderQuota | null { + if (!value || typeof value !== "object") return null; + const v = value as { generatedAt?: unknown; limits?: unknown }; + if (!finite(v.generatedAt) || v.generatedAt > now + 60_000 || now - v.generatedAt > 30 * 60_000 || !Array.isArray(v.limits)) return null; + const result: ProviderQuota = { updatedAt: v.generatedAt }; + const seen = new Set(); + for (const row of v.limits.slice(0, 16)) { + if (!row || typeof row !== "object" || !["CREDIT_LIMIT", "TOKENS_LIMIT"].includes(row.type)) continue; + const key = row.unit === 3 && row.number === 5 ? "fiveHour" : row.unit === 6 && row.number === 1 ? "weekly" : undefined; + if (!key) continue; + if (seen.has(key)) return null; // ambiguous windows are not interchangeable + seen.add(key); + let percent: number | undefined; + if (finite(row.usage) && row.usage > 0 && finite(row.remaining) && row.remaining >= 0 && row.remaining <= row.usage) { + percent = 100 * (1 - row.remaining / row.usage); + } else if (row.usage === undefined && row.remaining === undefined && finite(row.percentage) && row.percentage >= 0 && row.percentage <= 100) { + percent = row.percentage; + } + if (percent === undefined) continue; + result[`${key}Percent`] = percent; + if (finite(row.nextResetTime) && row.nextResetTime > 0 && row.nextResetTime <= 8.64e15) { + result[`${key}ResetAt`] = row.nextResetTime > 1e11 ? row.nextResetTime : row.nextResetTime * 1000; + } + } + return result.fiveHourPercent !== undefined || result.weeklyPercent !== undefined ? result : null; +} + +function context(provider: OcxProviderConfig): QuotaContext { + if (provider.adapter !== "zcode" || provider.authMode !== "local" || provider.disabled || process.platform !== "linux") throw new Error("unavailable"); + const settings = loadZcodeSettings(); + const managed = settings.desktopModels !== undefined; + const runtime = managed ? desktopStatus().runtime : process.env.OCX_ZCODE_DESKTOP_RUNTIME; + if (!runtime) throw new Error("unavailable"); + const runtimeRoot = dirname(dirname(dirname(resolveDesktopRuntime(runtime)))); + const config = managed ? join(homedir(), ".zcode/v2/config.json") : settings.settingsPath; + if (!lstatSync(config).isFile() || statSync(config).size > 4 * 1024 * 1024) throw new Error("unavailable"); + if (!managed && !realpathSync(config).startsWith(realpathSync(settings.home) + sep)) throw new Error("unavailable"); + const credentialFile = join(homedir(), ".zcode/v2/credentials.json"); + const credentials = managed && existsSync(credentialFile) ? credentialFile : undefined; + if (credentials && (!lstatSync(credentials).isFile() || statSync(credentials).size > 1024 * 1024)) throw new Error("unavailable"); + const sourceProvider = provider.defaultModel?.split("/")[0] || (managed ? "builtin:zai-coding-plan" : "zai"); + if (managed && sourceProvider !== "builtin:zai-coding-plan") throw new Error("unavailable"); + const executable = join(runtimeRoot, "zcode"); + if (!lstatSync(executable).isFile()) throw new Error("unavailable"); + const identity = createHash("sha256").update(JSON.stringify([settings.scope, runtimeRoot, sourceProvider, + ...[config, credentials, executable, join(runtimeRoot, "resources/app.asar")].filter(Boolean).map(p => { + const st = statSync(p!); return [p, st.dev, st.ino, st.size, st.mtimeMs, st.ctimeMs]; + })])).digest("hex"); + return { identity, runtimeRoot, config, credentials, sourceProvider, managed }; +} + +export function zcodeQuotaIdentity(provider: OcxProviderConfig): string { + try { return context(provider).identity; } catch { return "unavailable"; } +} + +function command(c: QuotaContext): string[] { + const args = ["--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--ro-bind", "/usr", "/usr"]; + for (const p of ["/bin", "/lib", "/lib64", "/sbin"]) if (existsSync(p)) { + args.push(...(lstatSync(p).isSymbolicLink() ? ["--symlink", readlinkSync(p), p] : ["--ro-bind", p, p])); + } + args.push("--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/etc"); + for (const p of ["/etc/passwd", "/etc/resolv.conf", "/etc/ssl", "/etc/pki"]) if (existsSync(p)) args.push("--ro-bind", p, p); + args.push("--ro-bind", c.runtimeRoot, "/zcode", "--tmpfs", homedir(), "--ro-bind", c.config, "/desktop/config.json"); + if (c.credentials) args.push("--ro-bind", c.credentials, "/desktop/credentials.json"); + args.push("--ro-bind", fileURLToPath(new URL("./quota-bootstrap.cjs", import.meta.url)), "/bridge.cjs", + "--clearenv", "--setenv", "HOME", homedir(), "--setenv", "PATH", "/usr/bin:/bin", "--setenv", "ELECTRON_RUN_AS_NODE", "1", + "--chdir", homedir(), "/zcode/zcode", "/bridge.cjs", c.managed ? "desktop" : "advanced", c.sourceProvider); + return args; +} + +/** One short-lived official host, without a writable user workspace or persistent credentials. */ +async function probe(c: QuotaContext): Promise { + const bwrap = Bun.which("bwrap"); + if (!bwrap) return null; + return new Promise(resolve => { + const child = spawn(bwrap, command(c), { stdio: ["ignore", "pipe", "pipe"], env: { PATH: "/usr/bin:/bin" } }); + let output = ""; + let invalid = false; + const timer = setTimeout(() => { invalid = true; child.kill("SIGKILL"); }, 45_000); + child.stderr.resume(); + child.stdout.on("data", chunk => { + if (invalid) return; + if (Buffer.byteLength(output) + chunk.length > 32_000) { invalid = true; child.kill("SIGKILL"); return; } + output += chunk.toString("utf8"); + }); + child.once("error", () => { clearTimeout(timer); resolve(null); }); + child.once("close", code => { + clearTimeout(timer); + if (code || invalid) return resolve(null); + try { resolve(parseZcodeQuota(JSON.parse(output))); } catch { resolve(null); } + }); + }); +} + +let inflight: { identity: string; promise: Promise } | undefined; +export async function readZcodeQuota(provider: OcxProviderConfig, deps: { context?: typeof context; probe?: typeof probe } = {}): Promise<{ identity: string; quota: ProviderQuota } | null> { + let c: QuotaContext; + try { c = (deps.context ?? context)(provider); } catch { return null; } + // Never accumulate native hosts during repeated dashboard refreshes. + if (inflight && inflight.identity !== c.identity) return null; + if (!inflight) { + const request = { identity: c.identity, promise: (deps.probe ?? probe)(c).catch(() => null) }; + inflight = request; + void request.promise.finally(() => { if (inflight === request) inflight = undefined; }); + } + const quota = await inflight.promise; + try { return quota && (deps.context ?? context)(provider).identity === c.identity ? { identity: c.identity, quota } : null; } + catch { return null; } +} diff --git a/src/providers/quota-routing-cache.ts b/src/providers/quota-routing-cache.ts index 065d7338ca..d79849cd2e 100644 --- a/src/providers/quota-routing-cache.ts +++ b/src/providers/quota-routing-cache.ts @@ -9,6 +9,8 @@ export function clearCachedProviderQuotas(): void { export function replaceCachedProviderQuotas(reports: ProviderQuotaReport[]): void { quotaCache.clear(); for (const report of reports) { + // Native Desktop account snapshots are display-only, not an automatic routing policy. + if (report.source === "zcode-desktop") continue; quotaCache.set(report.provider, report.quota); } } diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 79e6a6bf91..2bfb283ff9 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -1,3 +1,4 @@ +import { readZcodeQuota, zcodeQuotaIdentity } from "../adapters/zcode/quota"; import { createHash } from "node:crypto"; import { effectiveCodexAuthAccountId, @@ -177,7 +178,7 @@ function cacheKey(config: OcxConfig): string { ? resolveProviderApiKey(provider.apiKey)?.trim() : undefined; const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none"; - return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`; + return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}${provider.adapter === "zcode" ? `:${zcodeQuotaIdentity(provider)}` : ""}`; }) .sort() .join("|"); @@ -2944,6 +2945,14 @@ async function maybeFetchProviderQuota( ): Promise { if (provider.disabled === true) return null; try { + if (provider.adapter === "zcode") { + const result = await readZcodeQuota(provider); + if (!result) return AUTHORITATIVE_EMPTY_QUOTA; + const report: ProviderQuotaReport = { provider: name, label: "ZCode", source: "zcode-desktop", + quota: result.quota, updatedAt: result.quota.updatedAt, reverseEngineered: true }; + accountReportCurrent.set(report, () => zcodeQuotaIdentity(provider) === result.identity); + return report; + } if (isBuiltInChatGptForwardProvider(name, provider)) { return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot); } @@ -2998,6 +3007,7 @@ let pendingProviderObservation: Promise = Promise.resolve(); * mirrors that discriminator instead of inventing a second notion of identity. */ function providerObservationAccountKey(provider: string, config: OcxConfig): string { + if (config.providers[provider]?.adapter === "zcode") return `${provider}\u0000zcode:${zcodeQuotaIdentity(config.providers[provider])}`; const oauthAccountId = getAccountSet(provider)?.activeAccountId; if (oauthAccountId !== undefined) return `${provider}\u0000${oauthAccountId}`; const providerConfig = config.providers[provider]; diff --git a/structure/10_adapter-registry.md b/structure/10_adapter-registry.md index 55a2fb9a19..0de0d1df7d 100644 --- a/structure/10_adapter-registry.md +++ b/structure/10_adapter-registry.md @@ -29,6 +29,10 @@ failover candidates. Launcher authority comes from either the operator environme GUI-consented Desktop connection. Data-plane requests and ordinary provider configuration cannot set command/workspace paths. The managed Desktop bootstrap keeps credential-bearing runtime descriptors inside its OS sandbox; the parent sees public model identities only. +Subscription quota is separately read through the official Desktop host entitlement RPC in a +short-lived tmpfs sandbox. Only numeric quota windows leave that process. An advanced launcher +requires explicit `OCX_ZCODE_DESKTOP_RUNTIME` authority and reuses its own isolated model key; +quota discovery must not silently import another Desktop account or affect routing policy. Adding a production adapter requires: diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index f84e0a501b..901e58e8c7 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1181,5 +1181,6 @@ "zhipu-bigmodel-responses-quota.test.ts": "providers", "zz-ci-api-usage-isolation.test.ts": "ci-workflows", "zz-ci-storage-policy-isolation.test.ts": "ci-workflows", - "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows" + "zz-pr-coderabbit-readiness-revalidation.test.ts": "ci-workflows", + "zcode-quota.test.ts": "providers" } diff --git a/tests/providers/zcode-quota.test.ts b/tests/providers/zcode-quota.test.ts new file mode 100644 index 0000000000..08f07be4ce --- /dev/null +++ b/tests/providers/zcode-quota.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "bun:test"; +import { parseZcodeQuota, readZcodeQuota, type QuotaContext } from "../../src/adapters/zcode/quota"; +import { getCachedProviderQuota, replaceCachedProviderQuotas } from "../../src/providers/quota-routing-cache"; +import { readFileSync } from "node:fs"; +import { repoPath } from "../helpers/repo-root"; + +const now = 1_789_112_000_000; +const window = (unit = 3, number = 5) => ({ type: "CREDIT_LIMIT", unit, number, usage: 1000, remaining: 750, percentage: 25, nextResetTime: now + 3600000 }); +const snapshot = (limits: unknown[]) => ({ generatedAt: now, limits }); +const provider = { adapter: "zcode", baseUrl: "https://zcode.z.ai", authMode: "local" as const }; +const context = (identity = "account-a"): QuotaContext => ({ identity, runtimeRoot: "/official", config: "/isolated/config.json", managed: true, sourceProvider: "builtin:zai-coding-plan" }); + +describe("ZCode native subscription quota", () => { + test("maps real remaining credits and resets into the shared OpenAI quota contract", () => { + expect(parseZcodeQuota(snapshot([window(), { ...window(6, 1), remaining: 100 }]), now)).toEqual({ + updatedAt: now, fiveHourPercent: 25, fiveHourResetAt: now + 3600000, + weeklyPercent: 90, weeklyResetAt: now + 3600000, + }); + expect(parseZcodeQuota(snapshot([{ ...window(), remaining: 997.5, percentage: 1 }]), now)?.fiveHourPercent).toBeCloseTo(0.25); + }); + test("zero balance is exhausted; missing, impossible or stale data never becomes full balance", () => { + expect(parseZcodeQuota(snapshot([{ ...window(), remaining: 0 }]), now)?.fiveHourPercent).toBe(100); + for (const limits of [[], [null], [{ ...window(), remaining: -1 }], [{ ...window(), usage: 0 }], [{ ...window(), remaining: 1001 }], [{ ...window(), remaining: NaN }], [window(), window()]]) { + expect(parseZcodeQuota(snapshot(limits), now)).toBeNull(); + } + expect(parseZcodeQuota(snapshot([window()]), now + 31 * 60_000)).toBeNull(); + expect(parseZcodeQuota({ generatedAt: now + 120_000, limits: [window()] }, now)).toBeNull(); + }); + test("does not guess unnamed windows or invent missing weekly limits", () => { + expect(parseZcodeQuota(snapshot([{ ...window(), unit: 7 }, { type: "TIME_LIMIT", remaining: 100 }]), now)).toBeNull(); + expect(parseZcodeQuota(snapshot([window()]), now)?.weeklyPercent).toBeUndefined(); + expect(parseZcodeQuota(snapshot([{ type: "TOKENS_LIMIT", unit: 6, number: 1, percentage: 40 }]), now)?.weeklyPercent).toBe(40); + }); + test("coalesces native reads and discards snapshots after account change", async () => { + let calls = 0; let identity = "account-a"; + let finish!: (value: ReturnType) => void; + const deps = { context: () => context(identity), probe: async () => { calls++; return new Promise>(resolve => { finish = resolve; }); } }; + const first = readZcodeQuota(provider, deps); const second = readZcodeQuota(provider, deps); + expect(calls).toBe(1); + identity = "account-b"; + expect(await readZcodeQuota(provider, deps)).toBeNull(); + finish(parseZcodeQuota(snapshot([window()]), now)); + expect(await first).toBeNull(); expect(await second).toBeNull(); + }); + test("unavailable runtime has no direct API fallback", async () => { + expect(await readZcodeQuota(provider, { context: () => { throw new Error("not configured"); } })).toBeNull(); + const bootstrap = readFileSync(repoPath("src/adapters/zcode/quota-bootstrap.cjs"), "utf8"); + expect(bootstrap).toContain("getEntitlementSnapshot"); + expect(bootstrap).not.toMatch(/\bfetch\s*\(|https\.request|useCodingPlanReset|session\/send/); + }); + test("display snapshots do not change automatic routing policy", () => { + replaceCachedProviderQuotas([{ provider: "zcode", label: "ZCode", source: "zcode-desktop", updatedAt: now, quota: { updatedAt: now, fiveHourPercent: 100 } }]); + expect(getCachedProviderQuota("zcode", now)).toBeNull(); + }); +}); From bb2cef3877bc6f160bf04a27535251b9738b6edc Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 10:18:02 -0300 Subject: [PATCH 04/43] fix(zcode): select and validate compatible Desktop Node --- .../src/content/docs/guides/zcode-agent.md | 24 ++++++++ gui/src/components/ZcodeDesktopPane.tsx | 1 + gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/tests/zcode-desktop-pane.test.tsx | 14 +++++ src/adapters/zcode/desktop-node.ts | 56 +++++++++++++++++++ src/adapters/zcode/desktop.ts | 5 +- tests/providers/zcode-desktop.test.ts | 29 +++++++++- 15 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/adapters/zcode/desktop-node.ts diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 7f8c30ed7c..3a19c341d4 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -249,3 +249,27 @@ These bars are informational and do not enable quota-based automatic rerouting. The native session app-server's token accounting remains unavailable; subscription quota is a distinct measurement. See [ZCode Usage Stats](https://zcode.z.ai/en/docs/usage-stats) for the distinction between App Usage and Coding Plan statistics. + +### Desktop Node compatibility + +Managed Desktop setup requires **Node.js 24.0.0 or newer**, with working +node:sqlite, on the **OpenCodex process PATH**. This is the bridge's supported +floor, not a claim that every earlier Node version is incompatible with every +ZCode release. Selection checks each absolute PATH directory in order and skips +incompatible Node binaries, including an older npm/nvm prefix ahead of a modern +installation. Relative and empty PATH entries are not searched. It does not +assume that the Node which launched OpenCodex is the Node resolved by Bun. + +If no suitable executable is found, setup reports node_incompatible (or +node_missing when none exists) before reading the Desktop profile or starting +the app server. Add a compatible Node directory to the PATH used by your terminal +**or systemd service**, restart that process, and choose **Detect again**. +No global Node replacement is required. Capability checks discard stderr and +inherit neither credentials nor NODE_OPTIONS. Connection still requires explicit +consent and a successful official workspace/readState protocol check; the +bootstrap's model catalog alone does not prove readiness. + +The quota reader launches Desktop's own Electron executable with +ELECTRON_RUN_AS_NODE, not the external Node selected from PATH. Managed account +settings still pass the same Desktop prerequisites; advanced quota reads do not +use this external Node selection. diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index d93f1e002c..2e27dda2cc 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -58,6 +58,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: const problem = error === "desktop_missing" ? t("zcodeDesktop.missing") : error === "sandbox_missing" ? t("zcodeDesktop.sandboxMissing") : error === "node_missing" ? t("zcodeDesktop.nodeMissing") + : error === "node_incompatible" ? t("zcodeDesktop.nodeIncompatible") : error === "platform_unsupported" ? t("zcodeDesktop.platformUnsupported") : error === "profile_missing" || error === "models_missing" ? t("zcodeDesktop.loginNeeded") : error === "workspace_invalid" ? t("zcodeDesktop.workspaceInvalid") diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 2b8e920661..f4443ddf5b 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -40,6 +40,7 @@ export const de: Record = { "zcodeDesktop.missing": "Öffnen Sie ZCode Desktop auf dem Proxy-Computer und starten Sie die Erkennung erneut, oder wählen Sie den Installationsordner.", "zcodeDesktop.sandboxMissing": "Installieren Sie bubblewrap mit dem Linux-Paketmanager für die Isolation und starten Sie die Erkennung erneut.", "zcodeDesktop.nodeMissing": "Installieren Sie eine mit ZCode kompatible Node.js-Version und starten Sie die Erkennung erneut.", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop benötigt Node.js 24 oder neuer mit SQLite-Unterstützung. Fügen Sie eine kompatible Version zum PATH von OpenCodex hinzu, starten Sie den Dienst oder das Terminal neu und wiederholen Sie die Erkennung.", "zcodeDesktop.platformUnsupported": "Die verwaltete Desktop-Verbindung unterstützt derzeit nur Linux. Andere Plattformen benötigen den erweiterten isolierten Starter.", "zcodeDesktop.loginNeeded": "Melden Sie sich in ZCode Desktop an und richten Sie ein Z.AI-Modell ein. Verbinden Sie danach erneut.", "zcodeDesktop.workspaceInvalid": "Wählen Sie einen vorhandenen Projektordner, nicht den Home-, System- oder Zugangsdatenordner.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index bc8cb1a789..d0b2a9a8ba 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -41,6 +41,7 @@ export const en = { "zcodeDesktop.missing": "Open ZCode Desktop on the proxy computer, then detect again. You can also select its installed or extracted application folder.", "zcodeDesktop.sandboxMissing": "Bubblewrap is required for isolation. Install bubblewrap using your Linux package manager, then detect again.", "zcodeDesktop.nodeMissing": "Node.js is required by the ZCode runtime. Install a compatible Node.js version, then detect again.", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop requires Node.js 24 or newer with SQLite support. Add a compatible Node.js to the PATH used by OpenCodex, restart its service or terminal, then detect again.", "zcodeDesktop.platformUnsupported": "Managed Desktop connection currently supports Linux only. Other platforms can use the advanced isolated-launcher setup.", "zcodeDesktop.loginNeeded": "Sign in and configure a Z.AI model in ZCode Desktop, then detect and connect again.", "zcodeDesktop.workspaceInvalid": "Choose an existing project folder, not your home, a system folder or a credentials/configuration directory.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index cdaeacef11..73fc77b28a 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -39,6 +39,7 @@ export const fr: Record = { "zcodeDesktop.missing": "Ouvrez ZCode Desktop sur l’ordinateur du proxy, puis relancez la détection, ou indiquez son dossier installé ou extrait.", "zcodeDesktop.sandboxMissing": "Installez bubblewrap avec le gestionnaire de paquets Linux pour activer l’isolation, puis relancez la détection.", "zcodeDesktop.nodeMissing": "Installez une version de Node.js compatible avec ZCode, puis relancez la détection.", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop nécessite Node.js 24 ou ultérieur avec SQLite. Ajoutez une version compatible au PATH utilisé par OpenCodex, redémarrez le service ou le terminal, puis relancez la détection.", "zcodeDesktop.platformUnsupported": "La connexion Desktop gérée prend actuellement en charge Linux uniquement. Les autres plateformes nécessitent le lanceur isolé avancé.", "zcodeDesktop.loginNeeded": "Connectez-vous et configurez un modèle Z.AI dans ZCode Desktop, puis reconnectez le fournisseur.", "zcodeDesktop.workspaceInvalid": "Choisissez un dossier de projet existant, pas le dossier personnel, système ou des identifiants.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 1a40378f06..b7e31ce530 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -39,6 +39,7 @@ export const ja: Record = { "zcodeDesktop.missing": "プロキシのコンピューターで ZCode Desktop を開いて再検出するか、インストールまたは展開先を選択してください。", "zcodeDesktop.sandboxMissing": "隔離には Bubblewrap が必要です。Linux のパッケージマネージャーでインストールして再検出してください。", "zcodeDesktop.nodeMissing": "ZCode 対応の Node.js をインストールして再検出してください。", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop には SQLite 対応の Node.js 24 以降が必要です。OpenCodex が使う PATH に対応版を追加し、サービスまたはターミナルを再起動して再検出してください。", "zcodeDesktop.platformUnsupported": "管理された Desktop 接続は現在 Linux のみ対応です。他の環境では詳細な隔離ランチャー設定を使用してください。", "zcodeDesktop.loginNeeded": "ZCode Desktop にサインインして Z.AI モデルを設定し、再接続してください。", "zcodeDesktop.workspaceInvalid": "ホーム、システム、認証情報のフォルダーではなく、既存のプロジェクトフォルダーを選択してください。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 9b0c8cab43..9dbf448f57 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -39,6 +39,7 @@ export const ko: Record = { "zcodeDesktop.missing": "프록시 컴퓨터에서 ZCode Desktop을 열고 다시 감지하거나 설치 또는 압축 해제 폴더를 선택하세요.", "zcodeDesktop.sandboxMissing": "격리를 위해 Linux 패키지 관리자로 bubblewrap을 설치한 뒤 다시 감지하세요.", "zcodeDesktop.nodeMissing": "ZCode와 호환되는 Node.js를 설치한 뒤 다시 감지하세요.", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop에는 SQLite를 지원하는 Node.js 24 이상이 필요합니다. OpenCodex의 PATH에 호환 버전을 추가하고 서비스 또는 터미널을 다시 시작한 후 다시 감지하세요.", "zcodeDesktop.platformUnsupported": "관리형 Desktop 연결은 현재 Linux만 지원합니다. 다른 플랫폼에서는 고급 격리 실행기 설정을 사용하세요.", "zcodeDesktop.loginNeeded": "ZCode Desktop에서 로그인하고 Z.AI 모델을 설정한 뒤 다시 연결하세요.", "zcodeDesktop.workspaceInvalid": "홈, 시스템 또는 인증 정보 폴더가 아닌 기존 프로젝트 폴더를 선택하세요.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index f16183cb7f..58b7f12c3c 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -39,6 +39,7 @@ export const ru: Record = { "zcodeDesktop.missing": "Откройте ZCode Desktop на компьютере прокси и повторите поиск либо выберите папку установки или распаковки.", "zcodeDesktop.sandboxMissing": "Для изоляции установите bubblewrap через менеджер пакетов Linux и повторите поиск.", "zcodeDesktop.nodeMissing": "Установите совместимую с ZCode версию Node.js и повторите поиск.", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop требует Node.js 24 или новее с SQLite. Добавьте совместимую версию в PATH OpenCodex, перезапустите службу или терминал и повторите обнаружение.", "zcodeDesktop.platformUnsupported": "Управляемое подключение Desktop пока поддерживает только Linux. Для других платформ используйте расширенную настройку изолированного запуска.", "zcodeDesktop.loginNeeded": "Войдите и настройте модель Z.AI в ZCode Desktop, затем подключитесь снова.", "zcodeDesktop.workspaceInvalid": "Выберите существующую папку проекта, а не домашнюю, системную или папку с учётными данными.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index e0dd89fae9..fe3a6d7ee3 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -40,6 +40,7 @@ export const tr: Record = { "zcodeDesktop.missing": "Proxy bilgisayarında ZCode Desktop açıp yeniden algılayın veya kurulum ya da çıkarılmış uygulama klasörünü seçin.", "zcodeDesktop.sandboxMissing": "Yalıtım için Linux paket yöneticisiyle bubblewrap kurup yeniden algılayın.", "zcodeDesktop.nodeMissing": "ZCode ile uyumlu Node.js sürümünü kurup yeniden algılayın.", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop, SQLite destekli Node.js 24 veya üzerini gerektirir. Uyumlu sürümü OpenCodex PATH ortamına ekleyin, hizmeti veya terminali yeniden başlatıp tekrar algılayın.", "zcodeDesktop.platformUnsupported": "Yönetilen Desktop bağlantısı şu anda yalnızca Linux destekler. Diğer platformlarda gelişmiş yalıtılmış başlatıcıyı kullanın.", "zcodeDesktop.loginNeeded": "ZCode Desktop içinde oturum açıp bir Z.AI modeli yapılandırın, ardından yeniden bağlanın.", "zcodeDesktop.workspaceInvalid": "Ev, sistem veya kimlik bilgileri klasörü yerine mevcut bir proje klasörü seçin.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index d8fd6aacac..29da9fcaea 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -37,6 +37,7 @@ export const zhTW: Record = { "zcodeDesktop.missing": "請在代理所在電腦開啟 ZCode Desktop 後重新偵測,或選取安裝或解壓縮目錄。", "zcodeDesktop.sandboxMissing": "隔離需要 Bubblewrap。請透過 Linux 套件管理員安裝 bubblewrap 後重新偵測。", "zcodeDesktop.nodeMissing": "請安裝與 ZCode 相容的 Node.js 版本後重新偵測。", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop 需要支援 SQLite 的 Node.js 24 或更新版本。請將相容版本加入 OpenCodex 使用的 PATH,重新啟動服務或終端機後再次偵測。", "zcodeDesktop.platformUnsupported": "受管理的 Desktop 連接目前僅支援 Linux。其他平台可使用進階隔離啟動器設定。", "zcodeDesktop.loginNeeded": "請在 ZCode Desktop 登入並設定 Z.AI 模型,然後重新連接。", "zcodeDesktop.workspaceInvalid": "請選取現有專案目錄,而非家目錄、系統目錄或憑證設定目錄。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index d61f28cbb2..ebc1ab87a1 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -39,6 +39,7 @@ export const zh: Record = { "zcodeDesktop.missing": "请在代理所在计算机上打开 ZCode Desktop 后重新检测,或选择其安装或解压目录。", "zcodeDesktop.sandboxMissing": "隔离需要 Bubblewrap。请通过 Linux 包管理器安装 bubblewrap 后重新检测。", "zcodeDesktop.nodeMissing": "请安装与 ZCode 兼容的 Node.js 版本后重新检测。", + "zcodeDesktop.nodeIncompatible": "ZCode Desktop 需要支持 SQLite 的 Node.js 24 或更新版本。请将兼容版本加入 OpenCodex 使用的 PATH,重启服务或终端后重新检测。", "zcodeDesktop.platformUnsupported": "托管 Desktop 连接目前仅支持 Linux。其他平台可以使用高级隔离启动器配置。", "zcodeDesktop.loginNeeded": "请在 ZCode Desktop 中登录并配置 Z.AI 模型,然后重新连接。", "zcodeDesktop.workspaceInvalid": "请选择现有项目目录,而非主目录、系统目录或凭据配置目录。", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 555a09fa40..91ad5e487b 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -93,3 +93,17 @@ test("one-request test is a separate explicit quota-spending action", async () = expect(requests.find(r => r.path.endsWith("/test"))?.body).toEqual({ model: "builtin:zai/model", consent: true }); expect(host.textContent).toContain("ZCode answered successfully."); }); + +test("incompatible Node preflight shows safe actionable guidance without connecting", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async () => Response.json({ + connected: false, issue: "node_incompatible", runtimes: [], runtime: "", workspace: "/project", models: [], + }) }); + await mountPane(); + const alert = host.querySelector('[role="alert"]')!; + expect(alert.textContent).toContain("Node.js 24"); + expect(alert.textContent).toContain("PATH"); + expect(alert.textContent).toContain("restart"); + expect(alert.textContent).not.toContain("runtime_failed"); + expect(host.querySelector('input[type="checkbox"]')!.checked).toBe(false); + expect(button("Connect Desktop").disabled).toBe(true); +}); diff --git a/src/adapters/zcode/desktop-node.ts b/src/adapters/zcode/desktop-node.ts new file mode 100644 index 0000000000..af000905c1 --- /dev/null +++ b/src/adapters/zcode/desktop-node.ts @@ -0,0 +1,56 @@ +import { spawnSync } from "node:child_process"; +import { accessSync, constants, realpathSync, statSync } from "node:fs"; +import { delimiter, isAbsolute, join } from "node:path"; + +// Bridge support floor, not a claim about every historical vendor release. +// Verify the complete official workspace/readState protocol at this floor as well. +export const DESKTOP_NODE_MIN_MAJOR = 24; +const checks = new Map(); +const script = ` +try { + if (process.release.name !== 'node' || process.versions.bun || + Number(process.versions.node.split('.')[0]) < ${DESKTOP_NODE_MIN_MAJOR} || + typeof Object.hasOwn !== 'function' || typeof fetch !== 'function') process.exit(1); + const db = new (require('node:sqlite').DatabaseSync)(':memory:'); + if (db.prepare('SELECT 1 AS value').get().value !== 1) process.exit(1); + db.close(); + process.stdout.write('compatible'); +} catch { process.exit(1); } +`; + +/** Check each absolute PATH entry, rather than trusting the first node or Bun's launcher. */ +export function resolveDesktopNode(path = process.env.PATH ?? ""): string { + let found = false; + const seen = new Set(); + const deadline = Date.now() + 3_000; + for (const directory of [...new Set(path.split(delimiter))].filter(isAbsolute).slice(0, 64)) { + let node: string; + let key: string; + try { + node = realpathSync(join(directory, "node")); + if (seen.has(node)) continue; + seen.add(node); + accessSync(node, constants.X_OK); + const st = statSync(node); + if (!st.isFile()) continue; + found = true; + key = JSON.stringify([node, st.dev, st.ino, st.size, st.mtimeMs, st.ctimeMs]); + } catch { continue; } + let compatible = checks.get(key); + if (compatible === undefined) { + const remaining = deadline - Date.now(); + if (remaining <= 0) break; + // No inherited NODE_OPTIONS, preload hooks, credentials or vendor stderr. + const result = spawnSync(node, ["--no-warnings", "-e", script], { + env: { PATH: "/usr/bin:/bin" }, cwd: "/", encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], timeout: Math.min(1_000, remaining), + killSignal: "SIGKILL", maxBuffer: 128, + }); + compatible = !result.error && result.status === 0 && result.stdout === "compatible"; + if (checks.size >= 64) checks.clear(); + checks.set(key, compatible); + } + if (compatible) return node; + } + throw new Error(found ? "node_incompatible" : "node_missing"); +} diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index d1e0947363..10ae976e89 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; import { getConfigDir } from "../../config/paths"; import { closeZcodeDesktopClients, ZcodeClient } from "./client"; import type { ZcodeSettings } from "./settings"; +import { resolveDesktopNode } from "./desktop-node"; export interface DesktopModel { id: string; providerId: string; modelId: string; label: string; contextWindow?: number } interface Connection { @@ -104,7 +105,9 @@ export function desktopFolders(path?: string) { function prerequisites(): { bwrap: string; node: string } { if (process.platform !== "linux") return fail("platform_unsupported"); const bwrap = Bun.which("bwrap"); if (!bwrap) return fail("sandbox_missing"); - const node = Bun.which("node"); if (!node) return fail("node_missing"); + let node: string; + try { node = resolveDesktopNode(); } + catch (error) { return fail(error instanceof Error && error.message === "node_missing" ? "node_missing" : "node_incompatible"); } return { bwrap: realpathSync(bwrap), node: realpathSync(node) }; } diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 0efa48a88a..379b2a7ffa 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -3,9 +3,11 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, wr import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { disconnectDesktop, loadDesktopSettings, resolveDesktopRuntime, validateDesktopWorkspace } from "../../src/adapters/zcode/desktop"; +import { desktopStatus, disconnectDesktop, loadDesktopSettings, resolveDesktopRuntime, validateDesktopWorkspace } from "../../src/adapters/zcode/desktop"; import { readZcodeModels } from "../../src/adapters/zcode/settings"; +import { resolveDesktopNode } from "../../src/adapters/zcode/desktop-node"; + const { normalizeDesktopConfig, desktopModelCatalog } = createRequire(import.meta.url)("../../src/adapters/zcode/desktop-bootstrap.cjs"); let root: string; let previousHome: string | undefined; @@ -72,3 +74,28 @@ describe("managed ZCode Desktop", () => { expect(result[0]?.runtimeModel).toEqual({}); }); }); + +test("Node selection skips an incompatible nvm prefix and fails safely", () => { + const oldPath = process.env.PATH; + const old = join(root, "old/bin"), modern = join(root, "modern/bin"); + mkdirSync(old, { recursive: true }); mkdirSync(modern, { recursive: true }); + // Executable fixtures exercise selection independently of host Node installations. + writeFileSync(join(old, "node"), "#!/bin/sh\necho private-vendor-diagnostic >&2\nexit 1\n", { mode: 0o755 }); + writeFileSync(join(modern, "node"), "#!/bin/sh\nprintf compatible\n", { mode: 0o755 }); + try { + expect(resolveDesktopNode(old + ":" + modern)).toBe(join(modern, "node")); + expect(() => resolveDesktopNode(old)).toThrow("node_incompatible"); + expect(() => resolveDesktopNode(join(root, "absent"))).toThrow("node_missing"); + expect(() => resolveDesktopNode(".")).toThrow("node_missing"); + const bwrap = Bun.which("bwrap"); + if (bwrap) { + symlinkSync(bwrap, join(old, "bwrap")); + process.env.PATH = old; + expect(desktopStatus().issue).toBe("node_incompatible"); + } + writeFileSync(join(modern, "node"), "#!/bin/sh\nexit 1\n", { mode: 0o755 }); + expect(() => resolveDesktopNode(modern)).toThrow("node_incompatible"); + } finally { + if (oldPath === undefined) delete process.env.PATH; else process.env.PATH = oldPath; + } +}); From 7df545f2c04d884622386402d000b52727c0ce43 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 10:47:43 -0300 Subject: [PATCH 05/43] fix(zcode): complete Desktop provider and catalog activation --- .../src/content/docs/guides/zcode-agent.md | 23 +++- gui/src/components/AddProviderModal.tsx | 2 +- gui/src/components/ZcodeDesktopPane.tsx | 28 +++-- gui/src/i18n/de.ts | 12 ++- gui/src/i18n/en.ts | 12 ++- gui/src/i18n/fr.ts | 12 ++- gui/src/i18n/ja.ts | 12 ++- gui/src/i18n/ko.ts | 12 ++- gui/src/i18n/ru.ts | 12 ++- gui/src/i18n/tr.ts | 12 ++- gui/src/i18n/zh-TW.ts | 12 ++- gui/src/i18n/zh.ts | 12 ++- gui/src/pages/Providers.tsx | 3 +- gui/tests/zcode-desktop-pane.test.tsx | 25 ++++- .../management/zcode-desktop-activation.ts | 70 ++++++++++++ src/server/management/zcode-desktop-routes.ts | 20 ++-- tests/providers/zcode-desktop-routes.test.ts | 100 ++++++++++++++++-- 17 files changed, 305 insertions(+), 74 deletions(-) create mode 100644 src/server/management/zcode-desktop-activation.ts diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 3a19c341d4..dc547b3158 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -35,12 +35,12 @@ or subscription terms; those remain the vendor's policy. 4. Choose a **workspace folder** with the folder browser. The default is a private disposable folder. Do not select your entire home or a credentials/configuration directory. 5. Review the native file/command execution notice, check the consent box and click - **Connect Desktop**. OpenCodex constructs the Bubblewrap launcher and persists the connection; + **Connect Desktop**. OpenCodex constructs the Bubblewrap launcher, verifies the protocol, enables the ZCode provider and updates the Codex catalog; no environment variables, API-key entry, token import or separate CLI login is required. 6. Optionally select a model and click **Test with one request**. This sends one brief prompt through the official ZCode runtime and consumes account quota. Connecting alone checks the local protocol and catalog, **not** account entitlement or inference. -7. Click **Use this provider**, then select a `zcode/builtin:zai-coding-plan/...` model in your +7. The provider is now available immediately. Select a ZCode model in your calling client. Only configured, enabled built-in Z.AI profiles are exposed by this workflow; custom Desktop providers and routes back to OpenCodex are not imported. @@ -273,3 +273,22 @@ The quota reader launches Desktop's own Electron executable with ELECTRON_RUN_AS_NODE, not the external Node selected from PATH. Managed account settings still pass the same Desktop prerequisites; advanced quota reads do not use this external Node selection. + + +### One-step activation and recovery + +**Connect Desktop** includes protocol verification, idempotent provider registration +and the normal Codex catalog convergence. No separate **Use this provider** action is +required. It does not change default provider/model selections or execute inference. +Reconnection enables the existing ZCode provider without replacing custom options, +aliases, model filters or pricing; conflicting provider registrations are not overwritten. + +Success is shown only when the provider is enabled and the connected models are visible +in the actual Codex catalog. A protocol-only connection, failed config write or failed, +skipped or incomplete catalog refresh remains a partial state. **Retry activation** +finishes registration/catalog publication without sending a prompt or reconnecting the +runtime. Review the activation consent again before retrying. Existing visibility filters +are preserved; if they hide the connected models, the catalog state remains incomplete. + +If a running Codex instance does not pick up the models, restart it after its tasks +finish. OpenCodex does not terminate or restart Codex processes automatically. diff --git a/gui/src/components/AddProviderModal.tsx b/gui/src/components/AddProviderModal.tsx index d14bae16fe..bbfe16bc9d 100644 --- a/gui/src/components/AddProviderModal.tsx +++ b/gui/src/components/AddProviderModal.tsx @@ -278,7 +278,7 @@ export default function AddProviderModal({ /> ) : form && ( preset.id === "zcode" ? ( - { void submit(); }} onBack={() => dispatch({ type: "back" })} /> + dispatch({ type: "back" })} /> ) : preset.auth === "oauth" && form.authMode === "oauth" ? ( ; } interface Folders { current: string; parent: string | null; folders: Array<{ name: string; path: string }> } export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: parentError }: { - apiBase: string; onConnected?: () => void; onBack?: () => void; error?: string; + apiBase: string; onConnected?: (name: string) => void; onBack?: () => void; error?: string; }) { const t = useT(); const [status, setStatus] = useState(null); @@ -22,7 +23,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: const [folders, setFolders] = useState(null); const applyStatus = (next: Status) => { setStatus(next); setRuntime(next.runtime); setWorkspace(next.workspace); - setModel(next.models[0]?.id ?? ""); setError(next.issue ?? ""); + setModel(next.models[0]?.id ?? ""); setError(next.error ?? next.issue ?? ""); }; useEffect(() => { const controller = new AbortController(); @@ -34,17 +35,20 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: return () => controller.abort(); }, [apiBase]); - const perform = async (action: "refresh" | "connect" | "disconnect" | "test") => { + const perform = async (action: "refresh" | "connect" | "activate" | "disconnect" | "test") => { setBusy(true); setError(""); setTested(false); try { const response = await fetch(`${apiBase}/api/zcode-desktop${action === "refresh" ? "" : `/${action}`}`, action === "refresh" ? {} : { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify(action === "connect" ? { runtime, workspace, consent } : action === "test" ? { model, consent: true } : {}), + body: JSON.stringify(action === "connect" || action === "activate" ? { runtime, workspace, consent } : action === "test" ? { model, consent: true } : {}), }); const result = await response.json(); - if (!response.ok || result.error) { setError(result.error ?? "runtime_failed"); return; } + if (!response.ok) { setError(result.error ?? "runtime_failed"); return; } if (action === "test") setTested(result.ok === true); - else { applyStatus(result as Status); setConsent(false); } + else { + applyStatus(result as Status); setConsent(false); + if ((action === "connect" || action === "activate") && result.activation === "ready" && result.providerName) onConnected?.(result.providerName); + } } catch { setError("runtime_failed"); } finally { setBusy(false); } }; @@ -63,12 +67,18 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: : error === "profile_missing" || error === "models_missing" ? t("zcodeDesktop.loginNeeded") : error === "workspace_invalid" ? t("zcodeDesktop.workspaceInvalid") : error === "inference_failed" ? t("zcodeDesktop.inferenceFailed") + : error === "provider_registration_failed" ? t("zcodeDesktop.providerPending") + : error === "catalog_update_failed" ? t("zcodeDesktop.catalogPending") : error ? t("zcodeDesktop.failed") : ""; const changed = runtime !== status?.runtime || workspace !== status?.workspace; + const partial = status?.connected && status.activation !== "ready"; return
ZCode Desktop

{t("zcodeDesktop.intro")}

-
{status?.connected ? t("zcodeDesktop.connected") : t("zcodeDesktop.notConnected")}
+
{status?.connected ? status.activation === "ready" ? t("zcodeDesktop.connected") + : status.activation === "provider_pending" ? t("zcodeDesktop.providerPending") : t("zcodeDesktop.catalogPending") + : t("zcodeDesktop.notConnected")}
+

{t("zcodeDesktop.restartNotice")}

)}
} - {(!status?.connected || changed) &&
{status?.connected && !changed && <>

{t("zcodeDesktop.testHint")}

diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index f4443ddf5b..03a9dbdcd9 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -16,9 +16,12 @@ export const de: Record = { "zcodeUsage.rules": "Spitzenzeit: Mo–Fr 14–18 Uhr Singapur; sonst Nebenzeit. Flash-Aktion: 3.–20. September 2026, täglich 23–9 Uhr Singapur. Der Hinweis endet vorsorglich am 20. September; eine Verlängerung der letzten Nacht ist nicht angegeben. Hinweise ändern keine Verbrauchs- oder Kostenberichte.", "zcodeUsage.source": "Offizielle Tarife · geprüft am 11. September 2026", "zcodeUsage.campaignSource": "Offizielle Aktionsbedingungen", - - "zcodeDesktop.intro": "Verbinden Sie die auf diesem Computer in ZCode Desktop eingerichteten Z.AI-Modelle ohne API-Schlüssel-Eingabe. OpenCodex startet die offizielle Laufzeit, keine vorhandene Desktop-Unterhaltung.", - "zcodeDesktop.connected": "Desktop verbunden · lokales Protokoll geprüft", + "zcodeDesktop.providerPending": "Desktop-Protokoll geprüft, Anbieterregistrierung unvollständig. Aktivierung erneut versuchen; Einstellungen bleiben erhalten.", + "zcodeDesktop.catalogPending": "Anbieter aktiviert, Codex-Katalog noch nicht bereit. Aktivierung erneut versuchen und bei fehlenden Modellen eigene Sichtbarkeitsfilter prüfen.", + "zcodeDesktop.retryActivation": "Aktivierung wiederholen", + "zcodeDesktop.restartNotice": "Falls Codex die Modelle nicht zeigt, starten Sie Codex nach Abschluss Ihrer Aufgaben neu. Kein automatischer Prozessneustart.", + "zcodeDesktop.intro": "ZCode Desktop verbinden, um den lokalen Anbieter zu aktivieren und seine Modelle in Codex bereitzustellen. Standardwerte bleiben unverändert; keine Inferenz.", + "zcodeDesktop.connected": "ZCode bereit · Anbieter aktiviert · Modelle veröffentlicht", "zcodeDesktop.notConnected": "Desktop nicht verbunden", "zcodeDesktop.open": "ZCode Desktop öffnen", "zcodeDesktop.detect": "Erneut erkennen", @@ -28,11 +31,10 @@ export const de: Record = { "zcodeDesktop.browse": "Ordner durchsuchen", "zcodeDesktop.up": "Übergeordneter Ordner", "zcodeDesktop.select": "Auswählen", - "zcodeDesktop.consent": "Ich erlaube ZCode, Dateien in diesem Ordner zu ändern und Befehle auszuführen. Desktop-Einstellungen bleiben schreibgeschützt. Befehle haben Netzwerkzugriff. Nur eine vertrauenswürdige offizielle Installation verbinden.", + "zcodeDesktop.consent": "Ich erlaube OpenCodex, offizielles ZCode in diesem Arbeitsbereich auszuführen, das Desktop-Profil zu lesen, den Anbieter zu aktivieren und den Codex-Katalog zu aktualisieren. Bei späteren Anfragen darf ZCode native Werkzeuge ausführen.", "zcodeDesktop.back": "Zurück", "zcodeDesktop.connect": "Desktop verbinden", "zcodeDesktop.disconnect": "Trennen", - "zcodeDesktop.use": "Diesen Anbieter verwenden", "zcodeDesktop.testHint": "Optional: Eine kurze Anfrage über ZCode prüft den Modellzugriff und verbraucht Kontingent. Die Verbindung allein verbraucht keines.", "zcodeDesktop.model": "Modell", "zcodeDesktop.test": "Mit einer Anfrage testen", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index d0b2a9a8ba..f2d5e606f9 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -17,9 +17,12 @@ export const en = { "zcodeUsage.rules": "Peak: Mon–Fri 14:00–18:00 Singapore; all other hours are off-peak. Flash campaign: September 3–20, 2026, daily 23:00–09:00 Singapore. We conservatively stop the notice at the end of September 20; the final overnight extension is not specified. Advisories do not modify usage or cost reports.", "zcodeUsage.source": "Official rates · verified September 11, 2026", "zcodeUsage.campaignSource": "Official campaign conditions", - - "zcodeDesktop.intro": "Connect the Z.AI models configured in ZCode Desktop on this computer. No API key to paste. OpenCodex starts the official runtime; it does not attach to an existing Desktop conversation.", - "zcodeDesktop.connected": "Desktop connected · local protocol verified", + "zcodeDesktop.providerPending": "Desktop protocol verified, but provider registration is incomplete. Retry activation; existing settings are preserved.", + "zcodeDesktop.catalogPending": "Provider enabled, but the Codex catalog is not ready. Retry activation; check custom model visibility filters if models remain missing.", + "zcodeDesktop.retryActivation": "Retry activation", + "zcodeDesktop.restartNotice": "If Codex does not show the new models, restart Codex when your tasks finish. No processes will be restarted automatically.", + "zcodeDesktop.intro": "Connect ZCode Desktop to enable its local provider and publish its models to Codex. Defaults stay unchanged; no inference is sent.", + "zcodeDesktop.connected": "ZCode ready · provider enabled · models published", "zcodeDesktop.notConnected": "Desktop not connected", "zcodeDesktop.open": "Open ZCode Desktop", "zcodeDesktop.detect": "Detect again", @@ -29,11 +32,10 @@ export const en = { "zcodeDesktop.browse": "Browse folders", "zcodeDesktop.up": "Parent folder", "zcodeDesktop.select": "Select", - "zcodeDesktop.consent": "I allow ZCode to edit files and run commands in this folder. Desktop settings are read-only. Commands have network access. Only connect a trusted official ZCode installation.", + "zcodeDesktop.consent": "I allow OpenCodex to run official ZCode in this workspace, read the Desktop profile, enable the ZCode provider and update the Codex catalog. ZCode may execute native tools when I later send a request.", "zcodeDesktop.back": "Back", "zcodeDesktop.connect": "Connect Desktop", "zcodeDesktop.disconnect": "Disconnect", - "zcodeDesktop.use": "Use this provider", "zcodeDesktop.testHint": "Optional: send one short request through ZCode to verify model access. This consumes account quota; connecting alone does not.", "zcodeDesktop.model": "Model", "zcodeDesktop.test": "Test with one request", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 73fc77b28a..6b6343db6f 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -15,9 +15,12 @@ export const fr: Record = { "zcodeUsage.rules": "Pointe : lun–ven 14 h–18 h à Singapour ; heures creuses le reste du temps. Promotion Flash : du 3 au 20 septembre 2026, chaque jour de 23 h à 9 h à Singapour. Par prudence, l’avis cesse à la fin du 20 septembre ; la prolongation de la dernière nuit n’est pas précisée. Aucun rapport de consommation ou de coût n’est modifié.", "zcodeUsage.source": "Tarifs officiels · vérifiés le 11 septembre 2026", "zcodeUsage.campaignSource": "Conditions officielles de la promotion", - - "zcodeDesktop.intro": "Connectez les modèles Z.AI configurés dans ZCode Desktop sur cet ordinateur, sans coller de clé API. OpenCodex lance le runtime officiel, sans reprendre une conversation Desktop.", - "zcodeDesktop.connected": "Desktop connecté · protocole local vérifié", + "zcodeDesktop.providerPending": "Protocole Desktop vérifié, mais inscription du fournisseur incomplète. Réessayez l’activation ; les réglages sont préservés.", + "zcodeDesktop.catalogPending": "Fournisseur activé, mais catalogue Codex non prêt. Réessayez et vérifiez vos filtres de visibilité si des modèles manquent.", + "zcodeDesktop.retryActivation": "Réessayer l’activation", + "zcodeDesktop.restartNotice": "Si Codex n’affiche pas les modèles, redémarrez-le après vos tâches. Aucun processus ne sera redémarré automatiquement.", + "zcodeDesktop.intro": "Connectez ZCode Desktop pour activer son fournisseur local et publier ses modèles dans Codex. Les choix par défaut restent inchangés ; aucune inférence.", + "zcodeDesktop.connected": "ZCode prêt · fournisseur activé · modèles publiés", "zcodeDesktop.notConnected": "Desktop non connecté", "zcodeDesktop.open": "Ouvrir ZCode Desktop", "zcodeDesktop.detect": "Détecter à nouveau", @@ -27,11 +30,10 @@ export const fr: Record = { "zcodeDesktop.browse": "Parcourir les dossiers", "zcodeDesktop.up": "Dossier parent", "zcodeDesktop.select": "Sélectionner", - "zcodeDesktop.consent": "J’autorise ZCode à modifier les fichiers et exécuter des commandes dans ce dossier. Les paramètres Desktop restent en lecture seule. Les commandes ont accès au réseau. Utilisez uniquement une installation officielle de confiance.", + "zcodeDesktop.consent": "J’autorise OpenCodex à exécuter ZCode officiel dans cet espace, lire le profil Desktop, activer le fournisseur et actualiser le catalogue Codex. ZCode pourra exécuter des outils natifs lors de mes futures requêtes.", "zcodeDesktop.back": "Retour", "zcodeDesktop.connect": "Connecter Desktop", "zcodeDesktop.disconnect": "Déconnecter", - "zcodeDesktop.use": "Utiliser ce fournisseur", "zcodeDesktop.testHint": "Facultatif : une courte requête via ZCode vérifie l’accès au modèle et consomme du quota. La connexion seule ne consomme pas de quota.", "zcodeDesktop.model": "Modèle", "zcodeDesktop.test": "Tester avec une requête", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index b7e31ce530..547eeec44d 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -15,9 +15,12 @@ export const ja: Record = { "zcodeUsage.rules": "ピーク:シンガポール時間の月~金14~18時。それ以外はオフピーク。Flashキャンペーン:2026年9月3~20日、毎日23~翌9時(シンガポール)。最終夜の延長は明記されていないため、安全側で9月20日末に表示を終了します。使用量・料金レポートは変更しません。", "zcodeUsage.source": "公式料金 · 2026年9月11日確認", "zcodeUsage.campaignSource": "公式キャンペーン条件", - - "zcodeDesktop.intro": "このコンピューターの ZCode Desktop で設定した Z.AI モデルを接続します。API キーの貼り付けは不要です。公式ランタイムを起動し、既存の Desktop 会話には接続しません。", - "zcodeDesktop.connected": "Desktop 接続済み・ローカルプロトコル確認済み", + "zcodeDesktop.providerPending": "Desktop プロトコルは確認済みですが、プロバイダー登録は未完了です。有効化を再試行してください。設定は保持されます。", + "zcodeDesktop.catalogPending": "プロバイダーは有効ですが、Codex カタログは未完了です。再試行し、モデルが不足する場合は表示フィルターを確認してください。", + "zcodeDesktop.retryActivation": "有効化を再試行", + "zcodeDesktop.restartNotice": "Codex にモデルが表示されない場合は、作業終了後に Codex を再起動してください。自動再起動は行いません。", + "zcodeDesktop.intro": "ZCode Desktop を接続するとローカルプロバイダーが有効になり、Codex にモデルを公開します。既定値は変更せず、推論も送信しません。", + "zcodeDesktop.connected": "ZCode 準備完了 · プロバイダー有効 · モデル公開済み", "zcodeDesktop.notConnected": "Desktop 未接続", "zcodeDesktop.open": "ZCode Desktop を開く", "zcodeDesktop.detect": "再検出", @@ -27,11 +30,10 @@ export const ja: Record = { "zcodeDesktop.browse": "フォルダーを参照", "zcodeDesktop.up": "親フォルダー", "zcodeDesktop.select": "選択", - "zcodeDesktop.consent": "このフォルダーでのファイル編集とコマンド実行を ZCode に許可します。Desktop 設定は読み取り専用です。コマンドはネットワークにアクセスできます。信頼できる公式インストールのみ接続してください。", + "zcodeDesktop.consent": "このワークスペースで公式 ZCode を実行し、Desktop プロファイルを読み、プロバイダーを有効化して Codex カタログを更新することを許可します。後でリクエストを送ると ZCode がネイティブツールを実行する場合があります。", "zcodeDesktop.back": "戻る", "zcodeDesktop.connect": "Desktop に接続", "zcodeDesktop.disconnect": "切断", - "zcodeDesktop.use": "このプロバイダーを使用", "zcodeDesktop.testHint": "任意:ZCode 経由で短いリクエストを1回送信し、モデルへのアクセスを確認します。割り当てを消費します。接続だけでは消費しません。", "zcodeDesktop.model": "モデル", "zcodeDesktop.test": "1回のリクエストでテスト", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 9dbf448f57..0c5d599467 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -15,9 +15,12 @@ export const ko: Record = { "zcodeUsage.rules": "피크: 싱가포르 월–금 14–18시, 나머지는 오프피크. Flash 행사: 2026년 9월 3–20일, 매일 싱가포르 23–다음 날 9시. 마지막 밤 연장이 명시되지 않아 9월 20일 종료 시 안내를 보수적으로 중단합니다. 사용량 및 비용 보고서는 변경하지 않습니다.", "zcodeUsage.source": "공식 요금 · 2026년 9월 11일 확인", "zcodeUsage.campaignSource": "공식 행사 조건", - - "zcodeDesktop.intro": "이 컴퓨터의 ZCode Desktop에 설정된 Z.AI 모델을 연결합니다. API 키를 붙여 넣을 필요가 없습니다. 공식 런타임을 시작하며 기존 Desktop 대화에는 연결하지 않습니다.", - "zcodeDesktop.connected": "Desktop 연결됨 · 로컬 프로토콜 확인됨", + "zcodeDesktop.providerPending": "Desktop 프로토콜은 확인했지만 공급자 등록이 미완료입니다. 활성화를 재시도하세요. 기존 설정은 유지됩니다.", + "zcodeDesktop.catalogPending": "공급자는 활성화되었지만 Codex 카탈로그는 미완료입니다. 재시도하고 모델이 없으면 사용자 표시 필터를 확인하세요.", + "zcodeDesktop.retryActivation": "활성화 재시도", + "zcodeDesktop.restartNotice": "Codex에 모델이 표시되지 않으면 작업 완료 후 Codex를 다시 시작하세요. 프로세스를 자동으로 다시 시작하지 않습니다.", + "zcodeDesktop.intro": "ZCode Desktop을 연결하면 로컬 공급자를 활성화하고 Codex에 모델을 게시합니다. 기본값은 유지되며 추론은 실행하지 않습니다.", + "zcodeDesktop.connected": "ZCode 준비 완료 · 공급자 활성화 · 모델 게시 완료", "zcodeDesktop.notConnected": "Desktop 연결 안 됨", "zcodeDesktop.open": "ZCode Desktop 열기", "zcodeDesktop.detect": "다시 감지", @@ -27,11 +30,10 @@ export const ko: Record = { "zcodeDesktop.browse": "폴더 찾아보기", "zcodeDesktop.up": "상위 폴더", "zcodeDesktop.select": "선택", - "zcodeDesktop.consent": "ZCode가 이 폴더에서 파일을 수정하고 명령을 실행하도록 허용합니다. Desktop 설정은 읽기 전용입니다. 명령은 네트워크에 접근할 수 있습니다. 신뢰할 수 있는 공식 설치만 연결하세요.", + "zcodeDesktop.consent": "이 작업 공간에서 공식 ZCode 실행, Desktop 프로필 읽기, 공급자 활성화 및 Codex 카탈로그 업데이트를 허용합니다. 나중에 요청을 보내면 ZCode가 네이티브 도구를 실행할 수 있습니다.", "zcodeDesktop.back": "뒤로", "zcodeDesktop.connect": "Desktop 연결", "zcodeDesktop.disconnect": "연결 해제", - "zcodeDesktop.use": "이 제공자 사용", "zcodeDesktop.testHint": "선택 사항: ZCode를 통해 짧은 요청을 한 번 보내 모델 접근을 확인합니다. 계정 할당량을 사용합니다. 연결만으로는 사용하지 않습니다.", "zcodeDesktop.model": "모델", "zcodeDesktop.test": "요청 한 번으로 테스트", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 58b7f12c3c..b3e502b8e6 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -15,9 +15,12 @@ export const ru: Record = { "zcodeUsage.rules": "Пик: пн–пт 14–18 по Сингапуру; остальное время — вне пика. Акция Flash: 3–20 сентября 2026, ежедневно 23–9 по Сингапуру. Уведомление консервативно прекращается в конце 20 сентября: продление последней ночи не уточнено. Отчёты о расходе и стоимости не меняются.", "zcodeUsage.source": "Официальные тарифы · проверено 11 сентября 2026", "zcodeUsage.campaignSource": "Официальные условия акции", - - "zcodeDesktop.intro": "Подключите модели Z.AI, настроенные в ZCode Desktop на этом компьютере, без ввода API-ключа. Запускается официальный runtime, а не существующий диалог Desktop.", - "zcodeDesktop.connected": "Desktop подключён · локальный протокол проверен", + "zcodeDesktop.providerPending": "Протокол Desktop проверен, но регистрация провайдера не завершена. Повторите активацию; настройки сохраняются.", + "zcodeDesktop.catalogPending": "Провайдер включён, но каталог Codex не готов. Повторите активацию и проверьте фильтры видимости отсутствующих моделей.", + "zcodeDesktop.retryActivation": "Повторить активацию", + "zcodeDesktop.restartNotice": "Если модели не появились, перезапустите Codex после завершения задач. Автоматического перезапуска процессов не будет.", + "zcodeDesktop.intro": "Подключение ZCode Desktop включает локального провайдера и публикует модели в Codex. Значения по умолчанию сохраняются; генерация не запускается.", + "zcodeDesktop.connected": "ZCode готов · провайдер включён · модели опубликованы", "zcodeDesktop.notConnected": "Desktop не подключён", "zcodeDesktop.open": "Открыть ZCode Desktop", "zcodeDesktop.detect": "Найти снова", @@ -27,11 +30,10 @@ export const ru: Record = { "zcodeDesktop.browse": "Обзор папок", "zcodeDesktop.up": "Родительская папка", "zcodeDesktop.select": "Выбрать", - "zcodeDesktop.consent": "Разрешаю ZCode изменять файлы и выполнять команды в этой папке. Настройки Desktop доступны только для чтения. Команды имеют доступ к сети. Подключайте только доверенную официальную установку.", + "zcodeDesktop.consent": "Разрешаю OpenCodex запускать официальный ZCode в этой папке, читать профиль Desktop, включать провайдера и обновлять каталог Codex. При последующих запросах ZCode может выполнять встроенные инструменты.", "zcodeDesktop.back": "Назад", "zcodeDesktop.connect": "Подключить Desktop", "zcodeDesktop.disconnect": "Отключить", - "zcodeDesktop.use": "Использовать провайдера", "zcodeDesktop.testHint": "Необязательно: один короткий запрос через ZCode проверит доступ к модели и потратит квоту. Само подключение квоту не расходует.", "zcodeDesktop.model": "Модель", "zcodeDesktop.test": "Проверить одним запросом", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index fe3a6d7ee3..3de519b26a 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -16,9 +16,12 @@ export const tr: Record = { "zcodeUsage.rules": "Yoğun: Singapur saatine göre Pzt–Cum 14–18; diğer tüm saatler yoğun değildir. Flash kampanyası: 3–20 Eylül 2026, her gün Singapur 23–ertesi gün 9. Son gece uzatması belirtilmediğinden bildirim tedbiren 20 Eylül sonunda durur. Kullanım veya maliyet raporlarını değiştirmez.", "zcodeUsage.source": "Resmî tarifeler · 11 Eylül 2026 tarihinde doğrulandı", "zcodeUsage.campaignSource": "Resmî kampanya koşulları", - - "zcodeDesktop.intro": "Bu bilgisayardaki ZCode Desktop içinde yapılandırılmış Z.AI modellerini API anahtarı yapıştırmadan bağlayın. Resmî çalışma zamanı başlatılır; mevcut Desktop konuşmasına bağlanılmaz.", - "zcodeDesktop.connected": "Desktop bağlı · yerel protokol doğrulandı", + "zcodeDesktop.providerPending": "Desktop protokolü doğrulandı, sağlayıcı kaydı tamamlanmadı. Etkinleştirmeyi yeniden deneyin; ayarlar korunur.", + "zcodeDesktop.catalogPending": "Sağlayıcı etkin, ancak Codex kataloğu hazır değil. Yeniden deneyin; eksik modeller için özel görünürlük filtrelerini kontrol edin.", + "zcodeDesktop.retryActivation": "Etkinleştirmeyi yeniden dene", + "zcodeDesktop.restartNotice": "Modeller görünmüyorsa görevler bitince Codex’i yeniden başlatın. Hiçbir işlem otomatik yeniden başlatılmaz.", + "zcodeDesktop.intro": "ZCode Desktop bağlantısı yerel sağlayıcıyı etkinleştirip modellerini Codex’e ekler. Varsayılanlar değişmez; çıkarım yapılmaz.", + "zcodeDesktop.connected": "ZCode hazır · sağlayıcı etkin · modeller yayımlandı", "zcodeDesktop.notConnected": "Desktop bağlı değil", "zcodeDesktop.open": "ZCode Desktop aç", "zcodeDesktop.detect": "Yeniden algıla", @@ -28,11 +31,10 @@ export const tr: Record = { "zcodeDesktop.browse": "Klasörlere göz at", "zcodeDesktop.up": "Üst klasör", "zcodeDesktop.select": "Seç", - "zcodeDesktop.consent": "ZCode’un bu klasörde dosyaları düzenlemesine ve komut çalıştırmasına izin veriyorum. Desktop ayarları salt okunurdur. Komutlar ağa erişebilir. Yalnızca güvenilir resmî kurulumu bağlayın.", + "zcodeDesktop.consent": "OpenCodex’in bu çalışma alanında resmi ZCode’u çalıştırmasına, Desktop profilini okumasına, sağlayıcıyı etkinleştirip Codex kataloğunu güncellemesine izin veriyorum. Sonraki isteklerimde ZCode yerel araçları çalıştırabilir.", "zcodeDesktop.back": "Geri", "zcodeDesktop.connect": "Desktop bağla", "zcodeDesktop.disconnect": "Bağlantıyı kes", - "zcodeDesktop.use": "Bu sağlayıcıyı kullan", "zcodeDesktop.testHint": "İsteğe bağlı: ZCode üzerinden kısa bir istek model erişimini doğrular ve hesap kotasını kullanır. Yalnızca bağlantı kurmak kota kullanmaz.", "zcodeDesktop.model": "Model", "zcodeDesktop.test": "Bir istekle test et", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 29da9fcaea..f2d9b2b1f3 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -13,9 +13,12 @@ export const zhTW: Record = { "zcodeUsage.rules": "尖峰:新加坡時間週一至週五 14:00–18:00,其餘皆為離峰。Flash 活動:2026 年 9 月 3–20 日,每天新加坡時間 23:00–翌日 09:00。末夜是否延長未明確說明,因此提示保守地於 9 月 20 日結束時停止。不修改用量或費用報告。", "zcodeUsage.source": "官方費率 · 2026 年 9 月 11 日驗證", "zcodeUsage.campaignSource": "官方活動條件", - - "zcodeDesktop.intro": "連接此電腦上 ZCode Desktop 中設定的 Z.AI 模型,無須貼上 API 金鑰。OpenCodex 啟動官方執行環境,不接管現有 Desktop 對話。", - "zcodeDesktop.connected": "Desktop 已連接 · 本機協定已驗證", + "zcodeDesktop.providerPending": "Desktop 協定已驗證,但供應商註冊尚未完成。請重試啟用;現有設定將保留。", + "zcodeDesktop.catalogPending": "供應商已啟用,但 Codex 目錄尚未就緒。請重試;若模型仍缺失,請檢查自訂可見性篩選。", + "zcodeDesktop.retryActivation": "重試啟用", + "zcodeDesktop.restartNotice": "若 Codex 未顯示新模型,請於任務結束後重新啟動 Codex。不會自動重新啟動任何程序。", + "zcodeDesktop.intro": "連接 ZCode Desktop 將啟用本機供應商並將模型發佈至 Codex。不變更預設設定,也不執行推論。", + "zcodeDesktop.connected": "ZCode 已就緒 · 供應商已啟用 · 模型已發佈", "zcodeDesktop.notConnected": "Desktop 未連接", "zcodeDesktop.open": "開啟 ZCode Desktop", "zcodeDesktop.detect": "重新偵測", @@ -25,11 +28,10 @@ export const zhTW: Record = { "zcodeDesktop.browse": "瀏覽目錄", "zcodeDesktop.up": "上層目錄", "zcodeDesktop.select": "選取", - "zcodeDesktop.consent": "我允許 ZCode 在此目錄修改檔案並執行命令。Desktop 設定維持唯讀。命令可存取網路。請僅連接可信任的官方 ZCode 安裝。", + "zcodeDesktop.consent": "我允許 OpenCodex 在此工作區執行官方 ZCode、讀取 Desktop 設定、啟用供應商並更新 Codex 目錄。日後傳送請求時,ZCode 可能執行原生工具。", "zcodeDesktop.back": "返回", "zcodeDesktop.connect": "連接 Desktop", "zcodeDesktop.disconnect": "中斷連接", - "zcodeDesktop.use": "使用此供應商", "zcodeDesktop.testHint": "選用:透過 ZCode 傳送一次簡短請求以驗證模型存取權限。這會消耗帳戶配額;僅連接不會消耗。", "zcodeDesktop.model": "模型", "zcodeDesktop.test": "傳送一次測試請求", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index ebc1ab87a1..ea3ca80b83 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -15,9 +15,12 @@ export const zh: Record = { "zcodeUsage.rules": "高峰:新加坡时间周一至周五 14:00–18:00,其余均为非高峰。Flash 活动:2026 年 9 月 3–20 日,每天新加坡时间 23:00–次日 09:00。末夜是否延长未明确说明,因此提示保守地于 9 月 20 日结束时停止。不修改用量或费用报告。", "zcodeUsage.source": "官方费率 · 2026 年 9 月 11 日核验", "zcodeUsage.campaignSource": "官方活动条件", - - "zcodeDesktop.intro": "连接此计算机上 ZCode Desktop 中配置的 Z.AI 模型,无需粘贴 API 密钥。OpenCodex 启动官方运行时,不接管现有 Desktop 对话。", - "zcodeDesktop.connected": "Desktop 已连接 · 本地协议已验证", + "zcodeDesktop.providerPending": "Desktop 协议已验证,但提供商注册尚未完成。请重试启用;现有设置将保留。", + "zcodeDesktop.catalogPending": "提供商已启用,但 Codex 目录尚未就绪。请重试;若模型仍缺失,请检查自定义可见性筛选。", + "zcodeDesktop.retryActivation": "重试启用", + "zcodeDesktop.restartNotice": "若 Codex 未显示新模型,请在任务结束后重启 Codex。不会自动重启任何进程。", + "zcodeDesktop.intro": "连接 ZCode Desktop 将启用本地提供商并将模型发布到 Codex。不更改默认设置,也不执行推理。", + "zcodeDesktop.connected": "ZCode 已就绪 · 提供商已启用 · 模型已发布", "zcodeDesktop.notConnected": "Desktop 未连接", "zcodeDesktop.open": "打开 ZCode Desktop", "zcodeDesktop.detect": "重新检测", @@ -27,11 +30,10 @@ export const zh: Record = { "zcodeDesktop.browse": "浏览目录", "zcodeDesktop.up": "上级目录", "zcodeDesktop.select": "选择", - "zcodeDesktop.consent": "我允许 ZCode 在此目录中修改文件并执行命令。Desktop 配置保持只读。命令可以访问网络。请仅连接可信的官方 ZCode 安装。", + "zcodeDesktop.consent": "我允许 OpenCodex 在此工作区运行官方 ZCode、读取 Desktop 配置、启用提供商并更新 Codex 目录。以后发送请求时,ZCode 可能执行原生工具。", "zcodeDesktop.back": "返回", "zcodeDesktop.connect": "连接 Desktop", "zcodeDesktop.disconnect": "断开连接", - "zcodeDesktop.use": "使用此提供商", "zcodeDesktop.testHint": "可选:通过 ZCode 发送一次简短请求以验证模型访问权限。这会消耗账户配额;仅连接不会消耗。", "zcodeDesktop.model": "模型", "zcodeDesktop.test": "发送一次测试请求", diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx index 88cb0b6f79..c0ccbdc8dc 100644 --- a/gui/src/pages/Providers.tsx +++ b/gui/src/pages/Providers.tsx @@ -698,7 +698,8 @@ export default function Providers({ apiBase }: { apiBase: string }) { modelsNotice.open(name, !config.providers[name]); fetchConfig(); fetchOauth(); - fetchProviderQuotas(true); + // Desktop connection is protocol/catalog only, not consent to validate accounts. + fetchProviderQuotas(name !== "zcode" && config.providers[name]?.adapter !== "zcode"); bumpModelsRefresh(); }} onAccountLogin={onAccountLogin} diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 91ad5e487b..18536a0580 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -36,7 +36,7 @@ beforeEach(() => { const url = new URL(String(input), "http://localhost"); requests.push({ path: url.pathname, body: options?.body ? JSON.parse(String(options.body)) : undefined }); if (url.pathname.endsWith("/test")) return Response.json({ ok: true }); - return Response.json({ connected: url.pathname.endsWith("/connect"), runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [{ id: "builtin:zai/model", label: "Model" }] }); + return Response.json({ connected: url.pathname.endsWith("/connect"), activation: url.pathname.endsWith("/connect") ? "ready" : "disconnected", providerName: "zcode", runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [{ id: "builtin:zai/model", label: "Model" }] }); }, }); @@ -81,9 +81,9 @@ test("Desktop connect requires explicit consent and does not automatically spend await click(button("Connect Desktop")); expect(requests.find(r => r.path.endsWith("/connect"))?.body).toEqual({ runtime: "/installed/ZCode", workspace: "/project", consent: true }); expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); - expect(closeCalls).toBe(0); - await click(button("Use this provider")); expect(closeCalls).toBe(1); + expect(host.textContent).not.toContain("Use this provider"); + expect(requests.some(r => r.path === "/api/providers")).toBe(false); }); test("one-request test is a separate explicit quota-spending action", async () => { await mountPane(); await click(host.querySelector('input[type="checkbox"]')!); @@ -107,3 +107,22 @@ test("incompatible Node preflight shows safe actionable guidance without connect expect(host.querySelector('input[type="checkbox"]')!.checked).toBe(false); expect(button("Connect Desktop").disabled).toBe(true); }); + +test("partial activation remains visible and retries without a second protocol connection", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { + const path = new URL(String(input), "http://localhost").pathname; + requests.push({ path, body: options?.body ? JSON.parse(String(options.body)) : undefined }); + return Response.json({ connected: true, activation: path.endsWith("/activate") ? "ready" : "catalog_pending", + ...(path.endsWith("/activate") ? {} : { error: "catalog_update_failed" }), providerName: "zcode", + runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [] }); + } }); + await mountPane(); + expect(host.textContent).toContain("catalog is not ready"); + expect(closeCalls).toBe(0); + expect(button("Retry activation").disabled).toBe(true); + await click(host.querySelector('input[type="checkbox"]')!); + await click(button("Retry activation")); + expect(closeCalls).toBe(1); + expect(requests.filter(r => r.path.endsWith("/connect") || r.path.endsWith("/test"))).toHaveLength(0); + expect(host.textContent).toContain("No processes will be restarted automatically"); +}); diff --git a/src/server/management/zcode-desktop-activation.ts b/src/server/management/zcode-desktop-activation.ts new file mode 100644 index 0000000000..5c59f0dfa2 --- /dev/null +++ b/src/server/management/zcode-desktop-activation.ts @@ -0,0 +1,70 @@ +import { saveConfigPreservingClaudeCode, withConfigMutationLockSync } from "../../config"; +import { readCatalog, readCodexCatalogPath, catalogModelSlug, applyProviderConfigHints } from "../../codex/catalog"; +import { clearModelCache } from "../../codex/model-cache"; +import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; +import type { ManagementContext } from "./context"; +import type { desktopStatus } from "../../adapters/zcode/desktop"; + +type DesktopStatus = ReturnType; +export const readDesktopCatalogSlugs = (): string[] => (readCatalog(readCodexCatalogPath())?.models ?? []) + .filter(row => row.visibility === "list").map(row => String(row.slug)); + +function providerName(ctx: ManagementContext): string | undefined { + const matches = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.adapter === "zcode"); + if (matches.includes("zcode")) return "zcode"; + return matches.length === 1 ? matches[0] : undefined; +} + +/** Read actual persisted catalog evidence, not just a successful protocol or cache flush. */ +export function desktopActivation(ctx: ManagementContext, status: DesktopStatus, readSlugs = readDesktopCatalogSlugs) { + const name = providerName(ctx); + const provider = name ? ctx.config.providers[name] : undefined; + const registered = !!provider && provider.disabled !== true && provider.authMode === "local"; + let catalogReady = false; + try { + const slugs = new Set(readSlugs()); + catalogReady = registered && status.models.length > 0 && status.models.every(model => slugs.has( + catalogModelSlug(applyProviderConfigHints(name!, provider!, { id: model.id, provider: name! })), + )); + } catch { /* Unreadable catalog is partial, never success. */ } + return { ...status, providerName: name, providerRegistered: registered, + activation: !status.connected ? "disconnected" : !registered ? "provider_pending" : !catalogReady ? "catalog_pending" : "ready" }; +} + +export async function activateDesktopProvider(ctx: ManagementContext, status: DesktopStatus, readSlugs = readDesktopCatalogSlugs) { + if (!status.connected) return desktopActivation(ctx, status, readSlugs); + let name: string; + try { + withConfigMutationLockSync(() => { + name = providerName(ctx) ?? "zcode"; + const existing = ctx.config.providers[name]; + if ((!providerName(ctx) && Object.values(ctx.config.providers).some(p => p.adapter === "zcode")) + || (existing && (existing.adapter !== "zcode" || existing.authMode !== "local"))) throw new Error("provider conflict"); + // Reconnection only enables the existing provider. Never replace its custom settings, + // explicit model filters, pricing, alias or defaults with a form/preset payload. + const next = existing ? { ...existing, disabled: false } + : { adapter: "zcode" as const, authMode: "local" as const, baseUrl: "https://zcode.z.ai", disabled: false }; + try { + ctx.config.providers[name] = next; + (ctx.deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(ctx.config); + } catch (error) { + if (existing) ctx.config.providers[name] = existing; else delete ctx.config.providers[name]; + throw error; + } + }); + reconcileLiveStateStores(); + clearModelCache(name!); + } catch { + return { ...desktopActivation(ctx, status, readSlugs), activation: "provider_pending", error: "provider_registration_failed" }; + } + try { + const result = await ctx.convergeCodexCatalog(); + const current = desktopActivation(ctx, status, readSlugs); + if (result.status !== "committed" || current.activation !== "ready") { + return { ...current, activation: "catalog_pending", error: "catalog_update_failed" }; + } + return current; + } catch { + return { ...desktopActivation(ctx, status, readSlugs), activation: "catalog_pending", error: "catalog_update_failed" }; + } +} diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts index d116934632..934fcb892f 100644 --- a/src/server/management/zcode-desktop-routes.ts +++ b/src/server/management/zcode-desktop-routes.ts @@ -6,13 +6,15 @@ import { createTranslatorBudget } from "../../lib/translator-budget"; import { invalidateCodexModelsCache } from "../../codex/catalog"; import { clearGatherRoutedModelsInflight } from "../../codex/catalog/provider-fetch"; import { readBoundedJsonRequestBody } from "../request-decompress"; +import { activateDesktopProvider, desktopActivation, readDesktopCatalogSlugs } from "./zcode-desktop-activation"; -const services = { connectDesktop, desktopStatus, disconnectDesktop }; +const services = { connectDesktop, desktopStatus, disconnectDesktop, readDesktopCatalogSlugs }; +let activating = false; let testing = false; export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = services): Promise { const { req, url } = ctx; if (!url.pathname.startsWith("/api/zcode-desktop")) return null; - if (url.pathname === "/api/zcode-desktop" && req.method === "GET") return jsonResponse(deps.desktopStatus()); + if (url.pathname === "/api/zcode-desktop" && req.method === "GET") return jsonResponse(desktopActivation(ctx, deps.desktopStatus(), deps.readDesktopCatalogSlugs)); if (url.pathname === "/api/zcode-desktop/folders" && req.method === "GET") { try { return jsonResponse(desktopFolders(url.searchParams.get("path") ?? undefined)); } catch { return jsonResponse({ error: "workspace_invalid" }, 400); } @@ -25,13 +27,19 @@ export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = se const raw = await readBoundedJsonRequestBody(req, 12_000, undefined, { signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]) }); if (!raw || typeof raw !== "object" || Array.isArray(raw)) return jsonResponse({ error: "invalid_request" }, 400); const body = raw as Record; - if (url.pathname === "/api/zcode-desktop/connect") { + if (url.pathname === "/api/zcode-desktop/connect" || url.pathname === "/api/zcode-desktop/activate") { if (body.consent !== true || typeof body.runtime !== "string" || typeof body.workspace !== "string") return jsonResponse({ error: "consent_required" }, 400); - const status = await deps.connectDesktop(body.runtime, body.workspace); - invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); - return jsonResponse(status); + if (activating) return jsonResponse({ error: "busy" }, 409); + activating = true; + try { + const status = url.pathname.endsWith("/activate") ? deps.desktopStatus() : await deps.connectDesktop(body.runtime, body.workspace); + if (url.pathname.endsWith("/activate") && (status.runtime !== body.runtime || status.workspace !== body.workspace)) return jsonResponse({ error: "invalid_request" }, 409); + clearGatherRoutedModelsInflight(); + return jsonResponse(await activateDesktopProvider(ctx, status, deps.readDesktopCatalogSlugs)); + } finally { activating = false; } } if (url.pathname === "/api/zcode-desktop/disconnect") { + if (activating) return jsonResponse({ error: "busy" }, 409); await deps.disconnectDesktop(); invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); return jsonResponse(deps.desktopStatus()); } diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index 3cedc6823c..c62603771c 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -1,15 +1,41 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { handleZcodeDesktopRoutes } from "../../src/server/management/zcode-desktop-routes"; import type { ManagementContext } from "../../src/server/management/context"; -const status = { connected: false, issue: undefined, runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [], platform: "linux" as const }; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { getDefaultConfig } from "../../src/config"; +import { activateDesktopProvider, desktopActivation } from "../../src/server/management/zcode-desktop-activation"; +let home: string; +let previousHome: string | undefined; +beforeEach(() => { + home = mkdtempSync(join(tmpdir(), "ocx-zcode-activation-")); + previousHome = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = home; +}); +afterEach(() => { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; + rmSync(home, { recursive: true, force: true }); +}); +const models = [{ id: "builtin:zai-coding-plan/glm-5.3", providerId: "builtin:zai-coding-plan", modelId: "glm-5.3", label: "GLM-5.3" }, + { id: "builtin:zai-coding-plan/glm-5.3-flash", providerId: "builtin:zai-coding-plan", modelId: "glm-5.3-flash", label: "GLM-5.3-Flash" }]; +const status = { connected: false, issue: undefined, runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models, platform: "linux" as const }; function context(path: string, body: unknown, principal?: ManagementContext["principal"]): ManagementContext { const req = new Request(`http://localhost${path}`, { method: "POST", headers: { "content-type": "application/json", origin: "http://localhost", "x-opencodex-gui-session": "forged" }, body: JSON.stringify(body) }); - return { req, url: new URL(req.url), principal } as ManagementContext; + const config = getDefaultConfig(); + const catalog = join(home, "catalog.json"); + return { req, url: new URL(req.url), principal, config, + deps: { saveConfigPreservingClaudeCode: c => writeFileSync(join(home, "config.json"), JSON.stringify(c)) }, + convergeCodexCatalog: async () => { + writeFileSync(catalog, JSON.stringify(models.map(m => "zcode/" + m.id.replaceAll("/", "-")))); + return { status: "committed", changed: true, degraded: false, notices: [] }; + }, + } as ManagementContext; } function fixture() { let calls = 0; - const deps = { desktopStatus: () => status, connectDesktop: async () => { calls++; return { ...status, connected: true }; }, disconnectDesktop: async () => { calls++; } }; + const deps = { desktopStatus: () => status, connectDesktop: async () => { calls++; return { ...status, connected: true }; }, disconnectDesktop: async () => { calls++; }, readDesktopCatalogSlugs: () => { try { return JSON.parse(readFileSync(join(home, "catalog.json"), "utf8")); } catch { return []; } } }; return { deps, calls: () => calls }; } describe("ZCode Desktop management consent", () => { @@ -27,12 +53,72 @@ describe("ZCode Desktop management consent", () => { const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { runtime: "/installed/ZCode", workspace: "/project" }, "gui-session"), f.deps); expect(response?.status).toBe(400); expect(f.calls()).toBe(0); }); - test("connection does not send inference or mutate provider settings", async () => { - const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { consent: true, runtime: "/installed/ZCode", workspace: "/project" }, "gui-session"), f.deps); - expect(await response?.json()).toMatchObject({ connected: true }); expect(f.calls()).toBe(1); + test("connection enables the provider and publishes models without changing defaults", async () => { + const f = fixture(); const ctx = context("/api/zcode-desktop/connect", { consent: true, runtime: "/installed/ZCode", workspace: "/project" }, "gui-session"); + const before = ctx.config.defaultProvider; + const response = await handleZcodeDesktopRoutes(ctx, f.deps); + expect(await response?.json()).toMatchObject({ connected: true, activation: "ready", providerRegistered: true }); + expect(f.calls()).toBe(1); + expect(ctx.config.defaultProvider).toBe(before); + expect(JSON.parse(readFileSync(join(home, "config.json"), "utf8")).providers.zcode).toMatchObject({ adapter: "zcode", disabled: false, authMode: "local" }); + expect(f.deps.readDesktopCatalogSlugs()).toEqual(models.map(m => "zcode/" + m.id.replaceAll("/", "-"))); }); test("oversized setup payload is rejected before runtime execution", async () => { const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { consent: true, runtime: "x".repeat(14000), workspace: "/project" }, "gui-session"), f.deps); expect(response?.status).toBe(400); expect(f.calls()).toBe(0); }); }); + +test("reconnection preserves custom settings and restart observes actual persisted catalog", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); + const custom = { adapter: "zcode" as const, authMode: "local" as const, baseUrl: "https://zcode.z.ai", disabled: true, + defaultModel: models[0]!.id, note: "custom note", contextWindow: 64000 }; + ctx.config.providers.zcode = custom; + const connected = { ...status, connected: true }; + await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs); + await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs); + expect(ctx.config.providers.zcode).toEqual({ ...custom, disabled: false }); + expect(Object.values(ctx.config.providers).filter(p => p.adapter === "zcode")).toHaveLength(1); + ctx.config = JSON.parse(readFileSync(join(home, "config.json"), "utf8")); + expect(desktopActivation(ctx, connected, f.deps.readDesktopCatalogSlugs).activation).toBe("ready"); + rmSync(join(home, "catalog.json")); + expect(desktopActivation(ctx, connected, f.deps.readDesktopCatalogSlugs).activation).toBe("catalog_pending"); +}); + +test("registration failure rolls back config, preserves protocol state and allows retry", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); + const save = ctx.deps.saveConfigPreservingClaudeCode; + ctx.deps.saveConfigPreservingClaudeCode = () => { throw new Error("private profile content"); }; + const connected = { ...status, connected: true }; + const result = await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs); + expect(result).toMatchObject({ connected: true, activation: "provider_pending", error: "provider_registration_failed" }); + expect(JSON.stringify(result)).not.toContain("private profile content"); + expect(ctx.config.providers.zcode).toBeUndefined(); + ctx.deps.saveConfigPreservingClaudeCode = save; + expect((await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs)).activation).toBe("ready"); +}); + +test("failed or skipped catalog and committed-but-missing models remain partial", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); + const converge = ctx.convergeCodexCatalog, connected = { ...status, connected: true }; + for (const outcome of [ + { status: "skipped", reason: "busy", retryable: true }, + { status: "failed", reason: "disk", phase: "commit", retryable: true, partialWrite: true }, + { status: "committed", changed: false, degraded: true, notices: [] }, + ] as const) { + ctx.convergeCodexCatalog = async () => outcome; + expect(await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs)).toMatchObject({ + connected: true, providerRegistered: true, activation: "catalog_pending", error: "catalog_update_failed", + }); + } + ctx.convergeCodexCatalog = converge; + expect((await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs)).activation).toBe("ready"); +}); + +test("a different provider occupying zcode is never overwritten", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); + ctx.config.providers.zcode = { adapter: "openai-chat", baseUrl: "https://example.invalid" }; + const before = structuredClone(ctx.config.providers); + expect((await activateDesktopProvider(ctx, { ...status, connected: true }, f.deps.readDesktopCatalogSlugs)).activation).toBe("provider_pending"); + expect(ctx.config.providers).toEqual(before); +}); From 222f9c1ba8f341e1a920b34efceace2e02160d52 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 10:53:28 -0300 Subject: [PATCH 06/43] test(zcode): cover activation retries and explain dashboard authorization --- .../src/content/docs/guides/zcode-agent.md | 2 +- gui/src/components/ZcodeDesktopPane.tsx | 1 + gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/tests/zcode-desktop-pane.test.tsx | 13 +++++++++++ tests/providers/zcode-desktop-routes.test.ts | 22 ++++++++++++++++++- 13 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index dc547b3158..8f166e62f0 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -28,7 +28,7 @@ or subscription terms; those remain the vendor's policy. 1. Install the official **ZCode Desktop**, open it on the **same computer as the proxy**, and sign in/configure your Z.AI models there. Keep Desktop open during initial detection. -2. In OpenCodex, choose **Providers → Add Provider → ZCode (local agent)**. +2. Use a browser session on a loopback-bound dashboard or a paired hub. An admin-token login on a public listener does not authorize Desktop setup. In OpenCodex, choose **Providers → Add Provider → ZCode (local agent)**. 3. Click **Detect again** if needed. OpenCodex finds running Desktop installations and standard installation folders. For portable/extracted installations, the advanced folder field lets you select the application directory; it never accepts a shell command. diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index dc3377ffcb..16db308cc3 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -61,6 +61,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: }; const problem = error === "desktop_missing" ? t("zcodeDesktop.missing") : error === "sandbox_missing" ? t("zcodeDesktop.sandboxMissing") + : error === "dashboard_required" ? t("zcodeDesktop.dashboardRequired") : error === "node_missing" ? t("zcodeDesktop.nodeMissing") : error === "node_incompatible" ? t("zcodeDesktop.nodeIncompatible") : error === "platform_unsupported" ? t("zcodeDesktop.platformUnsupported") diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 03a9dbdcd9..8ad4e72de0 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -20,6 +20,7 @@ export const de: Record = { "zcodeDesktop.catalogPending": "Anbieter aktiviert, Codex-Katalog noch nicht bereit. Aktivierung erneut versuchen und bei fehlenden Modellen eigene Sichtbarkeitsfilter prüfen.", "zcodeDesktop.retryActivation": "Aktivierung wiederholen", "zcodeDesktop.restartNotice": "Falls Codex die Modelle nicht zeigt, starten Sie Codex nach Abschluss Ihrer Aufgaben neu. Kein automatischer Prozessneustart.", + "zcodeDesktop.dashboardRequired": "Diese Aktion erfordert eine Browsersitzung über ein an Loopback gebundenes Dashboard oder einen gekoppelten Hub. Ein Admin-Token allein erlaubt keine Desktop-Einrichtung.", "zcodeDesktop.intro": "ZCode Desktop verbinden, um den lokalen Anbieter zu aktivieren und seine Modelle in Codex bereitzustellen. Standardwerte bleiben unverändert; keine Inferenz.", "zcodeDesktop.connected": "ZCode bereit · Anbieter aktiviert · Modelle veröffentlicht", "zcodeDesktop.notConnected": "Desktop nicht verbunden", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index f2d5e606f9..35ef6c60fe 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -21,6 +21,7 @@ export const en = { "zcodeDesktop.catalogPending": "Provider enabled, but the Codex catalog is not ready. Retry activation; check custom model visibility filters if models remain missing.", "zcodeDesktop.retryActivation": "Retry activation", "zcodeDesktop.restartNotice": "If Codex does not show the new models, restart Codex when your tasks finish. No processes will be restarted automatically.", + "zcodeDesktop.dashboardRequired": "This action requires a browser session on a loopback-bound dashboard or a paired hub. An admin token alone cannot authorize Desktop setup.", "zcodeDesktop.intro": "Connect ZCode Desktop to enable its local provider and publish its models to Codex. Defaults stay unchanged; no inference is sent.", "zcodeDesktop.connected": "ZCode ready · provider enabled · models published", "zcodeDesktop.notConnected": "Desktop not connected", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 6b6343db6f..2e15e9fc51 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -19,6 +19,7 @@ export const fr: Record = { "zcodeDesktop.catalogPending": "Fournisseur activé, mais catalogue Codex non prêt. Réessayez et vérifiez vos filtres de visibilité si des modèles manquent.", "zcodeDesktop.retryActivation": "Réessayer l’activation", "zcodeDesktop.restartNotice": "Si Codex n’affiche pas les modèles, redémarrez-le après vos tâches. Aucun processus ne sera redémarré automatiquement.", + "zcodeDesktop.dashboardRequired": "Cette action nécessite une session navigateur sur un tableau de bord lié à loopback ou un hub appairé. Le jeton administrateur seul ne permet pas la configuration Desktop.", "zcodeDesktop.intro": "Connectez ZCode Desktop pour activer son fournisseur local et publier ses modèles dans Codex. Les choix par défaut restent inchangés ; aucune inférence.", "zcodeDesktop.connected": "ZCode prêt · fournisseur activé · modèles publiés", "zcodeDesktop.notConnected": "Desktop non connecté", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 547eeec44d..6fdc3183ab 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -19,6 +19,7 @@ export const ja: Record = { "zcodeDesktop.catalogPending": "プロバイダーは有効ですが、Codex カタログは未完了です。再試行し、モデルが不足する場合は表示フィルターを確認してください。", "zcodeDesktop.retryActivation": "有効化を再試行", "zcodeDesktop.restartNotice": "Codex にモデルが表示されない場合は、作業終了後に Codex を再起動してください。自動再起動は行いません。", + "zcodeDesktop.dashboardRequired": "この操作にはループバックにバインドされたダッシュボードまたはペアリング済みハブのブラウザーセッションが必要です。管理トークンだけでは Desktop を設定できません。", "zcodeDesktop.intro": "ZCode Desktop を接続するとローカルプロバイダーが有効になり、Codex にモデルを公開します。既定値は変更せず、推論も送信しません。", "zcodeDesktop.connected": "ZCode 準備完了 · プロバイダー有効 · モデル公開済み", "zcodeDesktop.notConnected": "Desktop 未接続", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 0c5d599467..f3decba37a 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -19,6 +19,7 @@ export const ko: Record = { "zcodeDesktop.catalogPending": "공급자는 활성화되었지만 Codex 카탈로그는 미완료입니다. 재시도하고 모델이 없으면 사용자 표시 필터를 확인하세요.", "zcodeDesktop.retryActivation": "활성화 재시도", "zcodeDesktop.restartNotice": "Codex에 모델이 표시되지 않으면 작업 완료 후 Codex를 다시 시작하세요. 프로세스를 자동으로 다시 시작하지 않습니다.", + "zcodeDesktop.dashboardRequired": "이 작업에는 루프백에 바인딩된 대시보드 또는 페어링된 허브의 브라우저 세션이 필요합니다. 관리자 토큰만으로는 Desktop을 설정할 수 없습니다.", "zcodeDesktop.intro": "ZCode Desktop을 연결하면 로컬 공급자를 활성화하고 Codex에 모델을 게시합니다. 기본값은 유지되며 추론은 실행하지 않습니다.", "zcodeDesktop.connected": "ZCode 준비 완료 · 공급자 활성화 · 모델 게시 완료", "zcodeDesktop.notConnected": "Desktop 연결 안 됨", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index b3e502b8e6..94001167d9 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -19,6 +19,7 @@ export const ru: Record = { "zcodeDesktop.catalogPending": "Провайдер включён, но каталог Codex не готов. Повторите активацию и проверьте фильтры видимости отсутствующих моделей.", "zcodeDesktop.retryActivation": "Повторить активацию", "zcodeDesktop.restartNotice": "Если модели не появились, перезапустите Codex после завершения задач. Автоматического перезапуска процессов не будет.", + "zcodeDesktop.dashboardRequired": "Требуется сеанс браузера на панели, привязанной к loopback, или сопряжённом хабе. Одного токена администратора недостаточно для настройки Desktop.", "zcodeDesktop.intro": "Подключение ZCode Desktop включает локального провайдера и публикует модели в Codex. Значения по умолчанию сохраняются; генерация не запускается.", "zcodeDesktop.connected": "ZCode готов · провайдер включён · модели опубликованы", "zcodeDesktop.notConnected": "Desktop не подключён", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 3de519b26a..2217a98a0a 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -20,6 +20,7 @@ export const tr: Record = { "zcodeDesktop.catalogPending": "Sağlayıcı etkin, ancak Codex kataloğu hazır değil. Yeniden deneyin; eksik modeller için özel görünürlük filtrelerini kontrol edin.", "zcodeDesktop.retryActivation": "Etkinleştirmeyi yeniden dene", "zcodeDesktop.restartNotice": "Modeller görünmüyorsa görevler bitince Codex’i yeniden başlatın. Hiçbir işlem otomatik yeniden başlatılmaz.", + "zcodeDesktop.dashboardRequired": "Bu işlem, loopback’e bağlı panelde veya eşleştirilmiş hub’da tarayıcı oturumu gerektirir. Yalnızca yönetici jetonu Desktop kurulumuna yetki vermez.", "zcodeDesktop.intro": "ZCode Desktop bağlantısı yerel sağlayıcıyı etkinleştirip modellerini Codex’e ekler. Varsayılanlar değişmez; çıkarım yapılmaz.", "zcodeDesktop.connected": "ZCode hazır · sağlayıcı etkin · modeller yayımlandı", "zcodeDesktop.notConnected": "Desktop bağlı değil", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index f2d9b2b1f3..5c1319132c 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -17,6 +17,7 @@ export const zhTW: Record = { "zcodeDesktop.catalogPending": "供應商已啟用,但 Codex 目錄尚未就緒。請重試;若模型仍缺失,請檢查自訂可見性篩選。", "zcodeDesktop.retryActivation": "重試啟用", "zcodeDesktop.restartNotice": "若 Codex 未顯示新模型,請於任務結束後重新啟動 Codex。不會自動重新啟動任何程序。", + "zcodeDesktop.dashboardRequired": "此操作需要綁定至迴路位址的儀表板或已配對 Hub 的瀏覽器工作階段。僅管理員權杖無法授權 Desktop 設定。", "zcodeDesktop.intro": "連接 ZCode Desktop 將啟用本機供應商並將模型發佈至 Codex。不變更預設設定,也不執行推論。", "zcodeDesktop.connected": "ZCode 已就緒 · 供應商已啟用 · 模型已發佈", "zcodeDesktop.notConnected": "Desktop 未連接", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index ea3ca80b83..736be32a8a 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -19,6 +19,7 @@ export const zh: Record = { "zcodeDesktop.catalogPending": "提供商已启用,但 Codex 目录尚未就绪。请重试;若模型仍缺失,请检查自定义可见性筛选。", "zcodeDesktop.retryActivation": "重试启用", "zcodeDesktop.restartNotice": "若 Codex 未显示新模型,请在任务结束后重启 Codex。不会自动重启任何进程。", + "zcodeDesktop.dashboardRequired": "此操作需要绑定到回环地址的仪表板或已配对 Hub 的浏览器会话。仅管理员令牌无法授权 Desktop 设置。", "zcodeDesktop.intro": "连接 ZCode Desktop 将启用本地提供商并将模型发布到 Codex。不更改默认设置,也不执行推理。", "zcodeDesktop.connected": "ZCode 已就绪 · 提供商已启用 · 模型已发布", "zcodeDesktop.notConnected": "Desktop 未连接", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 18536a0580..46d65eba93 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -126,3 +126,16 @@ test("partial activation remains visible and retries without a second protocol c expect(requests.filter(r => r.path.endsWith("/connect") || r.path.endsWith("/test"))).toHaveLength(0); expect(host.textContent).toContain("No processes will be restarted automatically"); }); + +test("admin-token setup refusal shows browser-session guidance, not success", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (_input: RequestInfo | URL, options?: RequestInit) => + options?.method === "POST" ? Response.json({ error: "dashboard_required" }, { status: 403 }) : + Response.json({ connected: false, activation: "disconnected", runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [] }) + }); + await mountPane(); + await click(host.querySelector('input[type="checkbox"]')!); + await click(button("Connect Desktop")); + expect(host.querySelector('[role="alert"]')?.textContent).toContain("browser session"); + expect(closeCalls).toBe(0); + expect(host.textContent).not.toContain("ZCode ready"); +}); diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index c62603771c..5e5885e15d 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -42,7 +42,7 @@ describe("ZCode Desktop management consent", () => { for (const principal of [undefined, "admin-token"] as const) { test(`rejects native execution configuration from ${principal ?? "missing"} principal`, async () => { const f = fixture(); - for (const action of ["connect", "disconnect", "test"]) { + for (const action of ["connect", "activate", "disconnect", "test"]) { const response = await handleZcodeDesktopRoutes(context(`/api/zcode-desktop/${action}`, { consent: true, runtime: "/installed/ZCode", workspace: "/project" }, principal), f.deps); expect(response?.status).toBe(403); } @@ -122,3 +122,23 @@ test("a different provider occupying zcode is never overwritten", async () => { expect((await activateDesktopProvider(ctx, { ...status, connected: true }, f.deps.readDesktopCatalogSlugs)).activation).toBe("provider_pending"); expect(ctx.config.providers).toEqual(before); }); + +test("activation retry is consent-gated and never repeats the protocol probe", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/activate", { + consent: true, runtime: "/installed/ZCode", workspace: "/project", + }, "gui-session"); + f.deps.desktopStatus = () => ({ ...status, connected: true }); + const response = await handleZcodeDesktopRoutes(ctx, f.deps); + expect(await response?.json()).toMatchObject({ activation: "ready" }); + expect(f.calls()).toBe(0); +}); + +test("an existing renamed ZCode provider is reused without adding canonical zcode", async () => { + const ctx = context("/api/zcode-desktop/activate", {}, "gui-session"); + ctx.config.providers.desktop = { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", note: "keep" }; + const slugs = models.map(m => "desktop/" + m.id.replaceAll("/", "-")); + const result = await activateDesktopProvider(ctx, { ...status, connected: true }, () => slugs); + expect(result).toMatchObject({ activation: "ready", providerName: "desktop" }); + expect(ctx.config.providers.zcode).toBeUndefined(); + expect(ctx.config.providers.desktop?.note).toBe("keep"); +}); From c4d47ad0c760f75005fabb54540c2cf2e007923e Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 12:49:46 -0300 Subject: [PATCH 07/43] fix(zcode): resolve cold catalog routes and preflight sandbox policy --- .../src/content/docs/guides/zcode-agent.md | 15 ++++++++ gui/src/components/ZcodeDesktopPane.tsx | 1 + gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/tests/zcode-desktop-pane.test.tsx | 13 +++++++ src/adapters/zcode/desktop-sandbox.ts | 28 ++++++++++++++ src/adapters/zcode/desktop.ts | 16 ++++++++ src/adapters/zcode/quota.ts | 2 + src/router.ts | 4 ++ tests/codex-integration/slug-codec.test.ts | 38 +++++++++++++++++++ tests/providers/zcode-desktop.test.ts | 13 +++++++ 18 files changed, 139 insertions(+) create mode 100644 src/adapters/zcode/desktop-sandbox.ts diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 8f166e62f0..666688931c 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -292,3 +292,18 @@ are preserved; if they hide the connected models, the catalog state remains inco If a running Codex instance does not pick up the models, restart it after its tasks finish. OpenCodex does not terminate or restart Codex processes automatically. + + +### Sandbox policy and cold starts + +Detection runs a minimal Bubblewrap sandbox from the OpenCodex server itself, without +loading your profile or sending inference. If it reports `sandbox_unavailable`, ask +your administrator to review user namespace and AppArmor policy for that service. +A successful probe from a terminal or another application does not prove that the +server has the same permissions. OpenCodex never disables isolation or changes host +security policy automatically. After a policy correction, select **Detect again**. + +Managed Desktop native model IDs also supply routing after a cold restart, before +live discovery fills the cache. You do not need to add them manually to provider +settings. Existing model filters and defaults remain unchanged; ambiguous aliases +are still rejected instead of guessed. diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index 16db308cc3..0e0eb09a45 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -60,6 +60,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: } catch { setError("workspace_invalid"); } }; const problem = error === "desktop_missing" ? t("zcodeDesktop.missing") + : error === "sandbox_unavailable" ? t("zcodeDesktop.sandboxUnavailable") : error === "sandbox_missing" ? t("zcodeDesktop.sandboxMissing") : error === "dashboard_required" ? t("zcodeDesktop.dashboardRequired") : error === "node_missing" ? t("zcodeDesktop.nodeMissing") diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 8ad4e72de0..3c00ee51fa 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -41,6 +41,7 @@ export const de: Record = { "zcodeDesktop.test": "Mit einer Anfrage testen", "zcodeDesktop.testPassed": "ZCode hat erfolgreich geantwortet.", "zcodeDesktop.missing": "Öffnen Sie ZCode Desktop auf dem Proxy-Computer und starten Sie die Erkennung erneut, oder wählen Sie den Installationsordner.", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap kann im Sicherheitskontext des Servers keine Sandbox erstellen. Lassen Sie User-Namespaces und AppArmor prüfen und starten Sie die Erkennung erneut. Die Isolation bleibt aktiv.", "zcodeDesktop.sandboxMissing": "Installieren Sie bubblewrap mit dem Linux-Paketmanager für die Isolation und starten Sie die Erkennung erneut.", "zcodeDesktop.nodeMissing": "Installieren Sie eine mit ZCode kompatible Node.js-Version und starten Sie die Erkennung erneut.", "zcodeDesktop.nodeIncompatible": "ZCode Desktop benötigt Node.js 24 oder neuer mit SQLite-Unterstützung. Fügen Sie eine kompatible Version zum PATH von OpenCodex hinzu, starten Sie den Dienst oder das Terminal neu und wiederholen Sie die Erkennung.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 35ef6c60fe..bbbbe6c291 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -42,6 +42,7 @@ export const en = { "zcodeDesktop.test": "Test with one request", "zcodeDesktop.testPassed": "ZCode answered successfully.", "zcodeDesktop.missing": "Open ZCode Desktop on the proxy computer, then detect again. You can also select its installed or extracted application folder.", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap cannot create the required sandbox in the server’s security context. Ask your administrator to check user namespace and AppArmor policy, then detect again. Isolation will not be disabled.", "zcodeDesktop.sandboxMissing": "Bubblewrap is required for isolation. Install bubblewrap using your Linux package manager, then detect again.", "zcodeDesktop.nodeMissing": "Node.js is required by the ZCode runtime. Install a compatible Node.js version, then detect again.", "zcodeDesktop.nodeIncompatible": "ZCode Desktop requires Node.js 24 or newer with SQLite support. Add a compatible Node.js to the PATH used by OpenCodex, restart its service or terminal, then detect again.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 2e15e9fc51..a036d1f1d3 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -40,6 +40,7 @@ export const fr: Record = { "zcodeDesktop.test": "Tester avec une requête", "zcodeDesktop.testPassed": "ZCode a répondu correctement.", "zcodeDesktop.missing": "Ouvrez ZCode Desktop sur l’ordinateur du proxy, puis relancez la détection, ou indiquez son dossier installé ou extrait.", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap ne peut pas créer le bac à sable dans le contexte de sécurité du serveur. Faites vérifier les espaces de noms utilisateur et AppArmor, puis relancez la détection. L’isolation ne sera pas désactivée.", "zcodeDesktop.sandboxMissing": "Installez bubblewrap avec le gestionnaire de paquets Linux pour activer l’isolation, puis relancez la détection.", "zcodeDesktop.nodeMissing": "Installez une version de Node.js compatible avec ZCode, puis relancez la détection.", "zcodeDesktop.nodeIncompatible": "ZCode Desktop nécessite Node.js 24 ou ultérieur avec SQLite. Ajoutez une version compatible au PATH utilisé par OpenCodex, redémarrez le service ou le terminal, puis relancez la détection.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 6fdc3183ab..c89a813956 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -40,6 +40,7 @@ export const ja: Record = { "zcodeDesktop.test": "1回のリクエストでテスト", "zcodeDesktop.testPassed": "ZCode が正常に応答しました。", "zcodeDesktop.missing": "プロキシのコンピューターで ZCode Desktop を開いて再検出するか、インストールまたは展開先を選択してください。", + "zcodeDesktop.sandboxUnavailable": "サーバーのセキュリティコンテキストで Bubblewrap の隔離環境を作成できません。管理者にユーザー名前空間と AppArmor の設定を確認してもらい、再検出してください。隔離は無効化されません。", "zcodeDesktop.sandboxMissing": "隔離には Bubblewrap が必要です。Linux のパッケージマネージャーでインストールして再検出してください。", "zcodeDesktop.nodeMissing": "ZCode 対応の Node.js をインストールして再検出してください。", "zcodeDesktop.nodeIncompatible": "ZCode Desktop には SQLite 対応の Node.js 24 以降が必要です。OpenCodex が使う PATH に対応版を追加し、サービスまたはターミナルを再起動して再検出してください。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index f3decba37a..52eaddf8d0 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -40,6 +40,7 @@ export const ko: Record = { "zcodeDesktop.test": "요청 한 번으로 테스트", "zcodeDesktop.testPassed": "ZCode가 정상적으로 응답했습니다.", "zcodeDesktop.missing": "프록시 컴퓨터에서 ZCode Desktop을 열고 다시 감지하거나 설치 또는 압축 해제 폴더를 선택하세요.", + "zcodeDesktop.sandboxUnavailable": "서버 보안 컨텍스트에서 Bubblewrap 격리 환경을 만들 수 없습니다. 관리자에게 사용자 네임스페이스와 AppArmor 정책 확인을 요청한 뒤 다시 감지하세요. 격리는 비활성화되지 않습니다.", "zcodeDesktop.sandboxMissing": "격리를 위해 Linux 패키지 관리자로 bubblewrap을 설치한 뒤 다시 감지하세요.", "zcodeDesktop.nodeMissing": "ZCode와 호환되는 Node.js를 설치한 뒤 다시 감지하세요.", "zcodeDesktop.nodeIncompatible": "ZCode Desktop에는 SQLite를 지원하는 Node.js 24 이상이 필요합니다. OpenCodex의 PATH에 호환 버전을 추가하고 서비스 또는 터미널을 다시 시작한 후 다시 감지하세요.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 94001167d9..efa9aacc91 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -40,6 +40,7 @@ export const ru: Record = { "zcodeDesktop.test": "Проверить одним запросом", "zcodeDesktop.testPassed": "ZCode успешно ответил.", "zcodeDesktop.missing": "Откройте ZCode Desktop на компьютере прокси и повторите поиск либо выберите папку установки или распаковки.", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap не может создать изоляцию в контексте безопасности сервера. Попросите администратора проверить пользовательские пространства имён и AppArmor, затем повторите поиск. Изоляция не будет отключена.", "zcodeDesktop.sandboxMissing": "Для изоляции установите bubblewrap через менеджер пакетов Linux и повторите поиск.", "zcodeDesktop.nodeMissing": "Установите совместимую с ZCode версию Node.js и повторите поиск.", "zcodeDesktop.nodeIncompatible": "ZCode Desktop требует Node.js 24 или новее с SQLite. Добавьте совместимую версию в PATH OpenCodex, перезапустите службу или терминал и повторите обнаружение.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 2217a98a0a..ae9e56d2ba 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -41,6 +41,7 @@ export const tr: Record = { "zcodeDesktop.test": "Bir istekle test et", "zcodeDesktop.testPassed": "ZCode başarıyla yanıt verdi.", "zcodeDesktop.missing": "Proxy bilgisayarında ZCode Desktop açıp yeniden algılayın veya kurulum ya da çıkarılmış uygulama klasörünü seçin.", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap sunucunun güvenlik bağlamında yalıtım oluşturamıyor. Yöneticinizden kullanıcı ad alanı ve AppArmor politikasını kontrol etmesini isteyip yeniden algılayın. Yalıtım kapatılmayacaktır.", "zcodeDesktop.sandboxMissing": "Yalıtım için Linux paket yöneticisiyle bubblewrap kurup yeniden algılayın.", "zcodeDesktop.nodeMissing": "ZCode ile uyumlu Node.js sürümünü kurup yeniden algılayın.", "zcodeDesktop.nodeIncompatible": "ZCode Desktop, SQLite destekli Node.js 24 veya üzerini gerektirir. Uyumlu sürümü OpenCodex PATH ortamına ekleyin, hizmeti veya terminali yeniden başlatıp tekrar algılayın.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 5c1319132c..1713c339e8 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -38,6 +38,7 @@ export const zhTW: Record = { "zcodeDesktop.test": "傳送一次測試請求", "zcodeDesktop.testPassed": "ZCode 已成功回應。", "zcodeDesktop.missing": "請在代理所在電腦開啟 ZCode Desktop 後重新偵測,或選取安裝或解壓縮目錄。", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap 無法在伺服器的安全環境中建立隔離。請管理員檢查使用者命名空間與 AppArmor 政策後重新偵測。不會停用隔離。", "zcodeDesktop.sandboxMissing": "隔離需要 Bubblewrap。請透過 Linux 套件管理員安裝 bubblewrap 後重新偵測。", "zcodeDesktop.nodeMissing": "請安裝與 ZCode 相容的 Node.js 版本後重新偵測。", "zcodeDesktop.nodeIncompatible": "ZCode Desktop 需要支援 SQLite 的 Node.js 24 或更新版本。請將相容版本加入 OpenCodex 使用的 PATH,重新啟動服務或終端機後再次偵測。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 736be32a8a..1a7669f876 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -40,6 +40,7 @@ export const zh: Record = { "zcodeDesktop.test": "发送一次测试请求", "zcodeDesktop.testPassed": "ZCode 已成功响应。", "zcodeDesktop.missing": "请在代理所在计算机上打开 ZCode Desktop 后重新检测,或选择其安装或解压目录。", + "zcodeDesktop.sandboxUnavailable": "Bubblewrap 无法在服务器的安全上下文中创建隔离环境。请管理员检查用户命名空间和 AppArmor 策略,然后重新检测。不会禁用隔离。", "zcodeDesktop.sandboxMissing": "隔离需要 Bubblewrap。请通过 Linux 包管理器安装 bubblewrap 后重新检测。", "zcodeDesktop.nodeMissing": "请安装与 ZCode 兼容的 Node.js 版本后重新检测。", "zcodeDesktop.nodeIncompatible": "ZCode Desktop 需要支持 SQLite 的 Node.js 24 或更新版本。请将兼容版本加入 OpenCodex 使用的 PATH,重启服务或终端后重新检测。", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 46d65eba93..3d785821df 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -139,3 +139,16 @@ test("admin-token setup refusal shows browser-session guidance, not success", as expect(closeCalls).toBe(0); expect(host.textContent).not.toContain("ZCode ready"); }); + +test("sandbox denial explains server policy and never reports complete success", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async () => Response.json({ + connected: false, issue: "sandbox_unavailable", runtimes: [], runtime: "", workspace: "/project", models: [], + }) }); + await mountPane(); + const alert = host.querySelector('[role="alert"]')!; + expect(alert.textContent).toContain("AppArmor"); + expect(alert.textContent).toContain("server"); + expect(alert.textContent).not.toContain("Permission denied"); + expect(button("Connect Desktop").disabled).toBe(true); + expect(closeCalls).toBe(0); +}); diff --git a/src/adapters/zcode/desktop-sandbox.ts b/src/adapters/zcode/desktop-sandbox.ts new file mode 100644 index 0000000000..2b2d7f002e --- /dev/null +++ b/src/adapters/zcode/desktop-sandbox.ts @@ -0,0 +1,28 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs"; + +// Process-local success only; never persist a probe performed in another security domain. +const successes = new Map(); +export function verifyDesktopSandbox(executable: string): void { + try { + const path = realpathSync(executable), st = statSync(path); + const key = JSON.stringify([path, st.ino, st.mtimeMs, st.ctimeMs]); + if ((successes.get(key) ?? 0) > Date.now()) return; + const args = ["--unshare-all", "--share-net", "--die-with-parent", "--new-session", + "--ro-bind", "/usr", "/usr"]; + for (const path of ["/bin", "/lib", "/lib64", "/sbin"]) { + if (!existsSync(path)) continue; + args.push(...(lstatSync(path).isSymbolicLink() + ? ["--symlink", readlinkSync(path), path] : ["--ro-bind", path, path])); + } + args.push("--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", + "--clearenv", "--chdir", "/", "/usr/bin/true"); + // No vendor, credentials, workspace or inference; diagnostics never leave the child. + const result = spawnSync(path, args, { + env: { PATH: "/usr/bin:/bin" }, stdio: "ignore", timeout: 1500, + }); + if (result.error || result.status !== 0 || result.signal) throw new Error(); + if (successes.size > 32) successes.clear(); + successes.set(key, Date.now() + 10_000); + } catch { throw new Error("sandbox_unavailable"); } +} diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index 10ae976e89..4b912436f0 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; import { getConfigDir } from "../../config/paths"; import { closeZcodeDesktopClients, ZcodeClient } from "./client"; import type { ZcodeSettings } from "./settings"; +import { verifyDesktopSandbox } from "./desktop-sandbox"; import { resolveDesktopNode } from "./desktop-node"; export interface DesktopModel { id: string; providerId: string; modelId: string; label: string; contextWindow?: number } @@ -36,6 +37,20 @@ function readConnection(): Connection | undefined { } catch { return fail("connection_invalid"); } } +/** Public native IDs only: routing must work before live discovery warms the cache. + * No profile reads, process launches, or config mutations on the routing path. */ +export function desktopRoutingModelIds(): string[] { + try { + const connection = readConnection(); + if (!connection?.connected) return []; + return connection.models.flatMap(model => + model && typeof model.id === "string" && typeof model.providerId === "string" + && model.providerId.startsWith("builtin:zai") && typeof model.modelId === "string" + && model.id === `${model.providerId}/${model.modelId}` && !/[\x00-\x20]/.test(model.id) + ? [model.id] : []); + } catch { return []; } // Invalid/revoked state is still rejected by the adapter. +} + function persist(connection: Connection): void { mkdirSync(root(), { recursive: true, mode: 0o700 }); const temp = join(root(), `${randomUUID()}.tmp`); @@ -108,6 +123,7 @@ function prerequisites(): { bwrap: string; node: string } { let node: string; try { node = resolveDesktopNode(); } catch (error) { return fail(error instanceof Error && error.message === "node_missing" ? "node_missing" : "node_incompatible"); } + try { verifyDesktopSandbox(bwrap); } catch { return fail("sandbox_unavailable"); } return { bwrap: realpathSync(bwrap), node: realpathSync(node) }; } diff --git a/src/adapters/zcode/quota.ts b/src/adapters/zcode/quota.ts index 18038ffe92..d57334d919 100644 --- a/src/adapters/zcode/quota.ts +++ b/src/adapters/zcode/quota.ts @@ -1,3 +1,4 @@ +import { verifyDesktopSandbox } from "./desktop-sandbox"; import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs"; @@ -87,6 +88,7 @@ function command(c: QuotaContext): string[] { async function probe(c: QuotaContext): Promise { const bwrap = Bun.which("bwrap"); if (!bwrap) return null; + try { verifyDesktopSandbox(bwrap); } catch { return null; } return new Promise(resolve => { const child = spawn(bwrap, command(c), { stdio: ["ignore", "pipe", "pipe"], env: { PATH: "/usr/bin:/bin" } }); let output = ""; diff --git a/src/router.ts b/src/router.ts index 8528370efb..8687f4099e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,3 +1,4 @@ +import { desktopRoutingModelIds } from "./adapters/zcode/desktop"; import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types"; import { getCombo, @@ -111,6 +112,9 @@ export function knownModelIdsForProvider( ): string[] { const ids = new Set(); for (const id of prov.models ?? []) ids.add(id); + if (prov.adapter === "zcode") { + for (const id of desktopRoutingModelIds()) ids.add(id); + } if (prov.defaultModel) ids.add(prov.defaultModel); const registry = providerMatchesRegistryTransportWithStaticGuards(provName, prov) ? PROVIDER_REGISTRY.find(entry => entry.id === provName) diff --git a/tests/codex-integration/slug-codec.test.ts b/tests/codex-integration/slug-codec.test.ts index 611754ce18..11eba5bec3 100644 --- a/tests/codex-integration/slug-codec.test.ts +++ b/tests/codex-integration/slug-codec.test.ts @@ -1,3 +1,6 @@ +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; // 260718: Codex-facing slug codec for providers whose NATIVE model ids contain "/" // (zenmux `moonshotai/kimi-k3-free`, openrouter `anthropic/...`, nvidia `moonshotai/...`). // Codex's models-manager metadata lookup tolerates exactly one "/", so two-slash slugs @@ -404,3 +407,38 @@ describe("#2491 one selection resolver reports what it actually matched", () => expect(match.exact).toBe("turbo"); }); }); + +test("managed ZCode published slugs route with a cold cache and no configured models", () => { + const home = mkdtempSync(join(tmpdir(), "ocx-zcode-routing-")); + const previous = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = home; + try { + const ids = ["builtin:zai-coding-plan/GLM-5.3", "builtin:zai-coding-plan/GLM-5.3-Flash"]; + const directory = join(home, "zcode-desktop"); mkdirSync(directory); + const connection = { version: 1, connected: true, + generation: "11111111-1111-1111-1111-111111111111", runtime: "/not-launched", + workspace: "/not-opened", models: ids.map(id => ({ + id, providerId: "builtin:zai-coding-plan", modelId: id.split("/")[1], label: id, + })) }; + writeFileSync(join(directory, "connection.json"), JSON.stringify(connection), { mode: 0o600 }); + const config: OcxConfig = { port: 10100, defaultProvider: "openai", + providers: { zcode: { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai" } } }; + const before = JSON.stringify(config); + for (const id of ids) { + clearModelCache(); + const slug = routedSlug("zcode", id); + const route = routeModel(config, slug); + expect(route.providerName).toBe("zcode"); + expect(route.modelId).toBe(id); + } + expect(JSON.stringify(config)).toBe(before); + config.providers.zcode!.models = ["builtin:zai-coding-plan-GLM-5.3"]; + expect(() => routeModel(config, routedSlug("zcode", ids[0]!))).toThrow("ambiguous model id"); + connection.connected = false; + writeFileSync(join(directory, "connection.json"), JSON.stringify(connection), { mode: 0o600 }); + expect(knownModelIdsForProvider("zcode", { adapter: "zcode", baseUrl: "https://zcode.z.ai" })).toEqual([]); + } finally { + if (previous === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previous; + rmSync(home, { recursive: true, force: true }); + } +}); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 379b2a7ffa..6a6b4a747b 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -99,3 +99,16 @@ test("Node selection skips an incompatible nvm prefix and fails safely", () => { if (oldPath === undefined) delete process.env.PATH; else process.env.PATH = oldPath; } }); + +test("sandbox preflight executes a child and hides uid-map diagnostics", async () => { + if (process.platform !== "linux") return; + const { verifyDesktopSandbox } = await import("../../src/adapters/zcode/desktop-sandbox"); + const executable = join(root, "bwrap-probe"); + writeFileSync(executable, "#!/bin/sh\necho 'bwrap: setting up uid map: Permission denied' >&2\nexit 1\n", { mode: 0o755 }); + expect(() => verifyDesktopSandbox(executable)).toThrow("sandbox_unavailable"); + // A failed probe is not cached: an operator correction can be retried immediately. + writeFileSync(executable, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); + expect(() => verifyDesktopSandbox(executable)).not.toThrow(); + writeFileSync(executable, "#!/bin/sh\nexit 1\n", { mode: 0o755 }); + expect(() => verifyDesktopSandbox(executable)).toThrow("sandbox_unavailable"); +}); From 129086daa93c3f0c77ef6db1378864e8df55cb17 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 15:34:19 -0300 Subject: [PATCH 08/43] feat(zcode): run on host by default with optional sandbox --- .../src/content/docs/guides/zcode-agent.md | 57 ++++++++++----- gui/src/components/ZcodeDesktopPane.tsx | 3 +- gui/src/i18n/de.ts | 2 + gui/src/i18n/en.ts | 2 + gui/src/i18n/fr.ts | 2 + gui/src/i18n/ja.ts | 2 + gui/src/i18n/ko.ts | 2 + gui/src/i18n/ru.ts | 2 + gui/src/i18n/tr.ts | 2 + gui/src/i18n/zh-TW.ts | 2 + gui/src/i18n/zh.ts | 2 + gui/tests/zcode-desktop-pane.test.tsx | 17 +++++ src/adapters/zcode/client.ts | 4 +- src/adapters/zcode/desktop-bootstrap.cjs | 28 ++++++-- src/adapters/zcode/desktop.ts | 27 ++++--- src/adapters/zcode/quota-bootstrap.cjs | 18 +++-- src/adapters/zcode/quota.ts | 33 ++++++--- src/providers/registry.ts | 2 +- structure/adapters/registry.md | 8 ++- structure/data-planes/inbound-compat.md | 4 ++ structure/providers/chat-compat.md | 4 ++ structure/providers/cursor.md | 4 ++ structure/runtime.md | 4 ++ structure/transports/inventory.md | 4 ++ structure/transports/responses.md | 4 ++ tests/providers/zcode-desktop.test.ts | 72 ++++++++++++++++++- 26 files changed, 256 insertions(+), 55 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 666688931c..a2e5dd9b7a 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -1,6 +1,6 @@ --- title: ZCode local agent provider -description: Run the official ZCode app server as an explicitly enabled, isolated OpenCodex provider. +description: Run the official ZCode app server as an explicitly enabled OpenCodex provider with optional isolation. --- ## Direction and tool ownership @@ -50,7 +50,7 @@ Desktop conversations, or change its configuration. A saved disconnected state a an older environment-based setup from silently reactivating. Reconnect to refresh the model catalog after changing Desktop's model configuration. -The proxy host needs **Bubblewrap** and a Node.js version compatible with the installed ZCode +The proxy host needs a Node.js version compatible with the installed ZCode runtime. Missing prerequisites and unsupported platforms are shown in the panel. Managed setup is currently Linux-only; Windows/macOS need the advanced launcher flow below. A remote browser's local Desktop is not the proxy host's Desktop: detection and workspace selection operate on the @@ -63,19 +63,38 @@ ZCode's app-server is a stdio child, not a public Desktop TCP endpoint. OpenCode configuration. It starts an owned native session; it does not attach to a conversation already open in Desktop. The original **Integrations → ZCode** configuration export remains separate. -The managed sandbox exposes Desktop configuration and shared credentials **read-only**. A small -bootstrap inside the sandbox adapts the Desktop configuration to the app-server's runtime-model -protocol; provider keys are never returned to the proxy parent, model catalog or management API. -The compatible config exists in a temporary filesystem; ZCode owns its private persistent -session database. OpenCodex neither implements Z.AI authentication nor sends inference HTTP -requests itself. Authentication/configuration changes in Desktop fence off previous native -continuation state. If credentials need updating, do that in Desktop and reconnect. +By default, the managed runtime runs **without an OpenCodex OS sandbox**, with the +permissions of the proxy's operating-system user. It can read and modify files outside the +selected working directory, including sensitive files accessible to that user. Absolute +paths retain their host meaning. The selected workspace is a starting directory, not a +filesystem boundary. This default also applies to previously connected installations after +upgrading. No root elevation or permission-skipping flag is used. -The workspace and private native home are writable; runtime/system files are read-only. The -real user home and host sockets are not mounted. **Network access is shared**, not isolated: -only use trusted prompts/projects, and apply separate egress controls if necessary. ZCode needs -access to its own profile inside this execution context; this is not a guarantee that an agent -can never read its own configuration. No permission-skipping flags are used. +Any isolation must be applied by the harness/operator **where the native ZCode process +executes**. A sandbox in a remote calling client does not automatically constrain this +server-side process. Only use trusted callers and projects. + +OpenCodex still separates bridge-owned runtime state from the Desktop profile to avoid +overwriting Desktop settings. Temporary compatible configs are private and removed when +the child exits normally; the session database remains private and persistent. Separate +state directories do not prevent native tools from accessing other host files. Only official +ZCode handles account and inference traffic; no direct-API fallback is introduced. + +### Optional OpenCodex sandbox + +Set the following in the environment of the OpenCodex service or terminal, then restart +OpenCodex at a safe time: + +```sh +export OCX_ZCODE_SANDBOX=1 +``` + +Bubblewrap is required only when this option is enabled. The existing restricted workspace +mounts and real sandbox preflight apply; failure never falls back to host execution. In this +mode the selected workspace appears as `/workspace`; other host files are not mounted. +Unset the variable to restore the default host execution. Environment changes apply to +existing managed connections without requiring another connection. They do not reconfigure +an advanced operator-supplied launcher. Managed consent and installation/workspace paths are stored privately under `$OPENCODEX_HOME/zcode-desktop/`, separately from provider configuration. Data-plane requests @@ -226,10 +245,10 @@ Quota comes from the **official ZCode Desktop host service**, not from locally counted tokens or a proxy-made Z.AI API request. OpenCodex starts the trusted Desktop executable in its Node runtime mode and calls its existing read-only entitlement RPC. Native ZCode owns authentication and network requests. This -separate short-lived Bubblewrap sandbox has no writable project folder: Desktop -configuration is mounted read-only and copied only into disposable tmpfs so the -native service can perform its normal configuration normalization. Credential -material never leaves that sandbox. No model prompt, quota-reset action, or +separate short-lived host uses a disposable private profile copy so native normalization +does not rewrite Desktop settings. The copy is removed after the process exits. +When `OCX_ZCODE_SANDBOX=1` is enabled, this host additionally uses Bubblewrap and tmpfs. +No credential material is returned in the quota response. No model prompt, quota-reset action, or purchase is requested. Tested with Desktop 3.10.2 on Linux; other private host protocol versions can return unavailable instead of a fabricated quota. @@ -296,7 +315,7 @@ finish. OpenCodex does not terminate or restart Codex processes automatically. ### Sandbox policy and cold starts -Detection runs a minimal Bubblewrap sandbox from the OpenCodex server itself, without +When OCX_ZCODE_SANDBOX=1 is set, detection runs a minimal Bubblewrap sandbox from the OpenCodex server itself, without loading your profile or sending inference. If it reports `sandbox_unavailable`, ask your administrator to review user namespace and AppArmor policy for that service. A successful probe from a terminal or another application does not prove that the diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index 0e0eb09a45..fa9d66f2ea 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useT } from "../i18n/shared"; interface Status { - connected: boolean; issue?: string; runtimes: string[]; runtime: string; workspace: string; + connected: boolean; sandbox?: boolean; issue?: string; runtimes: string[]; runtime: string; workspace: string; activation?: string; providerName?: string; error?: string; models: Array<{ id: string; label: string }>; } @@ -77,6 +77,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: return
ZCode Desktop

{t("zcodeDesktop.intro")}

+

{t(status?.sandbox ? "zcodeDesktop.sandboxAccess" : "zcodeDesktop.hostAccess")}

{status?.connected ? status.activation === "ready" ? t("zcodeDesktop.connected") : status.activation === "provider_pending" ? t("zcodeDesktop.providerPending") : t("zcodeDesktop.catalogPending") : t("zcodeDesktop.notConnected")}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 3c00ee51fa..9dc82d2ca6 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -21,6 +21,8 @@ export const de: Record = { "zcodeDesktop.retryActivation": "Aktivierung wiederholen", "zcodeDesktop.restartNotice": "Falls Codex die Modelle nicht zeigt, starten Sie Codex nach Abschluss Ihrer Aufgaben neu. Kein automatischer Prozessneustart.", "zcodeDesktop.dashboardRequired": "Diese Aktion erfordert eine Browsersitzung über ein an Loopback gebundenes Dashboard oder einen gekoppelten Hub. Ein Admin-Token allein erlaubt keine Desktop-Einrichtung.", + "zcodeDesktop.hostAccess": "ZCode läuft ohne OpenCodex-Sandbox und kann alle Dateien lesen oder ändern, die Benutzerrechte und Harness erlauben. Der Arbeitsordner ist keine Zugriffsgrenze. Dies gilt nach Updates auch für bestehende Verbindungen. Für Isolation OCX_ZCODE_SANDBOX=1 am Proxy setzen und OpenCodex neu starten.", + "zcodeDesktop.sandboxAccess": "Die optionale Bubblewrap-Isolation ist aktiv. Nur der gewählte Arbeitsordner ist eingebunden; andere Hostpfade sind nicht zugänglich.", "zcodeDesktop.intro": "ZCode Desktop verbinden, um den lokalen Anbieter zu aktivieren und seine Modelle in Codex bereitzustellen. Standardwerte bleiben unverändert; keine Inferenz.", "zcodeDesktop.connected": "ZCode bereit · Anbieter aktiviert · Modelle veröffentlicht", "zcodeDesktop.notConnected": "Desktop nicht verbunden", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index bbbbe6c291..bdb4a3d80c 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -22,6 +22,8 @@ export const en = { "zcodeDesktop.retryActivation": "Retry activation", "zcodeDesktop.restartNotice": "If Codex does not show the new models, restart Codex when your tasks finish. No processes will be restarted automatically.", "zcodeDesktop.dashboardRequired": "This action requires a browser session on a loopback-bound dashboard or a paired hub. An admin token alone cannot authorize Desktop setup.", + "zcodeDesktop.hostAccess": "ZCode runs without an OpenCodex sandbox and can read or modify any file your operating-system user and harness allow. The workspace is only a working directory. This also applies to existing connections after upgrading. To enable isolation, set OCX_ZCODE_SANDBOX=1 on the proxy and restart OpenCodex.", + "zcodeDesktop.sandboxAccess": "Optional Bubblewrap isolation is enabled. Only the selected workspace is mounted; host absolute paths outside it are unavailable.", "zcodeDesktop.intro": "Connect ZCode Desktop to enable its local provider and publish its models to Codex. Defaults stay unchanged; no inference is sent.", "zcodeDesktop.connected": "ZCode ready · provider enabled · models published", "zcodeDesktop.notConnected": "Desktop not connected", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index a036d1f1d3..11ec90080c 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -20,6 +20,8 @@ export const fr: Record = { "zcodeDesktop.retryActivation": "Réessayer l’activation", "zcodeDesktop.restartNotice": "Si Codex n’affiche pas les modèles, redémarrez-le après vos tâches. Aucun processus ne sera redémarré automatiquement.", "zcodeDesktop.dashboardRequired": "Cette action nécessite une session navigateur sur un tableau de bord lié à loopback ou un hub appairé. Le jeton administrateur seul ne permet pas la configuration Desktop.", + "zcodeDesktop.hostAccess": "ZCode fonctionne sans bac à sable OpenCodex et peut lire ou modifier tout fichier autorisé par votre utilisateur système et le harness. Le dossier de travail ne limite pas cet accès. Cela concerne aussi les connexions existantes après mise à jour. Pour activer l’isolation, définissez OCX_ZCODE_SANDBOX=1 sur le proxy et redémarrez OpenCodex.", + "zcodeDesktop.sandboxAccess": "L’isolation Bubblewrap optionnelle est active. Seul le dossier de travail choisi est monté ; les autres chemins de l’hôte sont inaccessibles.", "zcodeDesktop.intro": "Connectez ZCode Desktop pour activer son fournisseur local et publier ses modèles dans Codex. Les choix par défaut restent inchangés ; aucune inférence.", "zcodeDesktop.connected": "ZCode prêt · fournisseur activé · modèles publiés", "zcodeDesktop.notConnected": "Desktop non connecté", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index c89a813956..3f91d502b9 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -20,6 +20,8 @@ export const ja: Record = { "zcodeDesktop.retryActivation": "有効化を再試行", "zcodeDesktop.restartNotice": "Codex にモデルが表示されない場合は、作業終了後に Codex を再起動してください。自動再起動は行いません。", "zcodeDesktop.dashboardRequired": "この操作にはループバックにバインドされたダッシュボードまたはペアリング済みハブのブラウザーセッションが必要です。管理トークンだけでは Desktop を設定できません。", + "zcodeDesktop.hostAccess": "ZCode は OpenCodex のサンドボックスなしで実行され、OS ユーザーとハーネスが許可するファイルを読み書きできます。作業ディレクトリはアクセス制限ではありません。更新後の既存接続にも適用されます。隔離するにはプロキシで OCX_ZCODE_SANDBOX=1 を設定し、OpenCodex を再起動してください。", + "zcodeDesktop.sandboxAccess": "任意の Bubblewrap 隔離が有効です。選択した作業ディレクトリのみがマウントされ、それ以外のホストパスにはアクセスできません。", "zcodeDesktop.intro": "ZCode Desktop を接続するとローカルプロバイダーが有効になり、Codex にモデルを公開します。既定値は変更せず、推論も送信しません。", "zcodeDesktop.connected": "ZCode 準備完了 · プロバイダー有効 · モデル公開済み", "zcodeDesktop.notConnected": "Desktop 未接続", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 52eaddf8d0..0606208b99 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -20,6 +20,8 @@ export const ko: Record = { "zcodeDesktop.retryActivation": "활성화 재시도", "zcodeDesktop.restartNotice": "Codex에 모델이 표시되지 않으면 작업 완료 후 Codex를 다시 시작하세요. 프로세스를 자동으로 다시 시작하지 않습니다.", "zcodeDesktop.dashboardRequired": "이 작업에는 루프백에 바인딩된 대시보드 또는 페어링된 허브의 브라우저 세션이 필요합니다. 관리자 토큰만으로는 Desktop을 설정할 수 없습니다.", + "zcodeDesktop.hostAccess": "ZCode는 OpenCodex 샌드박스 없이 실행되며 OS 사용자와 하네스가 허용하는 파일을 읽거나 수정할 수 있습니다. 작업 폴더는 접근 경계가 아닙니다. 업데이트 후 기존 연결에도 적용됩니다. 격리하려면 프록시에 OCX_ZCODE_SANDBOX=1을 설정하고 OpenCodex를 재시작하세요.", + "zcodeDesktop.sandboxAccess": "선택적 Bubblewrap 격리가 활성화되었습니다. 선택한 작업 폴더만 마운트되며 다른 호스트 경로에는 접근할 수 없습니다.", "zcodeDesktop.intro": "ZCode Desktop을 연결하면 로컬 공급자를 활성화하고 Codex에 모델을 게시합니다. 기본값은 유지되며 추론은 실행하지 않습니다.", "zcodeDesktop.connected": "ZCode 준비 완료 · 공급자 활성화 · 모델 게시 완료", "zcodeDesktop.notConnected": "Desktop 연결 안 됨", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index efa9aacc91..f1fcf55f5b 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -20,6 +20,8 @@ export const ru: Record = { "zcodeDesktop.retryActivation": "Повторить активацию", "zcodeDesktop.restartNotice": "Если модели не появились, перезапустите Codex после завершения задач. Автоматического перезапуска процессов не будет.", "zcodeDesktop.dashboardRequired": "Требуется сеанс браузера на панели, привязанной к loopback, или сопряжённом хабе. Одного токена администратора недостаточно для настройки Desktop.", + "zcodeDesktop.hostAccess": "ZCode работает без песочницы OpenCodex и может читать или менять любые файлы, разрешённые системному пользователю и средой запуска. Рабочий каталог не ограничивает доступ. Это относится и к существующим подключениям после обновления. Для изоляции задайте OCX_ZCODE_SANDBOX=1 у прокси и перезапустите OpenCodex.", + "zcodeDesktop.sandboxAccess": "Дополнительная изоляция Bubblewrap включена. Доступен только выбранный рабочий каталог; остальные пути хоста недоступны.", "zcodeDesktop.intro": "Подключение ZCode Desktop включает локального провайдера и публикует модели в Codex. Значения по умолчанию сохраняются; генерация не запускается.", "zcodeDesktop.connected": "ZCode готов · провайдер включён · модели опубликованы", "zcodeDesktop.notConnected": "Desktop не подключён", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index ae9e56d2ba..9355374671 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -21,6 +21,8 @@ export const tr: Record = { "zcodeDesktop.retryActivation": "Etkinleştirmeyi yeniden dene", "zcodeDesktop.restartNotice": "Modeller görünmüyorsa görevler bitince Codex’i yeniden başlatın. Hiçbir işlem otomatik yeniden başlatılmaz.", "zcodeDesktop.dashboardRequired": "Bu işlem, loopback’e bağlı panelde veya eşleştirilmiş hub’da tarayıcı oturumu gerektirir. Yalnızca yönetici jetonu Desktop kurulumuna yetki vermez.", + "zcodeDesktop.hostAccess": "ZCode, OpenCodex sanal alanı olmadan çalışır; işletim sistemi kullanıcınızın ve harness’in izin verdiği dosyaları okuyabilir veya değiştirebilir. Çalışma klasörü bir erişim sınırı değildir. Güncellemeden sonra mevcut bağlantılar için de geçerlidir. Yalıtım için proxy üzerinde OCX_ZCODE_SANDBOX=1 ayarlayıp OpenCodex’i yeniden başlatın.", + "zcodeDesktop.sandboxAccess": "İsteğe bağlı Bubblewrap yalıtımı açık. Yalnızca seçilen çalışma klasörü bağlanır; diğer ana makine yollarına erişilemez.", "zcodeDesktop.intro": "ZCode Desktop bağlantısı yerel sağlayıcıyı etkinleştirip modellerini Codex’e ekler. Varsayılanlar değişmez; çıkarım yapılmaz.", "zcodeDesktop.connected": "ZCode hazır · sağlayıcı etkin · modeller yayımlandı", "zcodeDesktop.notConnected": "Desktop bağlı değil", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 1713c339e8..48a52dc909 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -18,6 +18,8 @@ export const zhTW: Record = { "zcodeDesktop.retryActivation": "重試啟用", "zcodeDesktop.restartNotice": "若 Codex 未顯示新模型,請於任務結束後重新啟動 Codex。不會自動重新啟動任何程序。", "zcodeDesktop.dashboardRequired": "此操作需要綁定至迴路位址的儀表板或已配對 Hub 的瀏覽器工作階段。僅管理員權杖無法授權 Desktop 設定。", + "zcodeDesktop.hostAccess": "ZCode 預設不使用 OpenCodex 沙箱,可讀寫作業系統使用者與 harness 允許存取的任何檔案。工作目錄不是存取邊界。升級後現有連線也適用。如需隔離,請在代理上設定 OCX_ZCODE_SANDBOX=1 並重新啟動 OpenCodex。", + "zcodeDesktop.sandboxAccess": "已啟用選用的 Bubblewrap 隔離。僅掛載選定的工作目錄,無法存取其他主機路徑。", "zcodeDesktop.intro": "連接 ZCode Desktop 將啟用本機供應商並將模型發佈至 Codex。不變更預設設定,也不執行推論。", "zcodeDesktop.connected": "ZCode 已就緒 · 供應商已啟用 · 模型已發佈", "zcodeDesktop.notConnected": "Desktop 未連接", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 1a7669f876..84cb6fbce8 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -20,6 +20,8 @@ export const zh: Record = { "zcodeDesktop.retryActivation": "重试启用", "zcodeDesktop.restartNotice": "若 Codex 未显示新模型,请在任务结束后重启 Codex。不会自动重启任何进程。", "zcodeDesktop.dashboardRequired": "此操作需要绑定到回环地址的仪表板或已配对 Hub 的浏览器会话。仅管理员令牌无法授权 Desktop 设置。", + "zcodeDesktop.hostAccess": "ZCode 默认不使用 OpenCodex 沙箱,可读写操作系统用户和 harness 允许访问的任何文件。工作目录不是访问边界。升级后现有连接也适用。如需隔离,请在代理上设置 OCX_ZCODE_SANDBOX=1 并重启 OpenCodex。", + "zcodeDesktop.sandboxAccess": "已启用可选的 Bubblewrap 隔离。仅挂载所选工作目录,无法访问其他主机路径。", "zcodeDesktop.intro": "连接 ZCode Desktop 将启用本地提供商并将模型发布到 Codex。不更改默认设置,也不执行推理。", "zcodeDesktop.connected": "ZCode 已就绪 · 提供商已启用 · 模型已发布", "zcodeDesktop.notConnected": "Desktop 未连接", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 3d785821df..ee08150eab 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -152,3 +152,20 @@ test("sandbox denial explains server policy and never reports complete success", expect(button("Connect Desktop").disabled).toBe(true); expect(closeCalls).toBe(0); }); + +test("default host access is disclosed before connection consent", async () => { + await mountPane(); + expect(host.querySelector('[role="note"]')?.textContent).toContain("without an OpenCodex sandbox"); + expect(host.querySelector('[role="note"]')?.textContent).toContain("existing connections"); + expect(host.querySelector('[role="note"]')?.textContent).toContain("OCX_ZCODE_SANDBOX=1"); + expect(host.querySelector('input[type="checkbox"]')!.checked).toBe(false); +}); + +test("explicit sandbox mode shows its filesystem boundary", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async () => Response.json({ + connected: false, sandbox: true, runtimes: [], runtime: "", workspace: "/project", models: [], + }) }); + await mountPane(); + expect(host.querySelector('[role="note"]')?.textContent).toContain("Only the selected workspace"); + expect(host.querySelector('[role="note"]')?.textContent).not.toContain("without an OpenCodex sandbox"); +}); diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index e88738fb06..9f0a09d584 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -37,7 +37,7 @@ export class ZcodeClient { // Discard vendor diagnostics: stderr may contain account or request data. this.child.stderr.resume(); this.child.stdin.on("error", () => this.fail(new Error("ZCode protocol input closed."))); - this.child.once("error", () => this.fail(new Error("ZCode isolated launcher could not start."))); + this.child.once("error", () => this.fail(new Error("ZCode launcher could not start."))); this.child.once("exit", () => this.fail(new Error("ZCode app server exited before completing the turn."))); this.reading = this.read(); this.detachShutdown = registerOptionalShutdownHook(`zcode-${crypto.randomUUID()}`, () => { void this.close(); }); @@ -74,7 +74,7 @@ export class ZcodeClient { } }); } else if (message.method === "interaction/requestPermission") { // Permission decisions cannot be safely represented by all OpenCodex clients. - // Native non-interactive actions still run under ZCode's edit mode and OS sandbox. + // Native non-interactive actions still run under ZCode's edit mode and any operator/harness sandbox. this.write({ id: message.id, result: { decision: "deny", reason: "Interactive approval is unavailable through this bridge." } }); } else if (message.method === "interaction/requestUserInput") { this.write({ id: message.id, result: { action: "cancel" } }); diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs index f267f6fdee..bec5528704 100644 --- a/src/adapters/zcode/desktop-bootstrap.cjs +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -1,4 +1,4 @@ -// Runs INSIDE the managed OS sandbox. Desktop credentials/configuration are mounted read-only. +// Runs on the host by default, or inside an explicitly enabled OS sandbox. Desktop credentials/configuration are mounted read-only. // Only the official runtime consumes provider keys. The compatible settings file lives in tmpfs, // never in OpenCodex's data directory, management responses, logs or the Desktop profile. const fs = require("node:fs"); @@ -40,15 +40,31 @@ function desktopModelCatalog(config) { } module.exports = { normalizeDesktopConfig, desktopModelCatalog }; if (require.main === module) { + let temporaryHome; + const cleanup = () => { if (temporaryHome) fs.rmSync(temporaryHome, { recursive: true, force: true }); }; + process.on("exit", cleanup); try { - const path = "/desktop/config.json"; + const args = process.argv.slice(2); + const host = args[0] === "--host"; + if (host ? args.length !== 5 || args[4] !== "app-server" : args.length !== 1 || args[0] !== "app-server") throw new Error("unsupported command"); + const path = host ? args[2] : "/desktop/config.json"; if (fs.statSync(path).size > 4 * 1024 * 1024) throw new Error("oversized"); const input = JSON.parse(fs.readFileSync(path, "utf8")); const config = normalizeDesktopConfig(input); - fs.writeFileSync(`${process.env.HOME}/.zcode/cli/config.json`, JSON.stringify(config), { mode: 0o600, flag: "wx" }); - const args = process.argv.slice(2); - if (args.length !== 1 || args[0] !== "app-server") throw new Error("unsupported command"); - const child = spawn("/usr/bin/node", ["/runtime/zcode.cjs", "app-server"], { stdio: ["pipe", "inherit", "inherit"], shell: false }); + let env = process.env; + if (host) { + // Separate runtime state is not filesystem confinement: native tools retain host access. + const stableDb = require("node:path").join(process.env.HOME, ".zcode/cli/db"); + temporaryHome = fs.mkdtempSync(require("node:path").join(process.env.HOME, "turn-")); + fs.mkdirSync(temporaryHome + "/.zcode/cli", { recursive: true, mode: 0o700 }); + fs.symlinkSync(stableDb, temporaryHome + "/.zcode/cli/db", "dir"); + env = { ...process.env, HOME: temporaryHome, XDG_CONFIG_HOME: temporaryHome + "/.config", + XDG_CACHE_HOME: temporaryHome + "/.cache" }; + } + fs.writeFileSync(`${env.HOME}/.zcode/cli/config.json`, JSON.stringify(config), { mode: 0o600, flag: "wx" }); + const child = spawn(host ? process.execPath : "/usr/bin/node", [host ? args[1] : "/runtime/zcode.cjs", "app-server"], { + stdio: ["pipe", "inherit", "inherit"], shell: false, env, ...(host ? { cwd: args[3] } : {}), + }); const lines = require("node:readline").createInterface({ input: process.stdin }); lines.on("line", line => { try { diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index 4b912436f0..c501a60460 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -97,6 +97,7 @@ export function validateDesktopWorkspace(path: string): string { let workspace: string; try { workspace = realpathSync(path); if (!statSync(workspace).isDirectory()) return fail("workspace_invalid"); } catch { return fail("workspace_invalid"); } + if (!desktopSandboxEnabled()) return workspace; const home = realpathSync(homedir()); if (workspace === home || home.startsWith(workspace.endsWith(sep) ? workspace : workspace + sep)) return fail("workspace_invalid"); if (workspace !== resolve(defaultDesktopWorkspace())) { @@ -117,14 +118,17 @@ export function desktopFolders(path?: string) { return { current, parent: current === home ? null : dirname(current), folders }; } -function prerequisites(): { bwrap: string; node: string } { +export const desktopSandboxEnabled = () => process.env.OCX_ZCODE_SANDBOX === "1"; + +function prerequisites(): { bwrap?: string; node: string } { if (process.platform !== "linux") return fail("platform_unsupported"); - const bwrap = Bun.which("bwrap"); if (!bwrap) return fail("sandbox_missing"); + const bwrap = desktopSandboxEnabled() ? Bun.which("bwrap", { PATH: process.env.PATH }) : undefined; + if (desktopSandboxEnabled() && !bwrap) return fail("sandbox_missing"); let node: string; try { node = resolveDesktopNode(); } catch (error) { return fail(error instanceof Error && error.message === "node_missing" ? "node_missing" : "node_incompatible"); } - try { verifyDesktopSandbox(bwrap); } catch { return fail("sandbox_unavailable"); } - return { bwrap: realpathSync(bwrap), node: realpathSync(node) }; + try { if (bwrap) verifyDesktopSandbox(bwrap); } catch { return fail("sandbox_unavailable"); } + return { bwrap: bwrap ? realpathSync(bwrap) : undefined, node: realpathSync(node) }; } function desktopProfile(): { config: string; credentials?: string } { @@ -149,6 +153,12 @@ function settingsFor(connection: Connection): ZcodeSettings { const privateHome = join(root(), "home", connection.generation, profileStamp); const db = join(privateHome, ".zcode/cli/db"); mkdirSync(db, { recursive: true, mode: 0o700 }); + if (!bwrap) return { + command: [node, fileURLToPath(new URL("./desktop-bootstrap.cjs", import.meta.url)), + "--host", runtime, profile.config, workspace], + home: privateHome, workspace, settingsPath: "", + scope: `desktop:${connection.generation}:${profileStamp}:host`, desktopModels: connection.models, + }; const sandboxHome = homedir(); // preserve the official credential cipher's HOME/username identity const args = ["--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--ro-bind", "/usr", "/usr"]; for (const path of ["/bin", "/lib", "/lib64", "/sbin"]) { @@ -168,7 +178,7 @@ function settingsFor(connection: Connection): ZcodeSettings { "--setenv", "ZCODE_DATA_BASE_DIR", "/desktop", "--chdir", "/workspace", "/usr/bin/node", "/bridge/desktop-bootstrap.cjs"); return { command: [bwrap, ...args], home: privateHome, workspace: "/workspace", settingsPath: "", - scope: `desktop:${connection.generation}:${profileStamp}`, desktopModels: connection.models }; + scope: `desktop:${connection.generation}:${profileStamp}:sandbox`, desktopModels: connection.models }; } /** Persisted GUI consent is separate from provider config; data-plane requests cannot set it. */ @@ -192,7 +202,7 @@ export function desktopStatus() { } return { connected: connection?.connected === true && !issue, issue, runtimes, runtime: connection?.runtime ?? runtimes[0] ?? "", workspace: connection?.workspace ?? defaultDesktopWorkspace(), - models: connection?.models ?? [], platform: process.platform }; + models: connection?.models ?? [], sandbox: desktopSandboxEnabled(), platform: process.platform }; } let connecting = false; @@ -202,7 +212,8 @@ export async function connectDesktop(runtime: string, workspace: string): Promis try { const connection: Connection = { version: 1, connected: true, generation: randomUUID(), runtime: resolveDesktopRuntime(runtime), workspace: validateDesktopWorkspace(workspace), models: [] }; - const client = new ZcodeClient(settingsFor(connection)); + const settings = settingsFor(connection); + const client = new ZcodeClient(settings); try { const result = await client.request("opencodex/desktopModels", {}, 15_000); if (!Array.isArray(result.models) || !result.models.length || result.models.length > 1000) return fail("models_missing"); @@ -214,7 +225,7 @@ export async function connectDesktop(runtime: string, workspace: string): Promis })); if (!connection.models.length) return fail("models_missing"); // Official protocol readiness only. This does not send a prompt or spend inference quota. - await client.request("workspace/readState", { workspace: { workspacePath: "/workspace", workspaceKey: "/workspace" } }, 15_000); + await client.request("workspace/readState", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace } }, 15_000); } finally { await client.close(); } await closeZcodeDesktopClients(); persist(connection); diff --git a/src/adapters/zcode/quota-bootstrap.cjs b/src/adapters/zcode/quota-bootstrap.cjs index 597aa10e93..637d6d94dc 100644 --- a/src/adapters/zcode/quota-bootstrap.cjs +++ b/src/adapters/zcode/quota-bootstrap.cjs @@ -1,23 +1,27 @@ -// Runs only inside the quota sandbox. Native host and RPC modules are unmodified official ZCode. +// Runs in a private state directory; an OS sandbox is optional. Native host and RPC modules are unmodified official ZCode. // No HTTP implementation, quota reset or model inference here. All diagnostics stay off stdout. const {EventEmitter}=require('node:events'); const fs=require('node:fs'); const parent=new EventEmitter(); parent.postMessage=()=>{}; process.parentPort=parent; const output=process.stdout.write.bind(process.stdout); process.stdout.write=process.stderr.write.bind(process.stderr); const originalLog=value=>output(value+'\n'); -fs.mkdirSync(process.env.HOME+'/.zcode/v2',{recursive:true}); -const input=JSON.parse(fs.readFileSync('/desktop/config.json','utf8')); +const host=process.env.OCX_ZCODE_QUOTA_HOST==='1'; +const stateHome=host?process.env.ZCODE_DATA_BASE_DIR:process.env.HOME; +const profile=host?process.env.OCX_ZCODE_QUOTA_CONFIG:'/desktop/config.json'; +const credentials=host?process.env.OCX_ZCODE_QUOTA_CREDENTIALS:'/desktop/credentials.json'; +fs.mkdirSync(stateHome+'/.zcode/v2',{recursive:true}); +const input=JSON.parse(fs.readFileSync(profile,'utf8')); if(process.argv[2]==='advanced') { const raw=input.provider?.[process.argv[3]]; const url=new URL(raw?.options?.baseURL); if(url.protocol!=='https:' || url.hostname!=='api.z.ai' || url.username || url.password || !raw.options.apiKey) throw new Error('unsupported profile'); - fs.writeFileSync(process.env.HOME+'/.zcode/v2/config.json',JSON.stringify({provider:{'builtin:zai-coding-plan':{...raw,name:'Z.ai - Coding Plan',enabled:true}}}),{mode:0o600}); + fs.writeFileSync(stateHome+'/.zcode/v2/config.json',JSON.stringify({provider:{'builtin:zai-coding-plan':{...raw,name:'Z.ai - Coding Plan',enabled:true}}}),{mode:0o600}); } else { - fs.copyFileSync('/desktop/config.json',process.env.HOME+'/.zcode/v2/config.json'); - if(fs.existsSync('/desktop/credentials.json')) fs.copyFileSync('/desktop/credentials.json',process.env.HOME+'/.zcode/v2/credentials.json'); + fs.copyFileSync(profile,stateHome+'/.zcode/v2/config.json'); + if(credentials && fs.existsSync(credentials)) fs.copyFileSync(credentials,stateHome+'/.zcode/v2/credentials.json'); } setTimeout(()=>{originalLog(JSON.stringify({error:'timeout'}));process.exit(1)},40000); (async()=>{ - const dir='/zcode/resources/app.asar/out/host'; + const dir=(host?process.env.OCX_ZCODE_QUOTA_RUNTIME:'/zcode')+'/resources/app.asar/out/host'; let classes; for(const name of fs.readdirSync(dir).filter(n=>n.startsWith('chunk-')&&n.endsWith('.js'))){ const source=fs.readFileSync(dir+'/'+name,'utf8');if(source.includes('"ChannelClient"')&&source.includes('"MessagePortProtocol"')){classes=Object.values(await import(dir+'/'+name));break;} diff --git a/src/adapters/zcode/quota.ts b/src/adapters/zcode/quota.ts index d57334d919..6970a38959 100644 --- a/src/adapters/zcode/quota.ts +++ b/src/adapters/zcode/quota.ts @@ -1,13 +1,14 @@ import { verifyDesktopSandbox } from "./desktop-sandbox"; import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; -import { existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, existsSync, lstatSync, readlinkSync, realpathSync, statSync } from "node:fs"; import { dirname, join, sep } from "node:path"; import { homedir } from "node:os"; import { fileURLToPath } from "node:url"; import type { OcxProviderConfig } from "../../types"; import type { ProviderQuota } from "../../providers/quota-types"; -import { desktopStatus, resolveDesktopRuntime } from "./desktop"; +import { getConfigDir } from "../../config/paths"; +import { desktopSandboxEnabled, desktopStatus, resolveDesktopRuntime } from "./desktop"; import { loadZcodeSettings } from "./settings"; export interface QuotaContext { identity: string; runtimeRoot: string; config: string; credentials?: string; sourceProvider: string; managed: boolean } @@ -86,11 +87,26 @@ function command(c: QuotaContext): string[] { /** One short-lived official host, without a writable user workspace or persistent credentials. */ async function probe(c: QuotaContext): Promise { - const bwrap = Bun.which("bwrap"); - if (!bwrap) return null; - try { verifyDesktopSandbox(bwrap); } catch { return null; } - return new Promise(resolve => { - const child = spawn(bwrap, command(c), { stdio: ["ignore", "pipe", "pipe"], env: { PATH: "/usr/bin:/bin" } }); + const sandbox = desktopSandboxEnabled(); + const bwrap = sandbox ? Bun.which("bwrap", { PATH: process.env.PATH }) : undefined; + if (sandbox && !bwrap) return null; + try { if (bwrap) verifyDesktopSandbox(bwrap); } catch { return null; } + let temporaryHome: string | undefined; + try { + if (!sandbox) { + const directory = join(getConfigDir(), "zcode-desktop", "quota"); + mkdirSync(directory, { recursive: true, mode: 0o700 }); + temporaryHome = mkdtempSync(join(directory, "probe-")); + } + return await new Promise(resolve => { + const child = spawn(bwrap || join(c.runtimeRoot, "zcode"), bwrap ? command(c) : [ + fileURLToPath(new URL("./quota-bootstrap.cjs", import.meta.url)), c.managed ? "managed" : "advanced", c.sourceProvider, + ], { stdio: ["ignore", "pipe", "pipe"], env: bwrap ? { PATH: "/usr/bin:/bin" } : { + PATH: "/usr/bin:/bin", HOME: homedir(), ELECTRON_RUN_AS_NODE: "1", + ZCODE_DATA_BASE_DIR: temporaryHome!, OCX_ZCODE_QUOTA_HOST: "1", + OCX_ZCODE_QUOTA_RUNTIME: c.runtimeRoot, OCX_ZCODE_QUOTA_CONFIG: c.config, + ...(c.credentials ? { OCX_ZCODE_QUOTA_CREDENTIALS: c.credentials } : {}), + } }); let output = ""; let invalid = false; const timer = setTimeout(() => { invalid = true; child.kill("SIGKILL"); }, 45_000); @@ -106,7 +122,8 @@ async function probe(c: QuotaContext): Promise { if (code || invalid) return resolve(null); try { resolve(parseZcodeQuota(JSON.parse(output))); } catch { resolve(null); } }); - }); + }); + } finally { if (temporaryHome) rmSync(temporaryHome, { recursive: true, force: true }); } } let inflight: { identity: string; promise: Promise } | undefined; diff --git a/src/providers/registry.ts b/src/providers/registry.ts index f05df8749e..f79306367e 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -2029,7 +2029,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ id: "zcode", label: "ZCode (local agent)", adapter: "zcode", baseUrl: "https://zcode.z.ai", authKind: "local", liveModels: true, dashboardPreset: true, allowBaseUrlOverride: false, - note: "Connect the official runtime and Z.AI models configured in ZCode Desktop from the provider setup panel. On Linux, OpenCodex manages an isolated workspace after explicit consent; no API key or environment variables to paste. Desktop settings stay read-only. ZCode owns its tools; text input only. Advanced isolated launchers remain supported.", + note: "Connect the official runtime and Z.AI models configured in ZCode Desktop from the provider setup panel. On Linux, official ZCode runs with host user permissions by default; OCX_ZCODE_SANDBOX=1 optionally enables workspace isolation; no API key or environment variables to paste. The bridge does not rewrite Desktop settings. ZCode owns its tools; text input only. Advanced isolated launchers remain supported.", }, { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 82bae5b9c8..f8fc2e4c33 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -28,9 +28,9 @@ sets `replaySafe: false`; accepted failures terminate incomplete rather than bec failover candidates. Launcher authority comes from either the operator environment or a separately persisted, GUI-consented Desktop connection. Data-plane requests and ordinary provider configuration cannot set command/workspace paths. The managed Desktop bootstrap keeps credential-bearing runtime -descriptors inside its OS sandbox; the parent sees public model identities only. +descriptors inside the official child process; the parent sees public model identities only. Subscription quota is separately read through the official Desktop host entitlement RPC in a -short-lived tmpfs sandbox. Only numeric quota windows leave that process. An advanced launcher +short-lived private profile copy, optionally inside Bubblewrap when enabled. Only numeric quota windows leave that process. An advanced launcher requires explicit `OCX_ZCODE_DESKTOP_RUNTIME` authority and reuses its own isolated model key; quota discovery must not silently import another Desktop account or affect routing policy. @@ -54,3 +54,7 @@ request when a node carries both. Codex's own deferred tool catalog emits exactl so the schema is not something a user can fix from configuration (issue #2673). > Decision record: [ADR-0093](../decisions/ADR-0093-moonshot-ref-with-siblings-normalization.md) + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 927a26aa0e..de5bf58fa0 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -82,3 +82,7 @@ copies an authoritative catalog context window into `limit.context` and a nonemp reasoning ladder into `thinking.effortOptions`. Missing capabilities stay absent instead of falling back to OpenCodex guesses, and the integration does not write the removed `thinking.effort` / `defaultEffort` fields because MCode owns the active effort per session. + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index e0b87add2d..1b3d729f1a 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -264,3 +264,7 @@ fragments are not guessed onto pending ID-only calls. parallel/colliding identities, distinct unsafe raw JSON index literals, the maximum safe-integer boundary, invalid index types, missing/null continuations and UTF-8 byte-limit boundaries. + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index be42793e0b..cd4260b578 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -82,3 +82,7 @@ constraints cannot widen the canonical shape. Bare shell bridge names are reject on the freeform path. Namespaced tools do not acquire bare-shell behavior. Regression coverage lives in `tests/providers/cursor/cursor-tool-definitions.test.ts`. + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/structure/runtime.md b/structure/runtime.md index 3099c13bfd..693b80d600 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -185,3 +185,7 @@ not an authentication or entitlement decision. ## Remote Hub hardening ownership `src/remote/protocol.ts` owns pure interval/feature negotiation. `src/remote/hub-state.ts` owns the `GET|HEAD /v1/hub-state` contract, its caps, and the parser both sides share. `src/client/hub-client.ts` owns bounded, schema-validated remote catalog consumption, hub-state reads, and key-id probes; `src/client/hub-state.ts` owns the resolution and the owner-stamped 0600 cache, and a failed read reports "unavailable" rather than degrading to the client's own local provider and login state. `src/client/hub-relay.ts` is a fixed-authority management relay with URL, header, body, redirect, and stream bounds. The public data listener remains the direct client→hub path; the loopback management ingress never serves data-plane routes. + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index dc9af564d6..fa0d8aa6d3 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -57,3 +57,7 @@ does not cover ordinary requests, streaming, retries, or per-hop redirect review Caller-owned `provider.fetch` executors are also deferred: they receive literal/config checks and redirect blocking, but cannot inherit DNS classification or peer pinning without a verified-peer executor contract. Main-request migration must not treat that branch as fixed-transport equivalent. + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index b667d10b53..feedd666ee 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -466,3 +466,7 @@ deprecated, sunset, decommissioned, or no longer available). An unrelated applic not retried. > Decision record: [ADR-0071](../decisions/ADR-0071-combo-streaming-commit-boundary.md) + +ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, +not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 6a6b4a747b..c05e881633 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; -import { tmpdir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { desktopStatus, disconnectDesktop, loadDesktopSettings, resolveDesktopRuntime, validateDesktopWorkspace } from "../../src/adapters/zcode/desktop"; import { readZcodeModels } from "../../src/adapters/zcode/settings"; @@ -11,12 +11,15 @@ import { resolveDesktopNode } from "../../src/adapters/zcode/desktop-node"; const { normalizeDesktopConfig, desktopModelCatalog } = createRequire(import.meta.url)("../../src/adapters/zcode/desktop-bootstrap.cjs"); let root: string; let previousHome: string | undefined; +let previousSandbox: string | undefined; beforeEach(() => { + previousSandbox = process.env.OCX_ZCODE_SANDBOX; process.env.OCX_ZCODE_SANDBOX = "1"; root = mkdtempSync(join(tmpdir(), "ocx-desktop-test-")); previousHome = process.env.OPENCODEX_HOME; process.env.OPENCODEX_HOME = join(root, "proxy"); }); afterEach(() => { if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; + if (previousSandbox === undefined) delete process.env.OCX_ZCODE_SANDBOX; else process.env.OCX_ZCODE_SANDBOX = previousSandbox; rmSync(root, { recursive: true, force: true }); }); const provider = () => ({ enabled: true, name: "Z.AI", kind: "anthropic", options: { @@ -112,3 +115,70 @@ test("sandbox preflight executes a child and hides uid-map diagnostics", async ( writeFileSync(executable, "#!/bin/sh\nexit 1\n", { mode: 0o755 }); expect(() => verifyDesktopSandbox(executable)).toThrow("sandbox_unavailable"); }); + +test("host execution is the default; a workspace is not a filesystem boundary", async () => { + const { desktopSandboxEnabled } = await import("../../src/adapters/zcode/desktop"); + delete process.env.OCX_ZCODE_SANDBOX; + expect(desktopSandboxEnabled()).toBe(false); + expect(validateDesktopWorkspace(homedir())).toBe(homedir()); + process.env.OCX_ZCODE_SANDBOX = "1"; + expect(desktopSandboxEnabled()).toBe(true); + expect(() => validateDesktopWorkspace(homedir())).toThrow("workspace_invalid"); +}); + +test("host bootstrap reads and writes outside workspace without touching the source profile", async () => { + const { ZcodeClient } = await import("../../src/adapters/zcode/client"); + const { fileURLToPath } = await import("node:url"); + const home = join(root, "state"), workspace = join(root, "project"); + mkdirSync(join(home, ".zcode/cli/db"), { recursive: true }); + mkdirSync(workspace); + const external = join(root, "outside-report.txt"); + writeFileSync(external, "report fixture"); + const config = join(root, "desktop-config.json"); + const original = JSON.stringify({ provider: { "builtin:zai-coding-plan": provider() } }); + writeFileSync(config, original); + const runtime = join(root, "official-runtime-fixture.cjs"); + writeFileSync(runtime, ` + const fs=require("node:fs"); + require("node:readline").createInterface({input:process.stdin}).on("line",line=>{ + const r=JSON.parse(line); + const path=${JSON.stringify(external)}; + const value=fs.readFileSync(path,"utf8"); + fs.writeFileSync(path,value); + process.stdout.write(JSON.stringify({id:r.id,result:{read:value,written:true,cwd:process.cwd()}})+"\\n"); + }); + `); + const settings = { command: [resolveDesktopNode(), fileURLToPath(new URL("../../src/adapters/zcode/desktop-bootstrap.cjs", import.meta.url)), + "--host", runtime, config, workspace], home, workspace, settingsPath: "", scope: "desktop:test-host" }; + // Repeated processes must not fail on an existing config, or overwrite Desktop's file. + for (let i = 0; i < 2; i++) { + const client = new ZcodeClient(settings); + try { + const models = await client.request("opencodex/desktopModels", {}, 3000); + expect(models.models).toHaveLength(1); + const state = await client.request("workspace/readState", {}, 3000); + expect(state).toEqual({ read: "report fixture", written: true, cwd: workspace }); + } finally { await client.close(); } + } + expect(readFileSync(config, "utf8")).toBe(original); +}); + +test("missing or denied optional sandbox never falls back, while default host mode needs no bwrap", () => { + if (process.platform !== "linux") return; + const path = process.env.PATH; + const bin = join(root, "host-bin"); mkdirSync(bin); + writeFileSync(join(bin, "node"), "#!/bin/sh\nprintf compatible\n", { mode: 0o755 }); + try { + process.env.PATH = bin; + delete process.env.OCX_ZCODE_SANDBOX; + expect(desktopStatus().issue).not.toBe("sandbox_missing"); + expect(desktopStatus().sandbox).toBe(false); + process.env.OCX_ZCODE_SANDBOX = "1"; + expect(desktopStatus().issue).toBe("sandbox_missing"); + writeFileSync(join(bin, "bwrap"), "#!/bin/sh\nexit 1\n", { mode: 0o755 }); + expect(desktopStatus().issue).toBe("sandbox_unavailable"); + expect(desktopStatus().sandbox).toBe(true); + } finally { + if (path === undefined) delete process.env.PATH; else process.env.PATH = path; + } +}); From a05f7e84c19822377d91c59e3c8652a6ddb0110e Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 16:28:53 -0300 Subject: [PATCH 09/43] feat(zcode): add manually selected accounts through official OAuth --- .../src/content/docs/guides/zcode-agent.md | 46 ++++- gui/src/components/ZcodeAccountsPane.tsx | 107 ++++++++++ gui/src/components/ZcodeDesktopPane.tsx | 2 + gui/src/i18n/de.ts | 13 ++ gui/src/i18n/en.ts | 13 ++ gui/src/i18n/fr.ts | 13 ++ gui/src/i18n/ja.ts | 13 ++ gui/src/i18n/ko.ts | 13 ++ gui/src/i18n/ru.ts | 13 ++ gui/src/i18n/tr.ts | 13 ++ gui/src/i18n/zh-TW.ts | 13 ++ gui/src/i18n/zh.ts | 13 ++ gui/tests/zcode-desktop-pane.test.tsx | 54 +++++ src/adapters/zcode/account-runtime.ts | 29 +++ src/adapters/zcode/accounts.ts | 47 +++++ src/adapters/zcode/adapter.ts | 6 +- src/adapters/zcode/client.ts | 5 +- src/adapters/zcode/desktop.ts | 92 +++++---- src/adapters/zcode/native-oauth.ts | 98 +++++++++ src/adapters/zcode/oauth-bootstrap.cjs | 73 +++++++ src/adapters/zcode/quota.ts | 32 +-- src/adapters/zcode/settings.ts | 11 +- src/codex/catalog/provider-fetch.ts | 2 +- src/router.ts | 2 +- src/server/auth-cors.ts | 7 +- src/server/management/provider-routes.ts | 6 +- src/server/management/route-registry.ts | 8 + src/server/management/zcode-account-routes.ts | 186 ++++++++++++++++++ .../management/zcode-desktop-activation.ts | 15 +- src/types/provider.ts | 2 + structure/adapters/registry.md | 15 ++ structure/catalog.md | 4 + structure/clients/claude-desktop.md | 4 + structure/codex-home.md | 4 + structure/config.md | 4 + structure/data-planes/images.md | 4 + structure/data-planes/inbound-compat.md | 4 + structure/design-methodology.md | 4 + structure/gui-and-management-api.md | 13 ++ structure/ops/docs-and-release.md | 4 + structure/ops/service-and-sidecars.md | 4 + structure/overview.md | 4 + structure/providers/chat-compat.md | 4 + structure/providers/cursor.md | 4 + structure/providers/openai-tiers.md | 4 + structure/providers/xai-grok.md | 4 + structure/runtime.md | 4 + structure/subagents.md | 4 + structure/transports/inventory.md | 4 + structure/transports/responses.md | 4 + structure/transports/streaming-health.md | 4 + tests/providers/zcode-desktop-routes.test.ts | 148 ++++++++++++++ tests/providers/zcode-desktop.test.ts | 51 +++++ tests/providers/zcode-quota.test.ts | 15 +- 54 files changed, 1187 insertions(+), 78 deletions(-) create mode 100644 gui/src/components/ZcodeAccountsPane.tsx create mode 100644 src/adapters/zcode/account-runtime.ts create mode 100644 src/adapters/zcode/accounts.ts create mode 100644 src/adapters/zcode/native-oauth.ts create mode 100644 src/adapters/zcode/oauth-bootstrap.cjs create mode 100644 src/server/management/zcode-account-routes.ts diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index a2e5dd9b7a..785fcdeeb1 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -35,7 +35,7 @@ or subscription terms; those remain the vendor's policy. 4. Choose a **workspace folder** with the folder browser. The default is a private disposable folder. Do not select your entire home or a credentials/configuration directory. 5. Review the native file/command execution notice, check the consent box and click - **Connect Desktop**. OpenCodex constructs the Bubblewrap launcher, verifies the protocol, enables the ZCode provider and updates the Codex catalog; + **Connect Desktop**. OpenCodex constructs the native launcher, verifies the protocol, enables the ZCode provider and updates the Codex catalog; no environment variables, API-key entry, token import or separate CLI login is required. 6. Optionally select a model and click **Test with one request**. This sends one brief prompt through the official ZCode runtime and consumes account quota. Connecting alone checks the @@ -56,6 +56,50 @@ is currently Linux-only; Windows/macOS need the advanced launcher flow below. A local Desktop is not the proxy host's Desktop: detection and workspace selection operate on the computer running OpenCodex. +### Add another account (manual selection) + +In **Providers → Add Provider → ZCode (local agent)**, or the existing provider's +**Settings**, scroll to **Saved ZCode accounts**. This is separate from **Connect Desktop**: +connecting Desktop continues to use its current local sign-in; saved accounts retain +independent official ZCode profiles. + +1. Select the installed Desktop runtime and working directory above the accounts section. +2. Give the account a recognizable name, such as **Personal** or **Work**. +3. Read and check the saved-account consent, then choose **Add account**. +4. Open the **Sign in with ZCode** link. Sign in to the intended account on ZCode's official + page; use a private browser window or switch accounts there when adding a second identity. +5. Return to the panel. OpenCodex waits for ZCode's own OAuth polling, verifies the local + protocol, enables a separate provider and publishes its models automatically. It never + asks you to copy tokens or API keys. Do not close the panel before this finishes. +6. Select the desired **account name / model** in your client's model picker. Adding an + account does **not** select it as the default and does not run inference. If an existing + Codex process still shows the old picker, restart Codex yourself after its tasks finish. + +Repeat for each account (up to 20). Accounts do not form a pool: there is no automatic +rotation or fallback to another ZCode account or the Desktop profile. Combos may target +explicit account providers if you configure those routes yourself. Quota bars remain +specific to each provider/account. + +**Sign in again** replaces only the matching account's sign-in, retaining its provider ID +and custom settings. A different identity is rejected; use **Add account** for it instead. +Duplicate saved identities are rejected too. **Rename** updates generated picker labels, +not customized model names. **Remove** deletes OpenCodex's saved profile and provider, not +ZCode Desktop's original profile. Remove references from defaults/combos before removing an +account, and wait for its active tasks. No task is stopped automatically. + +A protocol/registration/catalog failure is displayed as pending, not complete success. +Use **Retry provider activation** for a saved connection whose catalog is pending. For a +failed or expired login, cancel the draft and start again. Saved accounts and provider +bindings persist across OpenCodex restarts; pending OAuth jobs do not. If a restart or +closed panel leaves an unfinished account, remove that draft before trying again. + +Native OAuth and Coding Plan credential setup run in the official installed ZCode host. +The bridge only carries a short-lived authorization URL and safe status codes to the +browser. Profiles stay in the proxy's private configuration directory; they are not +imported into the browser or pooled. The same host-access / optional-sandbox policy described +below applies. Setup is Linux-only and needs a dashboard-session principal, not just an +admin API token. + ### What “connect Desktop” means ZCode's app-server is a stdio child, not a public Desktop TCP endpoint. OpenCodex uses the diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx new file mode 100644 index 0000000000..61f49b2dad --- /dev/null +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -0,0 +1,107 @@ +import { useCallback, useEffect, useState } from "react"; +import { useT } from "../i18n/shared"; + +type Account = { id: string; label: string; activation: string; busy: boolean; providerName?: string }; +type Job = { jobId: string; accountId: string; phase: string; url?: string; error?: string }; +export default function ZcodeAccountsPane({ apiBase, runtime, workspace }: { apiBase: string; runtime: string; workspace: string }) { + const t = useT(); + const [accounts, setAccounts] = useState([]); + const [label, setLabel] = useState(""); + const [consented, setConsented] = useState(null); + const consentScope = JSON.stringify([apiBase, runtime, workspace]); + const consent = consented === consentScope; + const [job, setJob] = useState(null); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + const read = useCallback(async (path: string, body?: Record) => { + const response = await fetch(apiBase + "/api/zcode-accounts" + path, body ? { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...body, consent: true }), + } : undefined); + const value = await response.json(); + if (!response.ok || value.error) throw new Error(value.error || "native_oauth_failed"); + return value; + }, [apiBase]); + const refresh = useCallback(async () => { + const value = await read(""); + setAccounts(value.accounts ?? []); + }, [read]); + useEffect(() => { + let stopped = false; + void read("").then(value => { if (!stopped) setAccounts(value.accounts ?? []); }) + .catch(e => { if (!stopped) setError(e.message); }); + return () => { stopped = true; }; + }, [read]); + const jobId = job?.jobId, jobPhase = job?.phase; + useEffect(() => { + if (!jobId || !consent || !["waiting", "authenticated"].includes(jobPhase ?? "")) return; + let stopped = false, pending = false; + const poll = async () => { + if (pending) return; + pending = true; + try { + const next: Job = await read("/login?jobId=" + encodeURIComponent(jobId)); + if (stopped) return; + if (next.phase === "authenticated") { + const result = await read("/complete", { jobId: next.jobId }); + if (stopped) return; + setJob({ ...next, phase: "finished", url: undefined }); + if (result.activation !== "ready") setError("catalog_update_failed"); + await refresh(); + } else { setJob(next); if (next.phase === "failed") { setError(next.error || "native_oauth_failed"); await refresh(); } } + } catch (e) { if (!stopped) { setError(e instanceof Error ? e.message : "native_oauth_failed"); setJob(j => j ? { ...j, phase: "failed", url: undefined } : null); } } + finally { pending = false; } + }; + const timer = setInterval(() => void poll(), 2000); + return () => { stopped = true; clearInterval(timer); }; + }, [jobId, jobPhase, consent, read, refresh]); + const action = async (path: string, body: Record) => { + if ((!consent && path !== "/cancel") || busy) return; + setBusy(true); setError(""); + try { + const result = await read(path, body); + if (path === "/login") setJob(result); + else if (path === "/cancel") setJob(null); + else if (result.activation && result.activation !== "ready") setError("catalog_update_failed"); + await refresh(); + } catch (e) { setError(e instanceof Error ? e.message : "native_oauth_failed"); } + finally { setBusy(false); } + }; + const loggingIn = !!job && job.phase !== "finished"; + return
+

{t("zcodeAccounts.title")}

+

{t("zcodeAccounts.help")}

+ + + + {job && job.phase !== "finished" &&
+

{t("zcodeAccounts.loginHint")}

+ {job.url &&
{t("zcodeAccounts.login")}} + +
} + {error &&

{t("zcodeAccounts.failed")} {error}

} + {accounts.some(account => account.activation === "ready") &&

{t("zcodeDesktop.restartNotice")}

} + {accounts.map(account =>
+ {account.label} + {account.activation === "ready" ? t("zcodeDesktop.connected") : t("zcodeAccounts.pending")} + {account.providerName && {account.providerName}} +
+ + + + +
+
)} +
; +} diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index fa9d66f2ea..94879fe3ac 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -1,3 +1,4 @@ +import ZcodeAccountsPane from "./ZcodeAccountsPane"; import { useEffect, useState } from "react"; import { useT } from "../i18n/shared"; @@ -126,5 +127,6 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: } +
; } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 9dc82d2ca6..bb0d10137c 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -23,6 +23,19 @@ export const de: Record = { "zcodeDesktop.dashboardRequired": "Diese Aktion erfordert eine Browsersitzung über ein an Loopback gebundenes Dashboard oder einen gekoppelten Hub. Ein Admin-Token allein erlaubt keine Desktop-Einrichtung.", "zcodeDesktop.hostAccess": "ZCode läuft ohne OpenCodex-Sandbox und kann alle Dateien lesen oder ändern, die Benutzerrechte und Harness erlauben. Der Arbeitsordner ist keine Zugriffsgrenze. Dies gilt nach Updates auch für bestehende Verbindungen. Für Isolation OCX_ZCODE_SANDBOX=1 am Proxy setzen und OpenCodex neu starten.", "zcodeDesktop.sandboxAccess": "Die optionale Bubblewrap-Isolation ist aktiv. Nur der gewählte Arbeitsordner ist eingebunden; andere Hostpfade sind nicht zugänglich.", + "zcodeAccounts.title": "Gespeicherte ZCode-Konten", + "zcodeAccounts.help": "Jedes Konto hat eine eigene offizielle Anmeldung, Modelle und Quote. Modell manuell in Codex wählen; kein Pool oder automatischer Wechsel. Die Desktop-Verbindung bleibt separat.", + "zcodeAccounts.name": "Kontoname", + "zcodeAccounts.consent": "Ich erlaube die offizielle ZCode-Anmeldung und Verwaltung dieser Konten. Nach Anmeldung werden Anbieter und Katalog aktiviert, ohne Standardwerte zu ändern oder Inferenz auszuführen.", + "zcodeAccounts.add": "Konto hinzufügen", + "zcodeAccounts.loginHint": "Offizielle Anmeldung im Browser abschließen. Für ein anderes Konto dort wechseln oder ein privates Fenster nutzen. Der Link läuft ab; dieses Fenster offen lassen.", + "zcodeAccounts.login": "Offizielle Anmeldung öffnen", + "zcodeAccounts.failed": "Aktion fehlgeschlagen. Abgelaufene Anmeldung wiederholen, bestehende Konten erneut anmelden, aktive Aufgaben abwarten und Routing-Verweise vor dem Löschen entfernen.", + "zcodeAccounts.pending": "Einrichtung unvollständig", + "zcodeAccounts.reconnect": "Erneut anmelden", + "zcodeAccounts.rename": "Umbenennen", + "zcodeAccounts.remove": "Konto entfernen", + "zcodeAccounts.removeConfirm": "Gespeichertes Konto und Anbieter entfernen? Aktive Aufgaben und Routing-Verweise müssen zuerst beendet werden.", "zcodeDesktop.intro": "ZCode Desktop verbinden, um den lokalen Anbieter zu aktivieren und seine Modelle in Codex bereitzustellen. Standardwerte bleiben unverändert; keine Inferenz.", "zcodeDesktop.connected": "ZCode bereit · Anbieter aktiviert · Modelle veröffentlicht", "zcodeDesktop.notConnected": "Desktop nicht verbunden", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index bdb4a3d80c..77879fecee 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -24,6 +24,19 @@ export const en = { "zcodeDesktop.dashboardRequired": "This action requires a browser session on a loopback-bound dashboard or a paired hub. An admin token alone cannot authorize Desktop setup.", "zcodeDesktop.hostAccess": "ZCode runs without an OpenCodex sandbox and can read or modify any file your operating-system user and harness allow. The workspace is only a working directory. This also applies to existing connections after upgrading. To enable isolation, set OCX_ZCODE_SANDBOX=1 on the proxy and restart OpenCodex.", "zcodeDesktop.sandboxAccess": "Optional Bubblewrap isolation is enabled. Only the selected workspace is mounted; host absolute paths outside it are unavailable.", + "zcodeAccounts.title": "Saved ZCode accounts", + "zcodeAccounts.help": "Each account has its own official login, models and quota. Choose its model manually in Codex; there is no pool or automatic account switch. Existing Desktop connection stays separate.", + "zcodeAccounts.name": "Account name", + "zcodeAccounts.consent": "I authorize official ZCode login and management of these saved accounts. Completing login enables that account’s provider and catalog without changing defaults or sending inference.", + "zcodeAccounts.add": "Add account", + "zcodeAccounts.loginHint": "Complete the official login in the browser. For a different account, switch accounts there or use a private window. This link expires; keep this panel open.", + "zcodeAccounts.login": "Open official login", + "zcodeAccounts.failed": "Could not complete this action. Retry login if expired; reconnect duplicates, wait for active tasks, and remove routing references before deleting an account.", + "zcodeAccounts.pending": "Account setup incomplete", + "zcodeAccounts.reconnect": "Sign in again", + "zcodeAccounts.rename": "Rename", + "zcodeAccounts.remove": "Remove account", + "zcodeAccounts.removeConfirm": "Remove this saved account and its provider? Active tasks and routing references must be cleared first.", "zcodeDesktop.intro": "Connect ZCode Desktop to enable its local provider and publish its models to Codex. Defaults stay unchanged; no inference is sent.", "zcodeDesktop.connected": "ZCode ready · provider enabled · models published", "zcodeDesktop.notConnected": "Desktop not connected", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 11ec90080c..6d1cf00fa9 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -22,6 +22,19 @@ export const fr: Record = { "zcodeDesktop.dashboardRequired": "Cette action nécessite une session navigateur sur un tableau de bord lié à loopback ou un hub appairé. Le jeton administrateur seul ne permet pas la configuration Desktop.", "zcodeDesktop.hostAccess": "ZCode fonctionne sans bac à sable OpenCodex et peut lire ou modifier tout fichier autorisé par votre utilisateur système et le harness. Le dossier de travail ne limite pas cet accès. Cela concerne aussi les connexions existantes après mise à jour. Pour activer l’isolation, définissez OCX_ZCODE_SANDBOX=1 sur le proxy et redémarrez OpenCodex.", "zcodeDesktop.sandboxAccess": "L’isolation Bubblewrap optionnelle est active. Seul le dossier de travail choisi est monté ; les autres chemins de l’hôte sont inaccessibles.", + "zcodeAccounts.title": "Comptes ZCode enregistrés", + "zcodeAccounts.help": "Chaque compte possède sa connexion officielle, ses modèles et son quota. Sélection manuelle dans Codex, sans pool ni basculement automatique. La connexion Desktop reste séparée.", + "zcodeAccounts.name": "Nom du compte", + "zcodeAccounts.consent": "J’autorise la connexion officielle ZCode et la gestion de ces comptes. La connexion active le fournisseur et le catalogue sans modifier les valeurs par défaut ni envoyer d’inférence.", + "zcodeAccounts.add": "Ajouter un compte", + "zcodeAccounts.loginHint": "Terminez la connexion officielle dans le navigateur. Changez de compte ou utilisez une fenêtre privée pour un autre compte. Le lien expire ; gardez ce panneau ouvert.", + "zcodeAccounts.login": "Ouvrir la connexion officielle", + "zcodeAccounts.failed": "Action impossible. Recommencez une connexion expirée, reconnectez les doublons, attendez les tâches actives et retirez les références de routage avant suppression.", + "zcodeAccounts.pending": "Configuration incomplète", + "zcodeAccounts.reconnect": "Se reconnecter", + "zcodeAccounts.rename": "Renommer", + "zcodeAccounts.remove": "Supprimer le compte", + "zcodeAccounts.removeConfirm": "Supprimer ce compte et son fournisseur ? Terminez les tâches et retirez d’abord les références de routage.", "zcodeDesktop.intro": "Connectez ZCode Desktop pour activer son fournisseur local et publier ses modèles dans Codex. Les choix par défaut restent inchangés ; aucune inférence.", "zcodeDesktop.connected": "ZCode prêt · fournisseur activé · modèles publiés", "zcodeDesktop.notConnected": "Desktop non connecté", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 3f91d502b9..60ddadde26 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -22,6 +22,19 @@ export const ja: Record = { "zcodeDesktop.dashboardRequired": "この操作にはループバックにバインドされたダッシュボードまたはペアリング済みハブのブラウザーセッションが必要です。管理トークンだけでは Desktop を設定できません。", "zcodeDesktop.hostAccess": "ZCode は OpenCodex のサンドボックスなしで実行され、OS ユーザーとハーネスが許可するファイルを読み書きできます。作業ディレクトリはアクセス制限ではありません。更新後の既存接続にも適用されます。隔離するにはプロキシで OCX_ZCODE_SANDBOX=1 を設定し、OpenCodex を再起動してください。", "zcodeDesktop.sandboxAccess": "任意の Bubblewrap 隔離が有効です。選択した作業ディレクトリのみがマウントされ、それ以外のホストパスにはアクセスできません。", + "zcodeAccounts.title": "保存済み ZCode アカウント", + "zcodeAccounts.help": "各アカウントは公式ログイン、モデル、利用枠を個別に持ちます。Codex でモデルを手動選択します。プールや自動切替はありません。Desktop 接続は別です。", + "zcodeAccounts.name": "アカウント名", + "zcodeAccounts.consent": "公式 ZCode ログインとアカウント管理を許可します。ログイン完了でプロバイダーとカタログを有効にします。既定値の変更や推論は行いません。", + "zcodeAccounts.add": "アカウントを追加", + "zcodeAccounts.loginHint": "ブラウザーで公式ログインを完了してください。別のアカウントには切替またはプライベートウィンドウを使用します。リンクは期限付きです。このパネルを開いたままにしてください。", + "zcodeAccounts.login": "公式ログインを開く", + "zcodeAccounts.failed": "操作を完了できません。期限切れは再ログイン、重複は再接続してください。実行中の作業を待ち、削除前にルーティング参照を除去してください。", + "zcodeAccounts.pending": "設定未完了", + "zcodeAccounts.reconnect": "再ログイン", + "zcodeAccounts.rename": "名前を変更", + "zcodeAccounts.remove": "アカウントを削除", + "zcodeAccounts.removeConfirm": "このアカウントとプロバイダーを削除しますか?先に作業を終了し、ルーティング参照を除去してください。", "zcodeDesktop.intro": "ZCode Desktop を接続するとローカルプロバイダーが有効になり、Codex にモデルを公開します。既定値は変更せず、推論も送信しません。", "zcodeDesktop.connected": "ZCode 準備完了 · プロバイダー有効 · モデル公開済み", "zcodeDesktop.notConnected": "Desktop 未接続", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 0606208b99..5430321b93 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -22,6 +22,19 @@ export const ko: Record = { "zcodeDesktop.dashboardRequired": "이 작업에는 루프백에 바인딩된 대시보드 또는 페어링된 허브의 브라우저 세션이 필요합니다. 관리자 토큰만으로는 Desktop을 설정할 수 없습니다.", "zcodeDesktop.hostAccess": "ZCode는 OpenCodex 샌드박스 없이 실행되며 OS 사용자와 하네스가 허용하는 파일을 읽거나 수정할 수 있습니다. 작업 폴더는 접근 경계가 아닙니다. 업데이트 후 기존 연결에도 적용됩니다. 격리하려면 프록시에 OCX_ZCODE_SANDBOX=1을 설정하고 OpenCodex를 재시작하세요.", "zcodeDesktop.sandboxAccess": "선택적 Bubblewrap 격리가 활성화되었습니다. 선택한 작업 폴더만 마운트되며 다른 호스트 경로에는 접근할 수 없습니다.", + "zcodeAccounts.title": "저장된 ZCode 계정", + "zcodeAccounts.help": "계정마다 공식 로그인, 모델, 할당량이 분리됩니다. Codex에서 모델을 직접 선택하세요. 풀이나 자동 계정 전환은 없습니다. Desktop 연결은 별도입니다.", + "zcodeAccounts.name": "계정 이름", + "zcodeAccounts.consent": "공식 ZCode 로그인과 계정 관리를 허용합니다. 로그인 완료 시 기본값 변경이나 추론 없이 공급자와 카탈로그를 활성화합니다.", + "zcodeAccounts.add": "계정 추가", + "zcodeAccounts.loginHint": "브라우저에서 공식 로그인을 완료하세요. 다른 계정은 계정을 전환하거나 비공개 창을 사용하세요. 링크는 만료됩니다. 이 패널을 열어 두세요.", + "zcodeAccounts.login": "공식 로그인 열기", + "zcodeAccounts.failed": "작업을 완료할 수 없습니다. 만료 시 재로그인하고 중복 계정은 재연결하세요. 실행 중인 작업을 기다리고 삭제 전 라우팅 참조를 제거하세요.", + "zcodeAccounts.pending": "계정 설정 미완료", + "zcodeAccounts.reconnect": "다시 로그인", + "zcodeAccounts.rename": "이름 변경", + "zcodeAccounts.remove": "계정 제거", + "zcodeAccounts.removeConfirm": "이 계정과 공급자를 제거할까요? 먼저 작업을 끝내고 라우팅 참조를 제거하세요.", "zcodeDesktop.intro": "ZCode Desktop을 연결하면 로컬 공급자를 활성화하고 Codex에 모델을 게시합니다. 기본값은 유지되며 추론은 실행하지 않습니다.", "zcodeDesktop.connected": "ZCode 준비 완료 · 공급자 활성화 · 모델 게시 완료", "zcodeDesktop.notConnected": "Desktop 연결 안 됨", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index f1fcf55f5b..e7fb234b6a 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -22,6 +22,19 @@ export const ru: Record = { "zcodeDesktop.dashboardRequired": "Требуется сеанс браузера на панели, привязанной к loopback, или сопряжённом хабе. Одного токена администратора недостаточно для настройки Desktop.", "zcodeDesktop.hostAccess": "ZCode работает без песочницы OpenCodex и может читать или менять любые файлы, разрешённые системному пользователю и средой запуска. Рабочий каталог не ограничивает доступ. Это относится и к существующим подключениям после обновления. Для изоляции задайте OCX_ZCODE_SANDBOX=1 у прокси и перезапустите OpenCodex.", "zcodeDesktop.sandboxAccess": "Дополнительная изоляция Bubblewrap включена. Доступен только выбранный рабочий каталог; остальные пути хоста недоступны.", + "zcodeAccounts.title": "Сохранённые аккаунты ZCode", + "zcodeAccounts.help": "У каждого аккаунта свой официальный вход, модели и квота. Выбирайте модель вручную в Codex: без пула и автопереключения. Подключение Desktop остаётся отдельным.", + "zcodeAccounts.name": "Название аккаунта", + "zcodeAccounts.consent": "Разрешаю официальный вход ZCode и управление аккаунтами. Вход включает провайдера и каталог без изменения настроек по умолчанию и без инференса.", + "zcodeAccounts.add": "Добавить аккаунт", + "zcodeAccounts.loginHint": "Завершите официальный вход в браузере. Для другого аккаунта переключитесь там или используйте приватное окно. Ссылка истекает; оставьте панель открытой.", + "zcodeAccounts.login": "Открыть официальный вход", + "zcodeAccounts.failed": "Не удалось выполнить действие. Повторите истёкший вход, переподключите дубликат, дождитесь завершения задач и удалите ссылки маршрутизации перед удалением аккаунта.", + "zcodeAccounts.pending": "Настройка не завершена", + "zcodeAccounts.reconnect": "Войти повторно", + "zcodeAccounts.rename": "Переименовать", + "zcodeAccounts.remove": "Удалить аккаунт", + "zcodeAccounts.removeConfirm": "Удалить этот аккаунт и провайдера? Сначала завершите задачи и удалите ссылки маршрутизации.", "zcodeDesktop.intro": "Подключение ZCode Desktop включает локального провайдера и публикует модели в Codex. Значения по умолчанию сохраняются; генерация не запускается.", "zcodeDesktop.connected": "ZCode готов · провайдер включён · модели опубликованы", "zcodeDesktop.notConnected": "Desktop не подключён", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 9355374671..40e3d445ba 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -23,6 +23,19 @@ export const tr: Record = { "zcodeDesktop.dashboardRequired": "Bu işlem, loopback’e bağlı panelde veya eşleştirilmiş hub’da tarayıcı oturumu gerektirir. Yalnızca yönetici jetonu Desktop kurulumuna yetki vermez.", "zcodeDesktop.hostAccess": "ZCode, OpenCodex sanal alanı olmadan çalışır; işletim sistemi kullanıcınızın ve harness’in izin verdiği dosyaları okuyabilir veya değiştirebilir. Çalışma klasörü bir erişim sınırı değildir. Güncellemeden sonra mevcut bağlantılar için de geçerlidir. Yalıtım için proxy üzerinde OCX_ZCODE_SANDBOX=1 ayarlayıp OpenCodex’i yeniden başlatın.", "zcodeDesktop.sandboxAccess": "İsteğe bağlı Bubblewrap yalıtımı açık. Yalnızca seçilen çalışma klasörü bağlanır; diğer ana makine yollarına erişilemez.", + "zcodeAccounts.title": "Kayıtlı ZCode hesapları", + "zcodeAccounts.help": "Her hesabın resmi oturumu, modelleri ve kotası ayrıdır. Codex’te modeli elle seçin; havuz veya otomatik geçiş yoktur. Desktop bağlantısı ayrıdır.", + "zcodeAccounts.name": "Hesap adı", + "zcodeAccounts.consent": "Resmi ZCode girişini ve hesap yönetimini onaylıyorum. Giriş, varsayılanları değiştirmeden veya çıkarım göndermeden sağlayıcıyı ve kataloğu etkinleştirir.", + "zcodeAccounts.add": "Hesap ekle", + "zcodeAccounts.loginHint": "Tarayıcıda resmi girişi tamamlayın. Başka hesap için hesap değiştirin veya özel pencere kullanın. Bağlantı süreli; bu paneli açık tutun.", + "zcodeAccounts.login": "Resmi girişi aç", + "zcodeAccounts.failed": "İşlem tamamlanamadı. Süresi dolan girişi yenileyin, tekrar hesabı yeniden bağlayın, etkin işleri bekleyin ve silmeden önce yönlendirme referanslarını kaldırın.", + "zcodeAccounts.pending": "Hesap kurulumu eksik", + "zcodeAccounts.reconnect": "Yeniden giriş yap", + "zcodeAccounts.rename": "Yeniden adlandır", + "zcodeAccounts.remove": "Hesabı kaldır", + "zcodeAccounts.removeConfirm": "Bu hesap ve sağlayıcı kaldırılsın mı? Önce etkin işleri bitirin ve yönlendirme referanslarını kaldırın.", "zcodeDesktop.intro": "ZCode Desktop bağlantısı yerel sağlayıcıyı etkinleştirip modellerini Codex’e ekler. Varsayılanlar değişmez; çıkarım yapılmaz.", "zcodeDesktop.connected": "ZCode hazır · sağlayıcı etkin · modeller yayımlandı", "zcodeDesktop.notConnected": "Desktop bağlı değil", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 48a52dc909..7d36caac42 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -20,6 +20,19 @@ export const zhTW: Record = { "zcodeDesktop.dashboardRequired": "此操作需要綁定至迴路位址的儀表板或已配對 Hub 的瀏覽器工作階段。僅管理員權杖無法授權 Desktop 設定。", "zcodeDesktop.hostAccess": "ZCode 預設不使用 OpenCodex 沙箱,可讀寫作業系統使用者與 harness 允許存取的任何檔案。工作目錄不是存取邊界。升級後現有連線也適用。如需隔離,請在代理上設定 OCX_ZCODE_SANDBOX=1 並重新啟動 OpenCodex。", "zcodeDesktop.sandboxAccess": "已啟用選用的 Bubblewrap 隔離。僅掛載選定的工作目錄,無法存取其他主機路徑。", + "zcodeAccounts.title": "已儲存的 ZCode 帳戶", + "zcodeAccounts.help": "每個帳戶擁有獨立的官方登入、模型與額度。請在 Codex 中手動選擇模型;不使用帳戶池或自動切換。Desktop 連線保持獨立。", + "zcodeAccounts.name": "帳戶名稱", + "zcodeAccounts.consent": "我授權官方 ZCode 登入並管理這些帳戶。登入完成後啟用該帳戶的供應商與目錄,不更改預設設定,也不傳送推論請求。", + "zcodeAccounts.add": "新增帳戶", + "zcodeAccounts.loginHint": "請在瀏覽器完成官方登入。使用其他帳戶時,請切換帳戶或使用私密視窗。連結會過期,請保持此面板開啟。", + "zcodeAccounts.login": "開啟官方登入", + "zcodeAccounts.failed": "無法完成操作。登入過期請重試,重複帳戶請重新連線,等待執行中工作結束,並在刪除前移除路由參照。", + "zcodeAccounts.pending": "帳戶設定未完成", + "zcodeAccounts.reconnect": "重新登入", + "zcodeAccounts.rename": "重新命名", + "zcodeAccounts.remove": "移除帳戶", + "zcodeAccounts.removeConfirm": "移除此帳戶及其供應商?請先結束工作並移除路由參照。", "zcodeDesktop.intro": "連接 ZCode Desktop 將啟用本機供應商並將模型發佈至 Codex。不變更預設設定,也不執行推論。", "zcodeDesktop.connected": "ZCode 已就緒 · 供應商已啟用 · 模型已發佈", "zcodeDesktop.notConnected": "Desktop 未連接", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 84cb6fbce8..2603ba3e55 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -22,6 +22,19 @@ export const zh: Record = { "zcodeDesktop.dashboardRequired": "此操作需要绑定到回环地址的仪表板或已配对 Hub 的浏览器会话。仅管理员令牌无法授权 Desktop 设置。", "zcodeDesktop.hostAccess": "ZCode 默认不使用 OpenCodex 沙箱,可读写操作系统用户和 harness 允许访问的任何文件。工作目录不是访问边界。升级后现有连接也适用。如需隔离,请在代理上设置 OCX_ZCODE_SANDBOX=1 并重启 OpenCodex。", "zcodeDesktop.sandboxAccess": "已启用可选的 Bubblewrap 隔离。仅挂载所选工作目录,无法访问其他主机路径。", + "zcodeAccounts.title": "已保存的 ZCode 账户", + "zcodeAccounts.help": "每个账户拥有独立的官方登录、模型和额度。请在 Codex 中手动选择模型;不使用账户池或自动切换。Desktop 连接保持独立。", + "zcodeAccounts.name": "账户名称", + "zcodeAccounts.consent": "我授权官方 ZCode 登录并管理这些账户。登录完成后启用该账户的提供商和目录,不更改默认设置,也不发送推理请求。", + "zcodeAccounts.add": "添加账户", + "zcodeAccounts.loginHint": "请在浏览器完成官方登录。使用其他账户时,请切换账户或使用隐私窗口。链接会过期,请保持此面板打开。", + "zcodeAccounts.login": "打开官方登录", + "zcodeAccounts.failed": "无法完成操作。登录过期请重试,重复账户请重新连接,等待活动任务结束,并在删除前移除路由引用。", + "zcodeAccounts.pending": "账户设置未完成", + "zcodeAccounts.reconnect": "重新登录", + "zcodeAccounts.rename": "重命名", + "zcodeAccounts.remove": "移除账户", + "zcodeAccounts.removeConfirm": "移除此账户及其提供商?请先结束活动任务并移除路由引用。", "zcodeDesktop.intro": "连接 ZCode Desktop 将启用本地提供商并将模型发布到 Codex。不更改默认设置,也不执行推理。", "zcodeDesktop.connected": "ZCode 已就绪 · 提供商已启用 · 模型已发布", "zcodeDesktop.notConnected": "Desktop 未连接", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index ee08150eab..a8c7781ff4 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -169,3 +169,57 @@ test("explicit sandbox mode shows its filesystem boundary", async () => { expect(host.querySelector('[role="note"]')?.textContent).toContain("Only the selected workspace"); expect(host.querySelector('[role="note"]')?.textContent).not.toContain("without an OpenCodex sandbox"); }); + +test("saved accounts offer separate manual login and never start OAuth without consent", async () => { + await mountPane(); + expect(host.textContent).toContain("Saved ZCode accounts"); + expect(host.textContent).toContain("no pool or automatic account switch"); + expect(button("Add account").disabled).toBe(true); + expect(requests.some(r => r.path.endsWith("/zcode-accounts/login"))).toBe(false); +}); + +test("account activation failure stays partial and separate account labels remain visible", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL) => { + const path = new URL(String(input), "http://localhost").pathname; + if (path === "/api/zcode-accounts") return Response.json({ accounts: [ + { id: "a", label: "Personal fixture", activation: "ready", busy: false }, + { id: "b", label: "Work fixture", activation: "catalog_pending", busy: false }, + ] }); + return Response.json({ connected: false, runtimes: [], runtime: "", workspace: "/project", models: [] }); + } }); + await mountPane(); + expect(host.textContent).toContain("Personal fixture"); + expect(host.textContent).toContain("Work fixture"); + expect(host.textContent).toContain("Account setup incomplete"); + expect(button("Add account").disabled).toBe(true); +}); + +test("saved account UI completes official login then shows provider/catalog readiness", async () => { + let complete = 0; + const prior = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { + const url = new URL(String(input), "http://localhost"); + if (!url.pathname.startsWith("/api/zcode-accounts")) return prior(input, options); + requests.push({ path: url.pathname, body: options?.body ? JSON.parse(String(options.body)) : undefined }); + if (url.pathname.endsWith("/login")) return Response.json({jobId:"fixture-job",accountId:"fixture-account",phase:options?.method === "POST" ? "waiting" : "authenticated"}); + if (url.pathname.endsWith("/complete")) { complete++; return Response.json({activation:"ready"}); } + return Response.json({accounts:complete ? [{id:"fixture-account",label:"Personal fixture",activation:"ready",providerName:"zcode-fixture",busy:false}] : []}); + } }); + await mountPane(); + const section = host.querySelector("h3")!.closest("section")!; + const input = section.querySelector('input:not([type="checkbox"])') as HTMLInputElement; + await act(async () => { + const setter = Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value")!.set!; + setter.call(input, "Personal fixture"); input.dispatchEvent(new win.Event("input", {bubbles:true}) as unknown as Event); + (section.querySelector('input[type="checkbox"]') as HTMLInputElement).click(); + }); + expect(button("Add account").disabled).toBe(false); + await act(async () => { button("Add account").click(); }); + await act(async () => { await new Promise(resolve => setTimeout(resolve, 2200)); }); + expect(complete).toBe(1); + expect(section.textContent).toContain("Personal fixture"); + expect(section.textContent).toContain("provider enabled · models published"); + expect(section.textContent).toContain("No processes will be restarted automatically"); + expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); + expect(requests.filter(r => r.body).every(r => r.body!.consent === true)).toBe(true); +}); diff --git a/src/adapters/zcode/account-runtime.ts b/src/adapters/zcode/account-runtime.ts new file mode 100644 index 0000000000..b1326f0fc8 --- /dev/null +++ b/src/adapters/zcode/account-runtime.ts @@ -0,0 +1,29 @@ +import { readAccount, accountProfile } from "./accounts"; +import { desktopAccountBusy, desktopStatus } from "./desktop"; +import { runNativeOAuth } from "./native-oauth"; + +const refreshes = new Map>(); +const refreshed = new Map(); +export const accountRuntimeBusy = (id: string) => refreshes.has(id) || desktopAccountBusy(id); +export const invalidateAccountRefresh = (id: string) => refreshed.delete(id); +/** Refresh belongs to the official host, never a proxy OAuth/API implementation. */ +export async function refreshAccount(id: string): Promise { + if ((refreshed.get(id) ?? 0) > Date.now()) return; + const pending = refreshes.get(id); if (pending) return pending; + if (desktopAccountBusy(id)) return; // Do not mutate a profile used by a running turn. + const account = readAccount(id); + if (!account.subjectHash) throw new Error("account_login_required"); + const status = desktopStatus(id); + if (!status.connected) throw new Error("account_login_required"); + const promise = (async () => { + let identity: string | undefined; + await runNativeOAuth({ runtime: status.runtime, profileHome: accountProfile(id), mode: "refresh", + signal: AbortSignal.timeout(30_000), onEvent: event => { + if (event.type === "authenticated") identity = event.subjectHash; + } }); + if (identity !== account.subjectHash) throw new Error("account_identity_mismatch"); + refreshed.set(id, Date.now() + 60_000); + })(); + refreshes.set(id, promise); + try { await promise; } finally { refreshes.delete(id); } +} diff --git a/src/adapters/zcode/accounts.ts b/src/adapters/zcode/accounts.ts new file mode 100644 index 0000000000..1a16373eed --- /dev/null +++ b/src/adapters/zcode/accounts.ts @@ -0,0 +1,47 @@ +import { constants, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { randomUUID } from "node:crypto"; +import { getConfigDir } from "../../config/paths"; + +export interface ZcodeAccount { id: string; label: string; subjectHash?: string } +export function accountRoot(id: string): string { + if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id)) throw new Error("account_invalid"); + return join(getConfigDir(), "zcode-accounts", id); +} +export const accountProfile = (id: string) => join(accountRoot(id), "profile"); +export function readAccount(id: string): ZcodeAccount { + const fd = openSync(join(accountRoot(id), "account.json"), constants.O_RDONLY | constants.O_NOFOLLOW); + try { + const st = fstatSync(fd); + if (!st.isFile() || st.size > 4096 || (st.mode & 0o077) || st.uid !== process.getuid?.()) throw new Error(); + const value = JSON.parse(readFileSync(fd, "utf8")); + if (value.id !== id || typeof value.label !== "string" || value.label.length > 80 + || (value.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(value.subjectHash))) throw new Error(); + return { id, label: value.label, ...(value.subjectHash ? { subjectHash: value.subjectHash } : {}) }; + } finally { closeSync(fd); } +} +export function writeAccount(account: ZcodeAccount): void { + const dir = accountRoot(account.id); + const label = account.label.trim(); + if (!label || label.length > 80 || /[\x00-\x1f]/.test(label) + || (account.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(account.subjectHash))) throw new Error("account_invalid"); + mkdirSync(dir, { recursive: true, mode: 0o700 }); + const temp = join(dir, randomUUID() + ".tmp"); + writeFileSync(temp, JSON.stringify({ ...account, label }), { mode: 0o600, flag: "wx" }); + renameSync(temp, join(dir, "account.json")); +} +export function listAccounts(): ZcodeAccount[] { + const dir = join(getConfigDir(), "zcode-accounts"); + if (!existsSync(dir)) return []; + return readdirSync(dir).filter(id => /^[a-f0-9-]{36}$/.test(id)).slice(0, 100).flatMap(id => { + try { return [readAccount(id)]; } catch { return []; } + }); +} +export function allocateAccount(label: string): ZcodeAccount { + if (listAccounts().length >= 20) throw new Error("account_limit"); + const account = { id: randomUUID(), label }; + writeAccount(account); + mkdirSync(accountProfile(account.id), { recursive: true, mode: 0o700 }); + return account; +} +export function removeAccountFiles(id: string): void { rmSync(accountRoot(id), { recursive: true, force: true }); } diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 368b893915..b89789d625 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -1,3 +1,4 @@ +import { refreshAccount } from "./account-runtime"; import { createHash } from "node:crypto"; import type { OcxParsedRequest, OcxProviderConfig } from "../../types"; import type { ProviderAdapter } from "../base"; @@ -77,8 +78,9 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt let stop = () => {}; const cancelled = () => stop(); try { + if (provider.zcodeAccountId && !deps.settings) await refreshAccount(provider.zcodeAccountId); let settings: ZcodeSettings; - try { settings = (deps.settings ?? loadZcodeSettings)(); } + try { settings = (deps.settings ?? (() => loadZcodeSettings(process.env, provider.zcodeAccountId)))(); } catch { throw new Error("ZCode native execution is unavailable. Configure the isolated launcher, home, workspace and explicit opt-in."); } if (provider.authMode !== "local") throw new Error("ZCode requires local authentication mode; log in using the isolated ZCode client."); const model = readZcodeModels(settings).find(item => item.id === parsed.modelId); @@ -88,7 +90,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt } release = await lock(settings.scope, incoming.abortSignal); // A queued turn must not resurrect a revoked Desktop connection or old login. - if (settings.desktopModels && (deps.settings ?? loadZcodeSettings)().scope !== settings.scope) { + if (settings.desktopModels && (deps.settings ?? (() => loadZcodeSettings(process.env, provider.zcodeAccountId)))().scope !== settings.scope) { throw new Error("ZCode Desktop connection changed while this turn was queued."); } const scope = createHash("sha256").update(JSON.stringify([ diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index 9f0a09d584..cfa18364b4 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -6,8 +6,9 @@ import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks" export type ZcodeSpawn = typeof spawn; type Pending = { resolve: (value: JsonObject) => void; reject: (error: Error) => void; timer: ReturnType }; const desktopClients = new Set(); +export const hasZcodeAccountClients = (id: string) => [...desktopClients].some(client => client.accountId === id); export async function closeZcodeDesktopClients(): Promise { - await Promise.all([...desktopClients].map(client => client.disconnect())); + await Promise.all([...desktopClients].filter(client => !client.accountId).map(client => client.disconnect())); } /** ZCode 0.16.5 uses request/result NDJSON, without a jsonrpc field. */ @@ -23,7 +24,9 @@ export class ZcodeClient { onEvent: (message: JsonObject) => void = () => {}; onFailure: (error: Error) => void = () => {}; + readonly accountId?: string; constructor(settings: ZcodeSettings, spawnProcess: ZcodeSpawn = spawn) { + this.accountId = settings.accountId; const [command, ...args] = settings.command; this.child = spawnProcess(command!, [...args, "app-server"], { cwd: settings.home, shell: false, stdio: ["pipe", "pipe", "pipe"], diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index c501a60460..1f73b76d95 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -1,10 +1,11 @@ +import { accountRoot, accountProfile, readAccount } from "./accounts"; import { constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path"; import { homedir } from "node:os"; import { createHash, randomUUID } from "node:crypto"; import { fileURLToPath } from "node:url"; import { getConfigDir } from "../../config/paths"; -import { closeZcodeDesktopClients, ZcodeClient } from "./client"; +import { hasZcodeAccountClients, closeZcodeDesktopClients, ZcodeClient } from "./client"; import type { ZcodeSettings } from "./settings"; import { verifyDesktopSandbox } from "./desktop-sandbox"; import { resolveDesktopNode } from "./desktop-node"; @@ -18,12 +19,13 @@ export class DesktopSetupError extends Error { constructor(public code: string) { super(code); } } const fail = (code: string): never => { throw new DesktopSetupError(code); }; -const root = () => join(getConfigDir(), "zcode-desktop"); -const connectionPath = () => join(root(), "connection.json"); -export const defaultDesktopWorkspace = () => join(root(), "workspace"); +const root = (accountId?: string) => accountId ? accountRoot(accountId) : join(getConfigDir(), "zcode-desktop"); +const connectionPath = (accountId?: string) => join(root(accountId), "connection.json"); +export const defaultDesktopWorkspace = (accountId?: string) => join(root(accountId), "workspace"); -function readConnection(): Connection | undefined { - const path = connectionPath(); +function readConnection(accountId?: string): Connection | undefined { + if (accountId) readAccount(accountId); + const path = connectionPath(accountId); if (!existsSync(path)) return undefined; try { const st = lstatSync(path); @@ -39,9 +41,9 @@ function readConnection(): Connection | undefined { /** Public native IDs only: routing must work before live discovery warms the cache. * No profile reads, process launches, or config mutations on the routing path. */ -export function desktopRoutingModelIds(): string[] { +export function desktopRoutingModelIds(accountId?: string): string[] { try { - const connection = readConnection(); + const connection = readConnection(accountId); if (!connection?.connected) return []; return connection.models.flatMap(model => model && typeof model.id === "string" && typeof model.providerId === "string" @@ -51,11 +53,11 @@ export function desktopRoutingModelIds(): string[] { } catch { return []; } // Invalid/revoked state is still rejected by the adapter. } -function persist(connection: Connection): void { - mkdirSync(root(), { recursive: true, mode: 0o700 }); - const temp = join(root(), `${randomUUID()}.tmp`); +function persist(connection: Connection, accountId?: string): void { + mkdirSync(root(accountId), { recursive: true, mode: 0o700 }); + const temp = join(root(accountId), `${randomUUID()}.tmp`); writeFileSync(temp, JSON.stringify(connection), { mode: 0o600, flag: "wx" }); - renameSync(temp, connectionPath()); + renameSync(temp, connectionPath(accountId)); } /** Only an installed Desktop layout, never an arbitrary executable or shell command. */ @@ -131,8 +133,8 @@ function prerequisites(): { bwrap?: string; node: string } { return { bwrap: bwrap ? realpathSync(bwrap) : undefined, node: realpathSync(node) }; } -function desktopProfile(): { config: string; credentials?: string } { - const base = join(homedir(), ".zcode/v2"); +export function desktopProfile(accountId?: string): { config: string; credentials?: string } { + const base = join(accountId ? accountProfile(accountId) : homedir(), ".zcode/v2"); const config = join(base, "config.json"); try { if (!lstatSync(config).isFile() || statSync(config).size > 4 * 1024 * 1024) return fail("profile_missing"); } catch { return fail("profile_missing"); } @@ -140,20 +142,20 @@ function desktopProfile(): { config: string; credentials?: string } { return { config, ...(existsSync(credentials) && lstatSync(credentials).isFile() ? { credentials } : {}) }; } -function settingsFor(connection: Connection): ZcodeSettings { +function settingsFor(connection: Connection, accountId?: string): ZcodeSettings { const { bwrap, node } = prerequisites(); const runtime = resolveDesktopRuntime(connection.runtime); const workspace = validateDesktopWorkspace(connection.workspace); - const profile = desktopProfile(); + const profile = desktopProfile(accountId); // Do not carry a native conversation across Desktop login/profile changes. Metadata-only // fencing is conservative (a refresh may start a new session) and never reads credential bytes. const profileStamp = createHash("sha256").update(JSON.stringify([profile.config, profile.credentials].filter(Boolean).map(path => { const st = statSync(path!); return [st.dev, st.ino, st.size, st.mtimeMs, st.ctimeMs]; }))).digest("hex"); - const privateHome = join(root(), "home", connection.generation, profileStamp); + const privateHome = join(root(accountId), "home", connection.generation, profileStamp); const db = join(privateHome, ".zcode/cli/db"); mkdirSync(db, { recursive: true, mode: 0o700 }); - if (!bwrap) return { + if (!bwrap) return { accountId, command: [node, fileURLToPath(new URL("./desktop-bootstrap.cjs", import.meta.url)), "--host", runtime, profile.config, workspace], home: privateHome, workspace, settingsPath: "", @@ -177,42 +179,43 @@ function settingsFor(connection: Connection): ZcodeSettings { args.push("--clearenv", "--setenv", "HOME", sandboxHome, "--setenv", "PATH", "/usr/bin:/bin", "--setenv", "ZCODE_DATA_BASE_DIR", "/desktop", "--chdir", "/workspace", "/usr/bin/node", "/bridge/desktop-bootstrap.cjs"); - return { command: [bwrap, ...args], home: privateHome, workspace: "/workspace", settingsPath: "", + return { accountId, command: [bwrap, ...args], home: privateHome, workspace: "/workspace", settingsPath: "", scope: `desktop:${connection.generation}:${profileStamp}:sandbox`, desktopModels: connection.models }; } /** Persisted GUI consent is separate from provider config; data-plane requests cannot set it. */ -export function loadDesktopSettings(): ZcodeSettings | undefined { - if (connecting) return fail("busy"); - const connection = readConnection(); - if (!connection) return undefined; +export function loadDesktopSettings(accountId?: string): ZcodeSettings | undefined { + if (connecting.has(accountId ?? "desktop")) return fail("busy"); + const connection = readConnection(accountId); + if (!connection) { if (accountId) return fail("disconnected"); return undefined; } if (!connection.connected) return fail("disconnected"); - return settingsFor(connection); + return settingsFor(connection, accountId); } -export function desktopStatus() { +export function desktopStatus(accountId?: string) { let issue: string | undefined; - try { prerequisites(); desktopProfile(); } catch (e) { issue = e instanceof DesktopSetupError ? e.code : "connection_invalid"; } + try { prerequisites(); desktopProfile(accountId); } catch (e) { issue = e instanceof DesktopSetupError ? e.code : "connection_invalid"; } let connection: Connection | undefined; - try { connection = readConnection(); } catch { issue = "connection_invalid"; } + try { connection = readConnection(accountId); } catch { issue = "connection_invalid"; } const runtimes = detectDesktopRuntimes(); if (connection?.connected) { try { resolveDesktopRuntime(connection.runtime); validateDesktopWorkspace(connection.workspace); } catch (e) { issue = e instanceof DesktopSetupError ? e.code : "connection_invalid"; } } - return { connected: connection?.connected === true && !issue, issue, runtimes, - runtime: connection?.runtime ?? runtimes[0] ?? "", workspace: connection?.workspace ?? defaultDesktopWorkspace(), + return { ...(accountId ? { accountId } : {}), connected: connection?.connected === true && !issue, issue, runtimes, + runtime: connection?.runtime ?? runtimes[0] ?? "", workspace: connection?.workspace ?? defaultDesktopWorkspace(accountId), models: connection?.models ?? [], sandbox: desktopSandboxEnabled(), platform: process.platform }; } -let connecting = false; -export async function connectDesktop(runtime: string, workspace: string): Promise> { - if (connecting) return fail("busy"); - connecting = true; +const connecting = new Set(); +export async function connectDesktop(runtime: string, workspace: string, accountId?: string): Promise> { + if (connecting.has(accountId ?? "desktop")) return fail("busy"); + if (accountId && hasZcodeAccountClients(accountId)) return fail("busy"); + connecting.add(accountId ?? "desktop"); try { const connection: Connection = { version: 1, connected: true, generation: randomUUID(), runtime: resolveDesktopRuntime(runtime), workspace: validateDesktopWorkspace(workspace), models: [] }; - const settings = settingsFor(connection); + const settings = settingsFor(connection, accountId); const client = new ZcodeClient(settings); try { const result = await client.request("opencodex/desktopModels", {}, 15_000); @@ -227,16 +230,19 @@ export async function connectDesktop(runtime: string, workspace: string): Promis // Official protocol readiness only. This does not send a prompt or spend inference quota. await client.request("workspace/readState", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace } }, 15_000); } finally { await client.close(); } - await closeZcodeDesktopClients(); - persist(connection); - return desktopStatus(); + if (!accountId) await closeZcodeDesktopClients(); + persist(connection, accountId); + return desktopStatus(accountId); } catch (e) { if (e instanceof DesktopSetupError) throw e; return fail("runtime_failed"); } - finally { connecting = false; } + finally { connecting.delete(accountId ?? "desktop"); } } -export async function disconnectDesktop(): Promise { - if (connecting) return fail("busy"); - const previous = readConnection(); - persist({ version: 1, connected: false, generation: randomUUID(), runtime: previous?.runtime ?? "", workspace: previous?.workspace ?? defaultDesktopWorkspace(), models: [] }); - await closeZcodeDesktopClients(); +export async function disconnectDesktop(accountId?: string): Promise { + if (connecting.has(accountId ?? "desktop")) return fail("busy"); + if (accountId && hasZcodeAccountClients(accountId)) return fail("busy"); + const previous = readConnection(accountId); + persist({ version: 1, connected: false, generation: randomUUID(), runtime: previous?.runtime ?? "", workspace: previous?.workspace ?? defaultDesktopWorkspace(), models: [] }, accountId); + if (!accountId) await closeZcodeDesktopClients(); } + +export const desktopAccountBusy = (id: string) => connecting.has(id) || hasZcodeAccountClients(id); diff --git a/src/adapters/zcode/native-oauth.ts b/src/adapters/zcode/native-oauth.ts new file mode 100644 index 0000000000..0894352858 --- /dev/null +++ b/src/adapters/zcode/native-oauth.ts @@ -0,0 +1,98 @@ +import { verifyDesktopSandbox } from "./desktop-sandbox"; +import { spawn } from "node:child_process"; +import { existsSync, lstatSync, readlinkSync, realpathSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { homedir } from "node:os"; +import { fileURLToPath } from "node:url"; +import { desktopSandboxEnabled, resolveDesktopRuntime } from "./desktop"; + +export type NativeOAuthEvent = + | { type: "capabilities"; supported: true } + | { type: "authorization"; url: string } + | { type: "authenticated"; subjectHash: string } + | { type: "error"; code: "login_expired" | "native_oauth_failed" | "session_restore_failed" | "model_setup_failed" }; + +/** Strict projection even if a vendor/bootstrap version emits unexpected fields. */ +export function parseNativeOAuthEvent(value: unknown): NativeOAuthEvent { + if (!value || typeof value !== "object") throw new Error("native_oauth_failed"); + const row = value as Record; + if (row.type === "capabilities" && row.supported === true) return { type: "capabilities", supported: true }; + if (row.type === "authorization" && typeof row.url === "string" && row.url.length <= 8_192) { + let url: URL; + try { url = new URL(row.url); } catch { throw new Error("native_oauth_failed"); } + if (url.protocol === "https:" && url.hostname === "chat.z.ai" && !url.username && !url.password) return { type: "authorization", url: url.href }; + } + if (row.type === "authenticated" && typeof row.subjectHash === "string" && /^[a-f0-9]{64}$/.test(row.subjectHash)) { + return { type: "authenticated", subjectHash: row.subjectHash }; + } + if (row.type === "error") return { type: "error", code: + row.code === "login_expired" || row.code === "session_restore_failed" || row.code === "model_setup_failed" + ? row.code : "native_oauth_failed" }; + throw new Error("native_oauth_failed"); +} + +/** The caller supplies a newly allocated private profile, never the user's Desktop home. */ +export function nativeOAuthCommand(runtime: string, profileHome: string, mode: "login" | "capabilities" | "refresh"): string[] { + if (process.platform !== "linux") throw new Error("platform_unsupported"); + const profile = realpathSync(profileHome), st = lstatSync(profileHome); + if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid?.() || (st.mode & 0o077) || profile === realpathSync(homedir())) { + throw new Error("profile_invalid"); + } + const runtimeRoot = dirname(dirname(dirname(resolveDesktopRuntime(runtime)))); + if (!lstatSync(join(runtimeRoot, "zcode")).isFile()) throw new Error("desktop_missing"); + if (!desktopSandboxEnabled()) return [join(runtimeRoot, "zcode"), + fileURLToPath(new URL("./oauth-bootstrap.cjs", import.meta.url)), mode, runtimeRoot]; + const bwrap = Bun.which("bwrap", { PATH: process.env.PATH }); + if (!bwrap) throw new Error("sandbox_missing"); + verifyDesktopSandbox(bwrap); + const args = [realpathSync(bwrap), "--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--ro-bind", "/usr", "/usr"]; + for (const path of ["/bin", "/lib", "/lib64", "/sbin"]) if (existsSync(path)) { + args.push(...(lstatSync(path).isSymbolicLink() ? ["--symlink", readlinkSync(path), path] : ["--ro-bind", path, path])); + } + args.push("--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/etc"); + for (const path of ["/etc/passwd", "/etc/resolv.conf", "/etc/ssl", "/etc/pki"]) if (existsSync(path)) args.push("--ro-bind", path, path); + args.push("--ro-bind", runtimeRoot, "/zcode", "--bind", profile, homedir(), + "--ro-bind", fileURLToPath(new URL("./oauth-bootstrap.cjs", import.meta.url)), "/bridge.cjs", + "--clearenv", "--setenv", "HOME", homedir(), "--setenv", "PATH", "/usr/bin:/bin", + "--setenv", "ELECTRON_RUN_AS_NODE", "1", "--chdir", homedir(), "/zcode/zcode", "/bridge.cjs", mode); + return args; +} + +export async function runNativeOAuth(options: { + runtime: string; profileHome: string; mode: "login" | "capabilities" | "refresh"; + signal: AbortSignal; onEvent: (event: NativeOAuthEvent) => void; +}): Promise { + if (options.signal.aborted) throw new Error("login_cancelled"); + const [executable, ...args] = nativeOAuthCommand(options.runtime, options.profileHome, options.mode); + const child = spawn(executable!, args, { env: desktopSandboxEnabled() ? { PATH: "/usr/bin:/bin" } : { + PATH: "/usr/bin:/bin", HOME: homedir(), ELECTRON_RUN_AS_NODE: "1", ZCODE_DATA_BASE_DIR: options.profileHome, + }, stdio: ["ignore", "pipe", "pipe"] }); + child.stderr.resume(); + let invalid = false, finished = false, buffer = "", total = 0; + const stop = () => child.kill("SIGKILL"); + options.signal.addEventListener("abort", stop, { once: true }); + if (options.signal.aborted) stop(); + const timer = setTimeout(stop, options.mode === "login" ? 310_000 : 20_000); + try { + await new Promise((resolve, reject) => { + child.once("error", () => reject(new Error("native_oauth_failed"))); + child.stdout.on("data", chunk => { + total += chunk.length; + if (total > 16_384 || invalid) { invalid = true; stop(); return; } + buffer += chunk.toString("utf8"); + let end: number; + while ((end = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, end); buffer = buffer.slice(end + 1); + try { + const event = parseNativeOAuthEvent(JSON.parse(line)); + if (finished || event.type === "error") { invalid = true; stop(); } + finished ||= event.type === "authenticated" || event.type === "capabilities"; + options.onEvent(event); + } catch { invalid = true; stop(); } + } + }); + child.once("close", code => code === 0 && finished && !invalid && !buffer && !options.signal.aborted + ? resolve() : reject(new Error(options.signal.aborted ? "login_cancelled" : "native_oauth_failed"))); + }); + } finally { clearTimeout(timer); options.signal.removeEventListener("abort", stop); stop(); } +} diff --git a/src/adapters/zcode/oauth-bootstrap.cjs b/src/adapters/zcode/oauth-bootstrap.cjs new file mode 100644 index 0000000000..2f849a4cb3 --- /dev/null +++ b/src/adapters/zcode/oauth-bootstrap.cjs @@ -0,0 +1,73 @@ +// Runs with a private profile (optional OS sandbox) using the unmodified official Desktop host. +// OAuth HTTP, polling, credentials and refresh belong exclusively to ZCode. +const fs = require("node:fs"); +const { EventEmitter } = require("node:events"); +const { createHash } = require("node:crypto"); +const write = process.stdout.write.bind(process.stdout); +process.stdout.write = process.stderr.write.bind(process.stderr); +const send = value => write(JSON.stringify(value) + "\n"); +let stage = "native_oauth_failed"; +const parent = new EventEmitter(); +parent.postMessage = () => {}; +process.parentPort = parent; +const timer = setTimeout(() => { send({ type: "error", code: "login_expired" }); process.exit(1); }, 300_000); +async function main() { + fs.mkdirSync((process.env.ZCODE_DATA_BASE_DIR || process.env.HOME) + "/.zcode/v2", { recursive: true, mode: 0o700 }); + const dir = (process.argv[3] || "/zcode") + "/resources/app.asar/out/host"; + let classes = []; + for (const name of fs.readdirSync(dir).filter(n => n.startsWith("chunk-") && n.endsWith(".js"))) { + const source = fs.readFileSync(dir + "/" + name, "utf8"); + if (source.includes('"ChannelClient"') && source.includes('"MessagePortProtocol"')) { + classes = Object.values(await import(dir + "/" + name)); break; + } + } + const Client = classes.find(v => v?.name === "ChannelClient"); + const Protocol = classes.find(v => v?.name === "MessagePortProtocol"); + const left = new EventEmitter(), right = new EventEmitter(); + for (const [a, b] of [[left, right], [right, left]]) { + a.postMessage = data => queueMicrotask(() => b.emit("message", { data, ports: [] })); + a.start = () => {}; a.close = () => a.emit("close"); + a.addEventListener = a.on.bind(a); a.removeEventListener = a.off.bind(a); + } + const client = new Client(new Protocol(left)); + await import(dir + "/index.js"); + parent.emit("message", { data: { type: "init-local" }, ports: [right] }); + const oauth = client.getChannel("oauth"); + if (process.argv[2] === "capabilities") { + const providers = await oauth.call("getProviders", []); + if (!providers.some(p => (p.id ?? p.providerId) === "zai" && p.enabled)) throw new Error(); + send({ type: "capabilities", supported: true }); return; + } + const publish = async result => { + stage = "session_restore_failed"; + if (result?.kind !== "session" || result.provider !== "zai" || typeof result.userInfo?.id !== "string" || !result.userInfo.id) throw new Error(); + // Official service materializes/refreshes built-in provider credentials and model config. + stage = "model_setup_failed"; + const models = client.getChannel("model-provider"); + await models.call("getAll", []); + await models.call("refreshCodingPlanApiKey", ["builtin:zai-coding-plan"]); + send({ type: "authenticated", subjectHash: createHash("sha256").update("zai\0" + result.userInfo.id).digest("hex") }); + }; + if (process.argv[2] === "refresh") { + stage = "session_restore_failed"; + const session = await oauth.call("restoreCachedSessionState", []); + if (session?.status !== "authenticated") throw new Error(); + await publish({ kind: "session", provider: "zai", userInfo: session.userInfo }); return; + } + if (process.argv[2] !== "login") throw new Error(); + const flow = await oauth.call("startOAuthWithPolling", ["zai"]); + const url = new URL(flow.authorizeUrl); + if (url.protocol !== "https:" || url.hostname !== "chat.z.ai" || url.username || url.password) throw new Error(); + send({ type: "authorization", url: url.href }); + while (true) { + await new Promise(resolve => setTimeout(resolve, 2_000)); + const result = await oauth.call("pollPendingOAuth", []); + if (!result) continue; + if (result.kind !== "session" || result.provider !== "zai" || typeof result.userInfo?.id !== "string" || !result.userInfo.id) throw new Error(); + await publish(result); + return; + } +} +main().then(() => { clearTimeout(timer); process.exit(0); }).catch(() => { + send({ type: "error", code: stage }); process.exit(1); +}); diff --git a/src/adapters/zcode/quota.ts b/src/adapters/zcode/quota.ts index 6970a38959..32388069a7 100644 --- a/src/adapters/zcode/quota.ts +++ b/src/adapters/zcode/quota.ts @@ -1,3 +1,4 @@ +import { refreshAccount } from "./account-runtime"; import { verifyDesktopSandbox } from "./desktop-sandbox"; import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; @@ -8,7 +9,7 @@ import { fileURLToPath } from "node:url"; import type { OcxProviderConfig } from "../../types"; import type { ProviderQuota } from "../../providers/quota-types"; import { getConfigDir } from "../../config/paths"; -import { desktopSandboxEnabled, desktopStatus, resolveDesktopRuntime } from "./desktop"; +import { desktopProfile, desktopSandboxEnabled, desktopStatus, resolveDesktopRuntime } from "./desktop"; import { loadZcodeSettings } from "./settings"; export interface QuotaContext { identity: string; runtimeRoot: string; config: string; credentials?: string; sourceProvider: string; managed: boolean } @@ -44,16 +45,16 @@ export function parseZcodeQuota(value: unknown, now = Date.now()): ProviderQuota function context(provider: OcxProviderConfig): QuotaContext { if (provider.adapter !== "zcode" || provider.authMode !== "local" || provider.disabled || process.platform !== "linux") throw new Error("unavailable"); - const settings = loadZcodeSettings(); + const settings = loadZcodeSettings(process.env, provider.zcodeAccountId); const managed = settings.desktopModels !== undefined; - const runtime = managed ? desktopStatus().runtime : process.env.OCX_ZCODE_DESKTOP_RUNTIME; + const runtime = managed ? desktopStatus(provider.zcodeAccountId).runtime : process.env.OCX_ZCODE_DESKTOP_RUNTIME; if (!runtime) throw new Error("unavailable"); const runtimeRoot = dirname(dirname(dirname(resolveDesktopRuntime(runtime)))); - const config = managed ? join(homedir(), ".zcode/v2/config.json") : settings.settingsPath; + const config = managed ? desktopProfile(provider.zcodeAccountId).config : settings.settingsPath; if (!lstatSync(config).isFile() || statSync(config).size > 4 * 1024 * 1024) throw new Error("unavailable"); if (!managed && !realpathSync(config).startsWith(realpathSync(settings.home) + sep)) throw new Error("unavailable"); - const credentialFile = join(homedir(), ".zcode/v2/credentials.json"); - const credentials = managed && existsSync(credentialFile) ? credentialFile : undefined; + const credentialFile = managed ? desktopProfile(provider.zcodeAccountId).credentials : undefined; + const credentials = managed && credentialFile && existsSync(credentialFile) ? credentialFile : undefined; if (credentials && (!lstatSync(credentials).isFile() || statSync(credentials).size > 1024 * 1024)) throw new Error("unavailable"); const sourceProvider = provider.defaultModel?.split("/")[0] || (managed ? "builtin:zai-coding-plan" : "zai"); if (managed && sourceProvider !== "builtin:zai-coding-plan") throw new Error("unavailable"); @@ -126,18 +127,19 @@ async function probe(c: QuotaContext): Promise { } finally { if (temporaryHome) rmSync(temporaryHome, { recursive: true, force: true }); } } -let inflight: { identity: string; promise: Promise } | undefined; +const inflight = new Map>(); export async function readZcodeQuota(provider: OcxProviderConfig, deps: { context?: typeof context; probe?: typeof probe } = {}): Promise<{ identity: string; quota: ProviderQuota } | null> { let c: QuotaContext; - try { c = (deps.context ?? context)(provider); } catch { return null; } - // Never accumulate native hosts during repeated dashboard refreshes. - if (inflight && inflight.identity !== c.identity) return null; - if (!inflight) { - const request = { identity: c.identity, promise: (deps.probe ?? probe)(c).catch(() => null) }; - inflight = request; - void request.promise.finally(() => { if (inflight === request) inflight = undefined; }); + try { if (provider.zcodeAccountId && !deps.context) await refreshAccount(provider.zcodeAccountId); c = (deps.context ?? context)(provider); } catch { return null; } + // Coalesce per profile, without letting the first account starve every other account. + let pending = inflight.get(c.identity); + if (!pending) { + if (inflight.size >= 20) return null; + pending = (deps.probe ?? probe)(c).catch(() => null); + inflight.set(c.identity, pending); + void pending.finally(() => { if (inflight.get(c.identity) === pending) inflight.delete(c.identity); }); } - const quota = await inflight.promise; + const quota = await pending; try { return quota && (deps.context ?? context)(provider).identity === c.identity ? { identity: c.identity, quota } : null; } catch { return null; } } diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index 611b2b13f3..a6484e7c6a 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -1,3 +1,4 @@ +import { accountRoot } from "./accounts"; import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, statSync } from "node:fs"; import { isAbsolute, join, sep } from "node:path"; import { createHash } from "node:crypto"; @@ -9,6 +10,7 @@ export function record(value: unknown): JsonObject { } export interface ZcodeSettings { + accountId?: string; command: string[]; home: string; workspace: string; @@ -18,10 +20,11 @@ export interface ZcodeSettings { } /** Execution authority is operator environment or persisted GUI consent, never data-plane input. */ -export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env): ZcodeSettings { +export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env, accountId?: string): ZcodeSettings { + if (accountId !== undefined) accountRoot(accountId); // Explicit env fixtures remain hermetic. Production prefers the GUI's persisted connection. if (env === process.env) { - const desktop = loadDesktopSettings(); + const desktop = loadDesktopSettings(accountId); if (desktop) return desktop; } if (env.OCX_ZCODE_NATIVE_TOOLS !== "1") { @@ -135,6 +138,6 @@ function readModels(settings: ZcodeSettings): ZcodeModel[] { } /** Never include runtimeModel (which may contain a key) in discovery or management output. */ -export function discoverZcodeModels(): Array<{ id: string; label: string; contextWindow?: number }> { - return readZcodeModels(loadZcodeSettings()).map(({ id, label, contextWindow }) => ({ id, label, contextWindow })); +export function discoverZcodeModels(accountId?: string): Array<{ id: string; label: string; contextWindow?: number }> { + return readZcodeModels(loadZcodeSettings(process.env, accountId)).map(({ id, label, contextWindow }) => ({ id, label, contextWindow })); } diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 8305753b3a..4d977168c4 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -1658,7 +1658,7 @@ async function fetchProviderModelsWithAuth( ); if (prov.adapter === "zcode") { try { - const models = discoverZcodeModels().map(model => ({ + const models = discoverZcodeModels(prov.zcodeAccountId).map(model => ({ id: model.id, provider: name, ...catalogHintsFromProviderConfig(name, prov, model.id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias), displayName: model.label, diff --git a/src/router.ts b/src/router.ts index ed8db5bed2..1bcecde30a 100644 --- a/src/router.ts +++ b/src/router.ts @@ -113,7 +113,7 @@ export function knownModelIdsForProvider( const ids = new Set(); for (const id of prov.models ?? []) ids.add(id); if (prov.adapter === "zcode") { - for (const id of desktopRoutingModelIds()) ids.add(id); + for (const id of desktopRoutingModelIds(prov.zcodeAccountId)) ids.add(id); } if (prov.defaultModel) ids.add(prov.defaultModel); const registry = providerMatchesRegistryTransportWithStaticGuards(provName, prov) diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 3a93246cd0..d9e63ed30c 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -588,6 +588,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown): return "provider must be a plain object"; } const raw = provider as Record; + if (raw.zcodeAccountId !== undefined && (raw.adapter !== "zcode" || typeof raw.zcodeAccountId !== "string" + || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(raw.zcodeAccountId))) { + return "zcodeAccountId must identify a saved ZCode account."; + } const pinsError = providerReasoningPinsConfigError(raw); if (pinsError) return pinsError; for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) { @@ -785,7 +789,8 @@ export function copyIfDefined( type ProviderConfigFieldPolicy = "editor" | "redacted" | "runtime"; const PROVIDER_CONFIG_FIELD_POLICY = { - alias: "editor", + zcodeAccountId: "editor", + alias: "editor", modelAliases: "editor", modelDisplayNames: "editor", defaultAliases: "editor", diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 5d38b25bcb..8f80dce1d8 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -705,6 +705,10 @@ function canonicalOpenAiBudgetPatchError( export async function handleProviderRoutes(ctx: ManagementContext): Promise { const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx; + if (url.pathname === "/api/zcode-accounts" || url.pathname.startsWith("/api/zcode-accounts/")) { + const { handleZcodeAccountRoutes } = await import("./zcode-account-routes"); + return handleZcodeAccountRoutes(ctx); + } if (url.pathname === "/api/zcode-desktop" || url.pathname.startsWith("/api/zcode-desktop/")) { const { handleZcodeDesktopRoutes } = await import("./zcode-desktop-routes"); return handleZcodeDesktopRoutes(ctx); @@ -1374,7 +1378,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise 0, models: models.length, latencyMs: 0, message: "Local catalog loaded. Account inference is validated only by an explicit agent turn." }); } catch { diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index d8f81de5ec..1ebdae1eaf 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -214,6 +214,14 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/debug/injection-logs", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/debug/logs", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/debug/usage-logs", module: "server/management/logs-usage-routes", mutates: false }, + { method: "GET", path: "/api/zcode-accounts", module: "server/management/zcode-account-routes", mutates: false }, + { method: "GET", path: "/api/zcode-accounts/login", module: "server/management/zcode-account-routes", mutates: false }, + { method: "POST", path: "/api/zcode-accounts/login", module: "server/management/zcode-account-routes", mutates: true }, + { method: "POST", path: "/api/zcode-accounts/cancel", module: "server/management/zcode-account-routes", mutates: true }, + { method: "POST", path: "/api/zcode-accounts/complete", module: "server/management/zcode-account-routes", mutates: true }, + { method: "POST", path: "/api/zcode-accounts/activate", module: "server/management/zcode-account-routes", mutates: true }, + { method: "POST", path: "/api/zcode-accounts/rename", module: "server/management/zcode-account-routes", mutates: true }, + { method: "POST", path: "/api/zcode-accounts/remove", module: "server/management/zcode-account-routes", mutates: true }, { method: "GET", path: "/api/logs", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/storage/cleanup-policy", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/storage/cleanup-policy/test-stream", module: "server/management/logs-usage-routes", mutates: false, exempt: { reason: "test-seam", why: "Opt-in streaming seam declared at src/storage/policy-job.ts:71." } }, diff --git a/src/server/management/zcode-account-routes.ts b/src/server/management/zcode-account-routes.ts new file mode 100644 index 0000000000..3fe69e863d --- /dev/null +++ b/src/server/management/zcode-account-routes.ts @@ -0,0 +1,186 @@ +import { randomUUID } from "node:crypto"; +import { existsSync, renameSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { accountProfile, accountRoot, allocateAccount, listAccounts, readAccount, removeAccountFiles, writeAccount } from "../../adapters/zcode/accounts"; +import { accountRuntimeBusy, invalidateAccountRefresh } from "../../adapters/zcode/account-runtime"; +import { connectDesktop, desktopStatus, disconnectDesktop, resolveDesktopRuntime, validateDesktopWorkspace } from "../../adapters/zcode/desktop"; +import { runNativeOAuth } from "../../adapters/zcode/native-oauth"; +import { saveConfigPreservingClaudeCode, withConfigMutationLockSync } from "../../config"; +import { clearModelCache } from "../../codex/model-cache"; +import { clearGatherRoutedModelsInflight } from "../../codex/catalog/provider-fetch"; +import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; +import { jsonResponse } from "../auth-cors"; +import { readBoundedJsonRequestBody } from "../request-decompress"; +import { activateDesktopProvider, desktopActivation, readDesktopCatalogSlugs } from "./zcode-desktop-activation"; +import type { ManagementContext } from "./context"; + +type Job = { id: string; accountId: string; replaceId?: string; runtime: string; workspace: string; + phase: "waiting" | "authenticated" | "completing" | "finished" | "failed"; + url?: string; task?: Promise; identity?: string; error?: string; controller: AbortController; timer: ReturnType }; +const jobs = new Map(); +const pendingFor = (id: string) => [...jobs.values()].some(j => (j.accountId === id || j.replaceId === id) && !["failed", "finished"].includes(j.phase)); +const safeJob = (job: Job) => ({ jobId: job.id, accountId: job.replaceId ?? job.accountId, + phase: job.phase, ...(job.url ? { url: job.url } : {}), ...(job.error ? { error: job.error } : {}) }); +const fail = (code: string): never => { throw new Error(code); }; +const safeErrors = new Set(["account_invalid", "account_limit", "account_busy", "account_referenced", + "account_duplicate", "account_identity_mismatch", "account_login_required", "job_invalid", "native_oauth_failed", + "desktop_missing", "workspace_invalid", "node_missing", "node_incompatible", "sandbox_missing", "sandbox_unavailable", + "catalog_update_failed", "provider_registration_failed", "runtime_failed", "models_missing", + "platform_unsupported", "profile_missing", "connection_invalid", "busy"]); + +const services = { connectDesktop, desktopStatus, disconnectDesktop, runNativeOAuth, resolveDesktopRuntime, + validateDesktopWorkspace, accountRuntimeBusy, readDesktopCatalogSlugs }; +export function resetZcodeAccountJobsForTests(): void { + for (const job of jobs.values()) { job.controller.abort(); clearTimeout(job.timer); } + jobs.clear(); +} +export async function handleZcodeAccountRoutes(ctx: ManagementContext, deps = services): Promise { + const { connectDesktop, desktopStatus, disconnectDesktop, runNativeOAuth, resolveDesktopRuntime, + validateDesktopWorkspace, accountRuntimeBusy, readDesktopCatalogSlugs } = deps; + const path = ctx.url.pathname; + if (!path.startsWith("/api/zcode-accounts")) return null; + // URLs and account operations belong to a real GUI principal, not data/admin-token callers. + if (ctx.principal !== "gui-session") return jsonResponse({ error: "dashboard_required" }, 403); + try { + if (path === "/api/zcode-accounts" && ctx.req.method === "GET") { + return jsonResponse({ accounts: listAccounts().map(({ id, label }) => { + const status = desktopActivation(ctx, desktopStatus(id), readDesktopCatalogSlugs); + return { id, label, connected: status.connected, activation: status.activation, providerName: status.providerName, + busy: accountRuntimeBusy(id) || pendingFor(id) }; + }) }); + } + if (path === "/api/zcode-accounts/login" && ctx.req.method === "GET") { + const job = jobs.get(ctx.url.searchParams.get("jobId") ?? ""); + if (!job) return jsonResponse({ error: "job_invalid" }, 404); + return jsonResponse(safeJob(job)); + } + if (ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405); + const raw = await readBoundedJsonRequestBody(ctx.req, 12_000, undefined, { signal: AbortSignal.any([ctx.req.signal, AbortSignal.timeout(5000)]) }); + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return jsonResponse({ error: "invalid_request" }, 400); + const body = raw as Record; + if (body.consent !== true) return jsonResponse({ error: "consent_required" }, 400); + + if (path === "/api/zcode-accounts/login") { + if ([...jobs.values()].filter(job => !["failed", "finished"].includes(job.phase)).length >= 8) return jsonResponse({ error: "account_busy" }, 409); + if (typeof body.label !== "string" || typeof body.runtime !== "string" || typeof body.workspace !== "string") return fail("account_invalid"); + const runtime = resolveDesktopRuntime(body.runtime), workspace = validateDesktopWorkspace(body.workspace); + if (body.accountId !== undefined && typeof body.accountId !== "string") return fail("account_invalid"); + const replaceId = typeof body.accountId === "string" ? readAccount(body.accountId).id : undefined; + if (replaceId && (accountRuntimeBusy(replaceId) || pendingFor(replaceId))) return fail("account_busy"); + const account = allocateAccount(body.label); + const job: Job = { id: randomUUID(), accountId: account.id, replaceId, runtime, workspace, + phase: "waiting", controller: new AbortController(), timer: undefined! }; + job.timer = setTimeout(() => { + if (job.phase === "completing") return; + job.controller.abort(); jobs.delete(job.id); + void job.task?.then(() => { + if (!desktopStatus(job.accountId).connected && !readAccount(job.accountId).subjectHash) removeAccountFiles(job.accountId); + }).catch(() => {}); + }, 10 * 60_000); + job.timer.unref?.(); jobs.set(job.id, job); + job.task = runNativeOAuth({ runtime, profileHome: accountProfile(account.id), mode: "login", signal: job.controller.signal, + onEvent: event => { + if (event.type === "authorization") job.url = event.url; + if (event.type === "authenticated") job.identity = event.subjectHash; + if (event.type === "error") job.error = event.code; + } }).then(() => { + delete job.url; + if (!job.identity || job.controller.signal.aborted) throw new Error(); + job.phase = "authenticated"; + }).catch(() => { delete job.url; job.phase = "failed"; job.error ??= "native_oauth_failed"; }); + return jsonResponse(safeJob(job)); + } + if (path === "/api/zcode-accounts/cancel") { + const job = jobs.get(String(body.jobId)); + if (!job || job.phase === "completing") return fail("job_invalid"); + job.controller.abort(); await job.task; clearTimeout(job.timer); jobs.delete(job.id); + if (!desktopStatus(job.accountId).connected) removeAccountFiles(job.accountId); + return jsonResponse({ ok: true }); + } + if (path === "/api/zcode-accounts/complete") { + const job = jobs.get(String(body.jobId)); + if (!job || !job.identity || job.controller.signal.aborted || !["authenticated", "finished"].includes(job.phase)) return fail("job_invalid"); + if (job.phase === "finished") return jsonResponse(await activateDesktopProvider(ctx, desktopStatus(job.replaceId ?? job.accountId), readDesktopCatalogSlugs)); + if ([...jobs.values()].some(other => other.id !== job.id && other.phase === "completing" && other.identity === job.identity)) return fail("account_busy"); + const duplicate = listAccounts().find(a => a.subjectHash === job.identity && a.id !== job.accountId && a.id !== job.replaceId); + if (duplicate) return jsonResponse({ error: "account_duplicate", accountId: duplicate.id }, 409); + const id = job.replaceId ?? job.accountId; + if (accountRuntimeBusy(id)) return fail("account_busy"); + if (job.replaceId && readAccount(id).subjectHash !== job.identity) return fail("account_identity_mismatch"); + job.phase = "completing"; + let backup: string | undefined; + try { + if (job.replaceId) { + backup = join(accountRoot(id), "profile-previous-" + randomUUID()); + renameSync(accountProfile(id), backup); + renameSync(accountProfile(job.accountId), accountProfile(id)); + } + const status = await connectDesktop(job.runtime, job.workspace, id); + writeAccount({ ...readAccount(id), subjectHash: job.identity }); + invalidateAccountRefresh(id); + if (backup) { rmSync(backup, { recursive: true, force: true }); backup = undefined; removeAccountFiles(job.accountId); } + job.phase = "finished"; clearGatherRoutedModelsInflight(); + return jsonResponse(await activateDesktopProvider(ctx, status, readDesktopCatalogSlugs)); + } catch (error) { + if (backup && existsSync(backup)) { + renameSync(accountProfile(id), accountProfile(job.accountId)); renameSync(backup, accountProfile(id)); + } + job.phase = "authenticated"; throw error; + } + } + const id = typeof body.accountId === "string" ? readAccount(body.accountId).id : fail("account_invalid"); + if (accountRuntimeBusy(id) || pendingFor(id)) return fail("account_busy"); + if (path === "/api/zcode-accounts/activate") return jsonResponse(await activateDesktopProvider(ctx, desktopStatus(id), readDesktopCatalogSlugs)); + if (path === "/api/zcode-accounts/rename") { + if (typeof body.label !== "string") return fail("account_invalid"); + const previous = readAccount(id), next = { ...previous, label: body.label.trim() }; + const status = desktopStatus(id); + withConfigMutationLockSync(() => { + const providers = ctx.config.providers; + try { + writeAccount(next); + ctx.config.providers = Object.fromEntries(Object.entries(providers).map(([name, provider]) => { + if (provider.zcodeAccountId !== id) return [name, provider]; + const modelDisplayNames = { ...provider.modelDisplayNames }; + for (const model of status.models) { + if (modelDisplayNames[model.id] === `${previous.label} / ${model.label}`) { + modelDisplayNames[model.id] = `${next.label} / ${model.label}`; + } + } + return [name, { ...provider, modelDisplayNames }]; + })); + (ctx.deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(ctx.config); + } catch { + ctx.config.providers = providers; writeAccount(previous); + return fail("provider_registration_failed"); + } + }); + clearGatherRoutedModelsInflight(); + return jsonResponse(await activateDesktopProvider(ctx, status, readDesktopCatalogSlugs)); + } + if (path === "/api/zcode-accounts/remove") { + const names = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.zcodeAccountId === id); + const { providers: _providers, ...rest } = ctx.config; + if (names.some(name => JSON.stringify(rest).includes(name))) return fail("account_referenced"); + // Revoke first. A failed config/catalog save is explicit and cannot silently use another account. + await disconnectDesktop(id); + withConfigMutationLockSync(() => { + const previous = { ...ctx.config.providers }; + try { + for (const name of names) delete ctx.config.providers[name]; + (ctx.deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(ctx.config); + } catch (error) { ctx.config.providers = previous; throw error; } + }); + for (const name of names) clearModelCache(name); + reconcileLiveStateStores(); clearGatherRoutedModelsInflight(); + const result = await ctx.convergeCodexCatalog(); + if (result.status !== "committed") return fail("catalog_update_failed"); + removeAccountFiles(id); invalidateAccountRefresh(id); + return jsonResponse({ ok: true }); + } + return jsonResponse({ error: "not_found" }, 404); + } catch (error) { + const code = error instanceof Error && safeErrors.has(error.message) ? error.message : "native_oauth_failed"; + return jsonResponse({ error: code }, 400); + } +} diff --git a/src/server/management/zcode-desktop-activation.ts b/src/server/management/zcode-desktop-activation.ts index 5c59f0dfa2..f1ace061de 100644 --- a/src/server/management/zcode-desktop-activation.ts +++ b/src/server/management/zcode-desktop-activation.ts @@ -1,3 +1,4 @@ +import { readAccount } from "../../adapters/zcode/accounts"; import { saveConfigPreservingClaudeCode, withConfigMutationLockSync } from "../../config"; import { readCatalog, readCodexCatalogPath, catalogModelSlug, applyProviderConfigHints } from "../../codex/catalog"; import { clearModelCache } from "../../codex/model-cache"; @@ -9,15 +10,15 @@ type DesktopStatus = ReturnType; export const readDesktopCatalogSlugs = (): string[] => (readCatalog(readCodexCatalogPath())?.models ?? []) .filter(row => row.visibility === "list").map(row => String(row.slug)); -function providerName(ctx: ManagementContext): string | undefined { - const matches = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.adapter === "zcode"); +function providerName(ctx: ManagementContext, accountId?: string): string | undefined { + const matches = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.adapter === "zcode" && ctx.config.providers[name]?.zcodeAccountId === accountId); if (matches.includes("zcode")) return "zcode"; return matches.length === 1 ? matches[0] : undefined; } /** Read actual persisted catalog evidence, not just a successful protocol or cache flush. */ export function desktopActivation(ctx: ManagementContext, status: DesktopStatus, readSlugs = readDesktopCatalogSlugs) { - const name = providerName(ctx); + const name = providerName(ctx, status.accountId); const provider = name ? ctx.config.providers[name] : undefined; const registered = !!provider && provider.disabled !== true && provider.authMode === "local"; let catalogReady = false; @@ -36,14 +37,14 @@ export async function activateDesktopProvider(ctx: ManagementContext, status: De let name: string; try { withConfigMutationLockSync(() => { - name = providerName(ctx) ?? "zcode"; + name = providerName(ctx, status.accountId) ?? (status.accountId ? `zcode-${status.accountId}` : "zcode"); const existing = ctx.config.providers[name]; - if ((!providerName(ctx) && Object.values(ctx.config.providers).some(p => p.adapter === "zcode")) - || (existing && (existing.adapter !== "zcode" || existing.authMode !== "local"))) throw new Error("provider conflict"); + if ((!providerName(ctx, status.accountId) && Object.values(ctx.config.providers).some(p => p.adapter === "zcode" && p.zcodeAccountId === status.accountId)) + || (existing && (existing.adapter !== "zcode" || existing.authMode !== "local" || existing.zcodeAccountId !== status.accountId))) throw new Error("provider conflict"); // Reconnection only enables the existing provider. Never replace its custom settings, // explicit model filters, pricing, alias or defaults with a form/preset payload. const next = existing ? { ...existing, disabled: false } - : { adapter: "zcode" as const, authMode: "local" as const, baseUrl: "https://zcode.z.ai", disabled: false }; + : { adapter: "zcode" as const, authMode: "local" as const, baseUrl: "https://zcode.z.ai", disabled: false, ...(status.accountId ? { zcodeAccountId: status.accountId, modelDisplayNames: Object.fromEntries(status.models.map(m => [m.id, `${readAccount(status.accountId!).label} / ${m.label}`])) } : {}) }; try { ctx.config.providers[name] = next; (ctx.deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(ctx.config); diff --git a/src/types/provider.ts b/src/types/provider.ts index e65130a4fa..fe469fe95a 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -228,6 +228,8 @@ export type TierDecision = * retries are allowed; OAuth/forward credentials and local runtimes are never replayed. */ export interface OcxProviderConfig { + /** Explicit manual ZCode account binding; never participates in an implicit pool. */ + zcodeAccountId?: string; /** Optional short provider namespace used only at request/catalog presentation time. */ alias?: string; /** Native model id -> short, slash-free request alias. */ diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index f8fc2e4c33..4e31416441 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -58,3 +58,18 @@ so the schema is not something a user can fix from configuration (issue #2673). ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +## ZCode saved accounts + +`src/adapters/zcode/accounts.ts` stores private UUID-scoped metadata and official profiles. +`src/adapters/zcode/native-oauth.ts` and `src/adapters/zcode/oauth-bootstrap.cjs` invoke only +ZCode's installed host OAuth/credential services; authorization URLs and safe stage codes are +projected to the browser, never tokens. The official cached-session restore and Coding Plan +refresh run before account use. Disabled/unavailable vendor profiles remain unavailable. + +The provider's `zcodeAccountId` is an exact binding through routing, catalog discovery, +app-server settings, session/DB scope and quota reads. An invalid or revoked binding fails +closed, never to the legacy Desktop profile or another account. Legacy unbound `zcode` +retains its previous profile. Account providers do not participate in an implicit pool. +Native tools still run on the host by default; optional OS sandboxing is independent of +profile separation and does not turn the latter into a security boundary. diff --git a/structure/catalog.md b/structure/catalog.md index 9a0ca08296..a4d975f9d7 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -261,3 +261,7 @@ provider wire mapping; unpinned native requests retain their existing pass-throu > Decision record: [ADR-0025](decisions/ADR-0025-ultra-reasoning-level.md) > Decision record: [ADR-0026](decisions/ADR-0026-ultra-reasoning-level.md) + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index f8e3691f38..4b5b0a0a51 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -75,3 +75,7 @@ away from. Resolution stays a pure function of (env, platform, home) so the Wind testable on any host: stubbing `process.platform` does not propagate to `os.platform()` under Bun. > Decision record: [ADR-0046](../decisions/ADR-0046-claude-desktop-config-library-resolution.md) + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/codex-home.md b/structure/codex-home.md index 9b5e1ded4a..742f0f92af 100644 --- a/structure/codex-home.md +++ b/structure/codex-home.md @@ -223,3 +223,7 @@ a deliberate user choice: - Project-level Codex config that bypasses managed routing (`src/codex/project-config-warnings.ts`), surfaced by `ocx doctor` as a warning rather than an override. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/config.md b/structure/config.md index 224c5a350d..bc0e8a647a 100644 --- a/structure/config.md +++ b/structure/config.md @@ -192,3 +192,7 @@ the residual directory for manual review; there is no recursive-delete fallback. ## Remote client key files Client connection metadata stores a stable `apiKeyId` and a non-secret rotation `pendingOperation`. The current data secret remains only in `service-api-token`; a bounded rotation temporarily keeps the old secret in owner-only `service-api-token.prev`. Commit or recovery clears the marker before orphan cleanup. `ocx disconnect` is local-only and leaves remote revocation to the hub's **Integrations → API Keys** page. Hub and local usage stores are not mirrored. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 4183571a2a..6e22e84bf6 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -66,3 +66,7 @@ injects summary generation into a request, and config validation rejects a deliv conflicts with `modelSupportsReasoningSummaries: false` for the same model. > Decision record: [ADR-0045](../decisions/ADR-0045-standalone-images.md) + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index de5bf58fa0..286e654eb5 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -86,3 +86,7 @@ falling back to OpenCodex guesses, and the integration does not write the remove ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/design-methodology.md b/structure/design-methodology.md index bcaf940d72..084c9d4dee 100644 --- a/structure/design-methodology.md +++ b/structure/design-methodology.md @@ -36,3 +36,7 @@ surfaces, run through all 3 stages in order. - Design methodology: Product-Personality-Selection (dev-uiux-design §1) - 6 design dials: mood, lightness, density, shape, typography, motion - 7 axes total: design → domain → feature/data/security/ops/cost (derived) + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index c6b35fbad8..e97d40214d 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -504,3 +504,16 @@ converge the Codex catalog once and return its disposition. The Models UI owns a picker data resource so failure cannot erase the ordinary model inventory; Apply publishes through the resource's generation fence, and Most used reads usage only on explicit Apply. Stored mode survives availability drift, while complete/native custom orders await explicit replacement. + +## ZCode manual account lifecycle + +`gui/src/components/ZcodeAccountsPane.tsx` uses +`src/server/management/zcode-account-routes.ts` for saved-account login, polling, completion, +activation retry, rename and removal. These routes require a GUI-session principal; mutations +require explicit consent. The official OAuth job uses a fresh private profile. Completion +checks identity and protocol before registering an account-bound provider and converging the +catalog; a partial result stays visible and retryable. No default selection or inference is +part of this flow. Reconnect retains custom provider settings and rejects a different identity. +Rename changes only generated labels; removal refuses busy/referenced accounts. In-progress +OAuth jobs expire and do not survive restart; saved account profiles and bindings do. +See [the ZCode account runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 6d7988af78..9e1ec377eb 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -300,3 +300,7 @@ This keeps release runs short and makes release a deployment of a verified commi ## Remote Hub locale and release gate The Remote Hub guide and affected CLI, server-config, management-API, and dashboard references have eight sources: root English plus `fr`, `ko`, `zh-cn`, `zh-tw`, `ru`, `ja`, and `tr`. English is canonical; commands, defaults, endpoint auth, and warnings remain exact in translations. A release requires the remote-only focused/full gates, privacy scan, GUI/docs builds, protocol compatibility receipts, and the MAINTAINERS security review for the exact head. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 31319b28de..9578eea678 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -132,3 +132,7 @@ Binary detection decodes only the supplied buffer view; malformed UTF-8 can itse so the flag does not identify the peer responsible for corruption. Existing diagnostic files are not rewritten. Audio devices, WebRTC media negotiation, captions and spoken handoff delivery remain client responsibilities. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/overview.md b/structure/overview.md index 1802d31b72..15350cfee8 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -103,3 +103,7 @@ would pass while the rule was violated. - **INV-HOME-01** — `CODEX_HOME` wins over `~/.codex` when present and valid. - **INV-SLUG-01** — Routed model slugs use `provider/model`. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 1b3d729f1a..4173e770d6 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -268,3 +268,7 @@ byte-limit boundaries. ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index cd4260b578..de6666d663 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -86,3 +86,7 @@ Namespaced tools do not acquire bare-shell behavior. Regression coverage lives i ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 9f86870bae..c061531d85 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -368,3 +368,7 @@ model settings, and noncanonical `openai` rows never receive that recovery path. `GET /api/codex-auth/accounts?refresh=1` treats missing main credentials, HTTP 401, and allowlisted terminal 403 codes as `needsReauth`; generic permission failures remain non-terminal, and a successful main usage refresh clears the runtime mark. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 765b85a763..e7d52aca7b 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -41,3 +41,7 @@ to treat an explicit empty array as authoritative. Within this marked client-fac malformed, gapped, oversized, contradictory, failed, or incomplete streams stay fail-closed. > Decision record: [ADR-0059](../decisions/ADR-0059-xai-grok-hardening-official-grok-build-contract.md) + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/runtime.md b/structure/runtime.md index 693b80d600..09e7082941 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -189,3 +189,7 @@ not an authentication or entitlement decision. ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/subagents.md b/structure/subagents.md index a3513a726a..5a248f4b2a 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -195,3 +195,7 @@ Claude ModelInfo ordering receives optional `{ modelPickerOrder, featured }` aft It orders routed output groups after alias deduplication, preserving the collision winner and base/1M/Fast siblings. Native groups and explicit Desktop profile ownership are unchanged. Native Codex advertisements still follow display priority; private guidance ranks do not freeze them. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index fa0d8aa6d3..bf231f9b26 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -61,3 +61,7 @@ executor contract. Main-request migration must not treat that branch as fixed-tr ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index feedd666ee..37c73bc93f 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -470,3 +470,7 @@ not retried. ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 42d3442e99..8f3ab8e532 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -189,3 +189,7 @@ WebSocket clients observe the same canonical lifecycle. `ws-bridge.ts` preserves upstream `failed` and `incomplete` status values in the final WebSocket frame rather than always emitting `response.completed`. If the response status is `failed`, a `response.failed` frame is sent; otherwise `response.completed` carries through the original status. + +ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and +client integration exports. Their profile, catalog and transport contract is maintained in +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index 5e5885e15d..c4a76dc91d 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -1,3 +1,5 @@ +import { handleZcodeAccountRoutes, resetZcodeAccountJobsForTests } from "../../src/server/management/zcode-account-routes"; +import { listAccounts, accountProfile } from "../../src/adapters/zcode/accounts"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { handleZcodeDesktopRoutes } from "../../src/server/management/zcode-desktop-routes"; import type { ManagementContext } from "../../src/server/management/context"; @@ -15,6 +17,7 @@ beforeEach(() => { process.env.OPENCODEX_HOME = home; }); afterEach(() => { + resetZcodeAccountJobsForTests(); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; rmSync(home, { recursive: true, force: true }); }); @@ -142,3 +145,148 @@ test("an existing renamed ZCode provider is reused without adding canonical zcod expect(ctx.config.providers.zcode).toBeUndefined(); expect(ctx.config.providers.desktop?.note).toBe("keep"); }); + +function accountFixture() { + const ctx = context("/api/zcode-accounts/login", {}, "gui-session"); + const connected = new Set(); + let hash = "a".repeat(64), failCatalog = false, active = false; + let slugs: string[] = []; + const deps = { + resolveDesktopRuntime: (path: string) => path, + validateDesktopWorkspace: (path: string) => path, + accountRuntimeBusy: () => active, + desktopStatus: (accountId?: string) => ({ ...status, sandbox: false, accountId, connected: !!accountId && connected.has(accountId) }), + connectDesktop: async (_r: string, _w: string, accountId?: string) => { + connected.add(accountId!); return { ...status, sandbox: false, accountId, connected: true }; + }, + disconnectDesktop: async (id?: string) => { connected.delete(id!); }, + readDesktopCatalogSlugs: () => slugs, + runNativeOAuth: async (options: { onEvent: (e: any) => void }) => { + options.onEvent({ type: "authenticated", subjectHash: hash }); + }, + }; + ctx.convergeCodexCatalog = async () => { + if (failCatalog) throw new Error("fixture catalog failure"); + slugs = Object.entries(ctx.config.providers).filter(([, p]) => p.adapter === "zcode") + .flatMap(([name]) => models.map(m => name + "/" + m.id.replaceAll("/", "-"))); + return { status: "committed", changed: true, degraded: false, notices: [] }; + }; + const call = async (action: string, body: object, principal: ManagementContext["principal"] = "gui-session") => { + const request = context("/api/zcode-accounts/" + action, { consent: true, ...body }, principal); + ctx.req = request.req; ctx.url = request.url; ctx.principal = principal; + return (await handleZcodeAccountRoutes(ctx, deps))!; + }; + const login = async (extra = {}) => { + const result = await (await call("login", { label: "Personal", runtime: "/runtime", workspace: "/project", ...extra })).json(); + await Promise.resolve(); await Promise.resolve(); + return result; + }; + return { ctx, deps, call, login, slugs: () => slugs, hash: (h: string) => { hash = h; }, + failCatalog: (v: boolean) => { failCatalog = v; }, active: (v: boolean) => { active = v; } }; +} +test("manual accounts require GUI consent; account login enables an independent provider/catalog", async () => { + const f = accountFixture(), originalDefault = f.ctx.config.defaultProvider; + expect((await f.call("login", {}, "admin-token")).status).toBe(403); + expect((await f.call("login", { consent: false })).status).toBe(400); + expect(listAccounts()).toHaveLength(0); + const a = await f.login(); + expect(await (await f.call("complete", { jobId: a.jobId })).json()).toMatchObject({ activation: "ready", accountId: a.accountId }); + f.hash("b".repeat(64)); + const b = await f.login({ label: "Work" }); + expect(await (await f.call("complete", { jobId: b.jobId })).json()).toMatchObject({ activation: "ready", accountId: b.accountId }); + const providers = Object.values(f.ctx.config.providers).filter(p => p.adapter === "zcode"); + expect(providers.map(p => p.zcodeAccountId).sort()).toEqual([a.accountId, b.accountId].sort()); + expect(accountProfile(a.accountId)).not.toBe(accountProfile(b.accountId)); + expect(f.slugs()).toHaveLength(4); + expect(f.ctx.config.defaultProvider).toBe(originalDefault); + expect(JSON.stringify(await (await f.call("complete", { jobId: a.jobId })).json())).not.toContain("subjectHash"); +}); +test("duplicate identity is rejected and reconnect preserves provider settings and account id", async () => { + const f = accountFixture(), a = await f.login(); + const ready = await (await f.call("complete", { jobId: a.jobId })).json(); + f.ctx.config.providers[ready.providerName].contextWindow = 64000; + const duplicate = await f.login(); + expect(await (await f.call("complete", { jobId: duplicate.jobId })).json()).toMatchObject({ error: "account_duplicate", accountId: a.accountId }); + await f.call("cancel", { jobId: duplicate.jobId }); + const reconnect = await f.login({ accountId: a.accountId }); + expect(await (await f.call("complete", { jobId: reconnect.jobId })).json()).toMatchObject({ activation: "ready", accountId: a.accountId }); + expect(listAccounts()).toHaveLength(1); + expect(f.ctx.config.providers[ready.providerName].contextWindow).toBe(64000); +}); +test("partial catalog activation retries without login; busy and referenced accounts cannot be removed", async () => { + const f = accountFixture(), a = await f.login(); + f.failCatalog(true); + const partial = await (await f.call("complete", { jobId: a.jobId })).json(); + expect(partial).toMatchObject({ activation: "catalog_pending", error: "catalog_update_failed" }); + f.failCatalog(false); + expect(await (await f.call("activate", { accountId: a.accountId })).json()).toMatchObject({ activation: "ready" }); + f.active(true); + expect(await (await f.call("remove", { accountId: a.accountId })).json()).toMatchObject({ error: "account_busy" }); + f.active(false); + f.ctx.config.defaultProvider = partial.providerName; + expect(await (await f.call("remove", { accountId: a.accountId })).json()).toMatchObject({ error: "account_referenced" }); +}); + +test("rename preserves custom model labels and saved accounts survive config reload", async () => { + const f = accountFixture(), a = await f.login(); + const ready = await (await f.call("complete", { jobId: a.jobId })).json(); + const provider = f.ctx.config.providers[ready.providerName]; + provider.modelDisplayNames![models[1]!.id] = "Custom flash"; + expect(await (await f.call("rename", { accountId: a.accountId, label: "Work" })).json()).toMatchObject({ activation: "ready" }); + f.ctx.config = JSON.parse(readFileSync(join(home, "config.json"), "utf8")); + expect(listAccounts().map(a => a.label)).toEqual(["Work"]); + expect(f.ctx.config.providers[ready.providerName].modelDisplayNames).toEqual({ + [models[0]!.id]: "Work / GLM-5.3", [models[1]!.id]: "Custom flash", + }); + expect(await (await f.call("activate", { accountId: a.accountId })).json()).toMatchObject({ activation: "ready" }); + expect(await (await f.call("remove", { accountId: a.accountId })).json()).toEqual({ ok: true }); + expect(listAccounts()).toHaveLength(0); + expect(f.ctx.config.providers[ready.providerName]).toBeUndefined(); + expect(f.slugs()).toHaveLength(0); +}); +test("reconnecting a different identity or failed protocol preserves the original account", async () => { + const f = accountFixture(), a = await f.login(); + const ready = await (await f.call("complete", { jobId: a.jobId })).json(); + writeFileSync(join(accountProfile(a.accountId), "sentinel"), "original"); + const before = structuredClone(f.ctx.config.providers); + f.hash("b".repeat(64)); + const wrong = await f.login({ accountId: a.accountId }); + expect(await (await f.call("complete", { jobId: wrong.jobId })).json()).toMatchObject({ error: "account_identity_mismatch" }); + await f.call("cancel", { jobId: wrong.jobId }); + f.hash("a".repeat(64)); + const retry = await f.login({ accountId: a.accountId }); + f.deps.connectDesktop = async () => { throw new Error("runtime_failed"); }; + expect(await (await f.call("complete", { jobId: retry.jobId })).json()).toMatchObject({ error: "runtime_failed" }); + expect(readFileSync(join(accountProfile(a.accountId), "sentinel"), "utf8")).toBe("original"); + expect(f.ctx.config.providers).toEqual(before); + expect(f.ctx.config.providers[ready.providerName].zcodeAccountId).toBe(a.accountId); + await f.call("cancel", { jobId: retry.jobId }); + expect(listAccounts()).toHaveLength(1); +}); +test("failed official OAuth has a safe error and cancel removes the draft only", async () => { + const f = accountFixture(); + f.deps.runNativeOAuth = async () => { throw new Error("private vendor token"); }; + const a = await f.login(); + const poll = context("/api/zcode-accounts/login?jobId=" + a.jobId, {}, "gui-session"); + poll.req = new Request(poll.url); + const result = await (await handleZcodeAccountRoutes(poll, f.deps))!.json(); + expect(result).toMatchObject({ phase: "failed", error: "native_oauth_failed" }); + expect(JSON.stringify(result)).not.toContain("private vendor token"); + expect(await (await f.call("cancel", { jobId: a.jobId })).json()).toEqual({ ok: true }); + expect(listAccounts()).toHaveLength(0); +}); + +test("concurrent completions cannot register the same identity twice", async () => { + const f = accountFixture(), a = await f.login(), b = await f.login(); + const connect = f.deps.connectDesktop; + let release!: () => void; + const gate = new Promise(resolve => { release = resolve; }); + f.deps.connectDesktop = async (...args) => { await gate; return connect(...args); }; + const first = f.call("complete", {jobId:a.jobId}); + await new Promise(resolve => setTimeout(resolve, 5)); + expect(await (await f.call("complete", {jobId:b.jobId})).json()).toMatchObject({error:"account_busy"}); + release(); + expect(await (await first).json()).toMatchObject({activation:"ready"}); + expect(await (await f.call("complete", {jobId:b.jobId})).json()).toMatchObject({error:"account_duplicate"}); + expect(Object.values(f.ctx.config.providers).filter(p => p.adapter === "zcode")).toHaveLength(1); +}); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index c05e881633..18fba859ba 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { desktopStatus, disconnectDesktop, loadDesktopSettings, resolveDesktopRuntime, validateDesktopWorkspace } from "../../src/adapters/zcode/desktop"; import { readZcodeModels } from "../../src/adapters/zcode/settings"; +import { parseNativeOAuthEvent, nativeOAuthCommand } from "../../src/adapters/zcode/native-oauth"; import { resolveDesktopNode } from "../../src/adapters/zcode/desktop-node"; const { normalizeDesktopConfig, desktopModelCatalog } = createRequire(import.meta.url)("../../src/adapters/zcode/desktop-bootstrap.cjs"); @@ -103,6 +104,31 @@ test("Node selection skips an incompatible nvm prefix and fails safely", () => { } }); +test("native OAuth emits only bounded public events, never tokens or vendor errors", () => { + expect(parseNativeOAuthEvent({ type: "authenticated", subjectHash: "a".repeat(64), accessToken: "fixture-private-token" })) + .toEqual({ type: "authenticated", subjectHash: "a".repeat(64) }); + expect(parseNativeOAuthEvent({ type: "error", code: "private-profile-detail", stack: "secret" })) + .toEqual({ type: "error", code: "native_oauth_failed" }); + expect(parseNativeOAuthEvent({ type: "authorization", url: "https://chat.z.ai/api/oauth/authorize?state=fixture" })) + .toEqual({ type: "authorization", url: "https://chat.z.ai/api/oauth/authorize?state=fixture" }); + const credentialUrl = new URL("https://chat.z.ai/"); credentialUrl.username = "fixture"; + for (const url of ["http://chat.z.ai/", "https://chat.z.ai.evil.invalid/", credentialUrl.href, "not a url"]) { + expect(() => parseNativeOAuthEvent({ type: "authorization", url })).toThrow("native_oauth_failed"); + } + expect(() => parseNativeOAuthEvent({ type: "authenticated", subjectHash: "not-an-identity" })).toThrow("native_oauth_failed"); + expect(() => parseNativeOAuthEvent({ type: "tokens", accessToken: "secret" })).toThrow("native_oauth_failed"); +}); + +test("native OAuth sandbox refuses the live HOME and symlink profiles", () => { + if (process.platform !== "linux") return; + expect(() => nativeOAuthCommand("/not/used", homedir(), "login")).toThrow("profile_invalid"); + const privateHome = join(root, "private"); + mkdirSync(privateHome, { mode: 0o700 }); + symlinkSync(privateHome, join(root, "profile-link")); + expect(() => nativeOAuthCommand("/not/used", join(root, "profile-link"), "login")).toThrow("profile_invalid"); +}); + + test("sandbox preflight executes a child and hides uid-map diagnostics", async () => { if (process.platform !== "linux") return; const { verifyDesktopSandbox } = await import("../../src/adapters/zcode/desktop-sandbox"); @@ -182,3 +208,28 @@ test("missing or denied optional sandbox never falls back, while default host mo if (path === undefined) delete process.env.PATH; else process.env.PATH = path; } }); + +test("saved account routing and settings are explicit and never fall back to Desktop", async () => { + const { allocateAccount, accountRoot, accountProfile } = await import("../../src/adapters/zcode/accounts"); + const { desktopRoutingModelIds } = await import("../../src/adapters/zcode/desktop"); + const { loadZcodeSettings } = await import("../../src/adapters/zcode/settings"); + const { knownModelIdsForProvider } = await import("../../src/router"); + const a = allocateAccount("Personal"), b = allocateAccount("Work"); + for (const [account, modelId] of [[a, "personal-model"], [b, "work-model"]] as const) { + writeFileSync(join(accountRoot(account.id), "connection.json"), JSON.stringify({ + version: 1, connected: true, generation: crypto.randomUUID(), runtime: "/not-launched", workspace: root, + models: [{ id: "builtin:zai/" + modelId, providerId: "builtin:zai", modelId, label: modelId }], + }), { mode: 0o600 }); + } + expect(desktopRoutingModelIds(a.id)).toEqual(["builtin:zai/personal-model"]); + expect(desktopRoutingModelIds(b.id)).toEqual(["builtin:zai/work-model"]); + expect(knownModelIdsForProvider("personal", { adapter: "zcode", baseUrl: "https://zcode.z.ai", zcodeAccountId: a.id })) + .not.toContain("builtin:zai/work-model"); + expect(accountProfile(a.id)).not.toBe(accountProfile(b.id)); + expect(() => loadZcodeSettings(process.env, "../escape")).toThrow(); + expect(() => loadZcodeSettings(process.env, null as never)).toThrow(); + expect(() => loadZcodeSettings(process.env, crypto.randomUUID())).toThrow(); + await disconnectDesktop(a.id); + expect(desktopRoutingModelIds(a.id)).toEqual([]); + expect(desktopRoutingModelIds(b.id)).toEqual(["builtin:zai/work-model"]); +}); diff --git a/tests/providers/zcode-quota.test.ts b/tests/providers/zcode-quota.test.ts index 08f07be4ce..af831c3182 100644 --- a/tests/providers/zcode-quota.test.ts +++ b/tests/providers/zcode-quota.test.ts @@ -38,7 +38,6 @@ describe("ZCode native subscription quota", () => { const first = readZcodeQuota(provider, deps); const second = readZcodeQuota(provider, deps); expect(calls).toBe(1); identity = "account-b"; - expect(await readZcodeQuota(provider, deps)).toBeNull(); finish(parseZcodeQuota(snapshot([window()]), now)); expect(await first).toBeNull(); expect(await second).toBeNull(); }); @@ -53,3 +52,17 @@ describe("ZCode native subscription quota", () => { expect(getCachedProviderQuota("zcode", now)).toBeNull(); }); }); + +test("concurrent saved accounts receive only their own quota snapshots", async () => { + const a = { ...provider, zcodeAccountId: "account-a" }, b = { ...provider, zcodeAccountId: "account-b" }; + let calls = 0; + const deps = { context: (p: { zcodeAccountId?: string }) => context(p.zcodeAccountId!), probe: async (c: QuotaContext) => { + calls++; await new Promise(r => setTimeout(r, 5)); + return { updatedAt: now, fiveHourPercent: c.identity === "account-a" ? 10 : 90 }; + } }; + const [first, repeated, second] = await Promise.all([readZcodeQuota(a, deps), readZcodeQuota(a, deps), readZcodeQuota(b, deps)]); + expect(calls).toBe(2); + expect(first).toEqual(repeated); + expect(first).toMatchObject({ identity: "account-a", quota: { fiveHourPercent: 10 } }); + expect(second).toMatchObject({ identity: "account-b", quota: { fiveHourPercent: 90 } }); +}); From d7544dbdb0676a7625ae680f5dad89636386037d Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 16:38:22 -0300 Subject: [PATCH 10/43] fix(zcode): describe image inputs with the configured vision sidecar --- .../src/content/docs/guides/zcode-agent.md | 19 ++++++-- src/adapters/base.ts | 2 + src/adapters/zcode/adapter.ts | 1 + src/server/responses/core.ts | 11 ++--- src/vision/eligibility.ts | 5 ++- structure/adapters/registry.md | 12 +++++ structure/catalog.md | 4 ++ structure/clients/claude-desktop.md | 4 ++ structure/data-planes/images.md | 4 ++ structure/data-planes/inbound-compat.md | 4 ++ structure/gui-and-management-api.md | 4 ++ structure/ops/docs-and-release.md | 4 ++ structure/ops/service-and-sidecars.md | 4 ++ structure/providers/chat-compat.md | 4 ++ structure/providers/cursor.md | 4 ++ structure/providers/xai-grok.md | 4 ++ structure/runtime.md | 4 ++ structure/subagents.md | 4 ++ structure/transports/inventory.md | 4 ++ structure/transports/responses.md | 4 ++ structure/transports/streaming-health.md | 4 ++ tests/providers/zcode-adapter.test.ts | 1 + tests/providers/zcode-desktop.test.ts | 8 ++++ tests/providers/zcode-routing.test.ts | 44 ++++++++++++++++--- tests/vision/vision-eligibility.test.ts | 5 +++ .../vision/vision-text-only-predicate.test.ts | 6 +++ 26 files changed, 157 insertions(+), 17 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 785fcdeeb1..88afcd7dfa 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -11,7 +11,7 @@ exports OpenCodex models into ZCode Desktop. Enabling one does not enable the ot ZCode executes its own file and command tools. OpenCodex streams text, reasoning and informational tool progress; it never sends those native actions back to the calling client -as executable function calls. Client tool catalogs, tool-result input, images and explicit +as executable function calls. Client tool catalogs, tool-result input and explicit `tool_choice` constraints are not supported by this first version. This is an optional local transport, not a hosted API or a subscription-token bridge. @@ -19,9 +19,20 @@ Only the official ZCode runtime communicates with Z.AI. Protocol compatibility w with ZCode CLI **0.16.5**, shipped in ZCode Desktop **3.10.2** on Linux. The app-server protocol is private and may change; unsupported framing fails closed. -OpenCodex's vision, search, image and video sidecars are not used for this provider: they must -not replace an unsupported native operation with a direct API call. There is no direct-API -fallback. This describes the technical path, not a guarantee of promotion eligibility, billing +Attached images are described by the **vision sidecar configured in OpenCodex** before the +main request reaches ZCode. This helper can consume quota on its own configured provider; +the main GLM request still runs through official ZCode, never a direct Z.AI API fallback. +Search, image-generation and video-generation sidecars remain disabled for this native agent. + +The ZCode bridge itself accepts text, including for GLM-5.3-Flash: selecting Flash does not +make this transport image-capable. Neither model is offered as its own vision describer. +Configure an image-capable helper under **Dashboard → Vision sidecar**; existing backend/model +selection is respected. If vision is disabled, unavailable, capped or fails, images are replaced +with explicit unavailable-description markers rather than being silently passed to GLM or +presented as successfully analyzed. Unsupported video input and client tool results are not +converted into fabricated descriptions. + + This describes the technical path, not a guarantee of promotion eligibility, billing or subscription terms; those remain the vendor's policy. ## Connect from the dashboard (Linux) diff --git a/src/adapters/base.ts b/src/adapters/base.ts index ae7ebf77f2..2327f17b17 100644 --- a/src/adapters/base.ts +++ b/src/adapters/base.ts @@ -26,6 +26,8 @@ export interface ProviderAdapter { replaySafe?: boolean; /** False when all inference must stay inside the provider's own agent runtime. */ allowExternalSidecars?: boolean; + /** Explicit exception for input-image description only; native tools still belong to the agent. */ + allowVisionSidecar?: boolean; name: string; /** diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index b89789d625..23e7d0dea5 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -65,6 +65,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt name: "zcode", replaySafe: false, allowExternalSidecars: false, + allowVisionSidecar: true, buildRequest() { throw new Error("ZCode uses its local app-server transport, not HTTP."); }, async *parseStream() { yield { type: "error", message: "ZCode requires runTurn.", retryable: false }; }, async runTurn(parsed, incoming, emit) { diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 1df691b9c5..15be1db832 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -4538,10 +4538,11 @@ async function handleResponsesInner( } let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined; - // Opt-in native agents own the entire execution path. Do not turn an unsupported attachment - // into a separate direct-API inference (or resolve helper credentials) before their adapter. + // Native agents retain tool execution. An explicit vision-only capability allows the + // configured describer to turn input images into text, without enabling other helper tools. const nativeAgentOwnsExecution = adapter.allowExternalSidecars === false; - const needsOpenAiVision = !nativeAgentOwnsExecution && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed); + const allowsVisionSidecar = !nativeAgentOwnsExecution || adapter.allowVisionSidecar === true; + const needsOpenAiVision = allowsVisionSidecar && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed); const needsOpenAiSearch = !nativeAgentOwnsExecution && shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough); if (needsOpenAiVision || needsOpenAiSearch) { try { @@ -4591,7 +4592,7 @@ async function handleResponsesInner( // surface (whose bridge rebuilds headers) or as the raw header for native // Responses callers. Marked + text-only routed model → strip, depth cap 1. const visionDescribeTerminal = options.visionDescribeTerminal === true; - const visionPlan = visionDescribeTerminal || nativeAgentOwnsExecution + const visionPlan = visionDescribeTerminal || !allowsVisionSidecar ? undefined : planVisionSidecar(config, route.provider, route.modelId, parsed, openAiSidecar, { admission: options.admission, codexAuthPolicy: options.codexAuthPolicy, @@ -4606,7 +4607,7 @@ async function handleResponsesInner( recordSidecarOutcome, translatorBudget, ); - } else if (!nativeAgentOwnsExecution && isModelTextOnly(route.provider, route.modelId)) { + } else if (allowsVisionSidecar && isModelTextOnly(route.provider, route.modelId)) { // Sidecar-covered model but NO plan (no forward provider / missing forwarded auth / sidecar // disabled): fail closed — never forward raw images to a text-only upstream. stripImagesInPlace(parsed, translatorBudget); diff --git a/src/vision/eligibility.ts b/src/vision/eligibility.ts index 09a83d44db..fb444a73cd 100644 --- a/src/vision/eligibility.ts +++ b/src/vision/eligibility.ts @@ -77,9 +77,12 @@ type EnrichedProviderCache = Map; * not a text-only model and must not be widened to image through the vision sidecar. */ export function isModelVisionSidecarConsumer( - provider: Pick, + provider: Pick, modelId: string, ): boolean { + // This transport accepts text only, regardless of a vendor model's native capability. + // Keep catalog, input adaptation and describer eligibility aligned for every account/alias. + if (provider.adapter === "zcode") return true; if (modelInList(provider.noVisionModels, modelId)) return true; const modalities = modelRecordValue(provider.modelInputModalities, modelId); return Array.isArray(modalities) && modalities.includes("text") && !modalities.includes("image"); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 4e31416441..a156919d6b 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -73,3 +73,15 @@ closed, never to the legacy Desktop profile or another account. Legacy unbound ` retains its previous profile. Account providers do not participate in an implicit pool. Native tools still run on the host by default; optional OS sandboxing is independent of profile separation and does not turn the latter into a security boundary. + +## ZCode vision input adaptation + +`src/adapters/base.ts` exposes a vision-only exception to the native-agent sidecar gate. +ZCode sets `allowVisionSidecar: true` while retaining `allowExternalSidecars: false`. +`src/server/responses/core.ts` resolves the configured vision helper and rewrites images before +calling the official agent; search/image/video generation remain native-agent-owned. +`src/vision/eligibility.ts` classifies every ZCode transport model as a sidecar consumer, +including renamed/account-bound providers and Flash. The shared catalog predicate advertises +sidecar-backed image input and excludes these models from describer selection. Disabled or +unavailable vision uses explicit omission markers; recursion protection and quota/cancellation +bounds remain in the shared vision path. This does not implement native image support in ZCode. diff --git a/structure/catalog.md b/structure/catalog.md index a4d975f9d7..185f3fc1f6 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -265,3 +265,7 @@ provider wire mapping; unpinned native requests retain their existing pass-throu ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 4b5b0a0a51..0cc2d7fc9b 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -79,3 +79,7 @@ testable on any host: stubbing `process.platform` does not propagate to `os.plat ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 6e22e84bf6..02c956a214 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -70,3 +70,7 @@ conflicts with `modelSupportsReasoningSummaries: false` for the same model. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 286e654eb5..547ed90722 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -90,3 +90,7 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index e97d40214d..54695e4c52 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -517,3 +517,7 @@ part of this flow. Reconnect retains custom provider settings and rejects a diff Rename changes only generated labels; removal refuses busy/referenced accounts. In-progress OAuth jobs expire and do not survive restart; saved account profiles and bindings do. See [the ZCode account runtime contract](adapters/registry.md#zcode-saved-accounts). + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 9e1ec377eb..c7ccd0ed05 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -304,3 +304,7 @@ The Remote Hub guide and affected CLI, server-config, management-API, and dashbo ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 9578eea678..e9ca754c69 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -136,3 +136,7 @@ client responsibilities. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 4173e770d6..e1db6471d5 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -272,3 +272,7 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index de6666d663..4a5262683c 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -90,3 +90,7 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index e7d52aca7b..3f2c70241d 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -45,3 +45,7 @@ malformed, gapped, oversized, contradictory, failed, or incomplete streams stay ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/runtime.md b/structure/runtime.md index 09e7082941..a4201014d9 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -193,3 +193,7 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/subagents.md b/structure/subagents.md index 5a248f4b2a..11e5169c73 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -199,3 +199,7 @@ Native Codex advertisements still follow display priority; private guidance rank ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index bf231f9b26..a60b9dc03f 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -65,3 +65,7 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 37c73bc93f..3c40bf017f 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -474,3 +474,7 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 8f3ab8e532..0fe52d71fe 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -193,3 +193,7 @@ frame rather than always emitting `response.completed`. If the response status i ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +For ZCode, input-image description is the explicit exception to native-agent helper exclusion; +see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured +vision provider may consume its own quota; main inference remains in official ZCode. diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 03e9c133af..a430696bdc 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -86,6 +86,7 @@ describe("ZCode local agent", () => { expect(adapter.fetchResponse).toBeUndefined(); expect(adapter.replaySafe).toBe(false); expect(adapter.allowExternalSidecars).toBe(false); + expect(adapter.allowVisionSidecar).toBe(true); expect(() => adapter.buildRequest(request(), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() })) .toThrow("not HTTP"); expect(modelCapabilityFields({ supportsToolUse: false }).capabilities.supports_tool_use).toBe(false); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 18fba859ba..1187c6f2ec 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -233,3 +233,11 @@ test("saved account routing and settings are explicit and never fall back to Des expect(desktopRoutingModelIds(a.id)).toEqual([]); expect(desktopRoutingModelIds(b.id)).toEqual(["builtin:zai/work-model"]); }); + +test("ZCode catalog advertises sidecar-backed attachments for legacy and account providers", async () => { + const {applyProviderConfigHints} = await import("../../src/codex/catalog/provider-fetch"); + for(const name of ["zcode","work-account"]) { + const result = applyProviderConfigHints(name,{adapter:"zcode",authMode:"local",baseUrl:"https://zcode.z.ai"},{provider:name,id:"builtin:zai-coding-plan/GLM-5.3-Flash",inputModalities:["text"]}); + expect(result.inputModalities).toContain("image"); + } +}); diff --git a/tests/providers/zcode-routing.test.ts b/tests/providers/zcode-routing.test.ts index 001609b8a3..1333f3ea7f 100644 --- a/tests/providers/zcode-routing.test.ts +++ b/tests/providers/zcode-routing.test.ts @@ -1,28 +1,33 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; -import type { AdapterEvent, OcxConfig } from "../../src/types"; +import type { AdapterEvent, OcxConfig, OcxParsedRequest } from "../../src/types"; const vision = await import("../../src/vision"); const search = await import("../../src/web-search"); const images = await import("../../src/images"); const resolver = await import("../../src/server/adapter-resolve"); const forbidden = mock(() => { throw new Error("A native ZCode turn must not plan direct-API sidecars."); }); -mock.module("../../src/vision", () => ({ ...vision, - shouldResolveOpenAiVisionSidecar: forbidden, planVisionSidecar: forbidden, stripImagesInPlace: forbidden })); +const describeImages = mock(async (parsed: OcxParsedRequest, plan: {backend: string; routedModel?: string}) => { + expect(plan).toMatchObject({ backend: "routed", routedModel: "eyes/vision-model" }); + vision.stripImagesInPlace(parsed); + parsed.context.messages.push({ role: "user", content: "Vision caption: a red square", timestamp: 0 }); +}); +mock.module("../../src/vision", () => ({ ...vision, describeImagesInPlace: describeImages })); mock.module("../../src/web-search", () => ({ ...search, shouldResolveOpenAiWebSearchSidecar: forbidden, planWebSearch: forbidden })); mock.module("../../src/images", () => ({ ...images, planImageBridge: forbidden, planVideoBridge: forbidden })); let sends = 0; +let received: OcxParsedRequest | undefined; let terminal: AdapterEvent = { type: "done" }; mock.module("../../src/server/adapter-resolve", () => ({ ...resolver, - resolveAdapter: () => ({ name: "zcode", replaySafe: false, allowExternalSidecars: false, + resolveAdapter: () => ({ name: "zcode", replaySafe: false, allowExternalSidecars: false, allowVisionSidecar: true, buildRequest: forbidden, async *parseStream() { throw new Error("Unexpected HTTP transport"); }, - async runTurn(_parsed: unknown, _incoming: unknown, emit: (event: AdapterEvent) => void) { - sends++; emit(terminal); + async runTurn(_parsed: OcxParsedRequest, _incoming: unknown, emit: (event: AdapterEvent) => void) { + received = _parsed; sends++; emit(terminal); }, }), })); const { handleResponses } = await import("../../src/server/responses"); -beforeEach(() => { sends = 0; forbidden.mockClear(); terminal = { type: "done" }; }); +beforeEach(() => { received = undefined; describeImages.mockClear(); sends = 0; forbidden.mockClear(); terminal = { type: "done" }; }); function config(): OcxConfig { return { port: 0, emptyCompletionRetry: true, defaultProvider: "zcode", providers: { @@ -56,3 +61,28 @@ describe("ZCode routing keeps inference inside the official agent transport", () expect(forbidden).not.toHaveBeenCalled(); }); }); + +for (const model of ["builtin:zai-coding-plan/GLM-5.3", "builtin:zai-coding-plan/GLM-5.3-Flash"]) { + for (const enabled of [true, false]) { + test(`ZCode image adaptation uses configured vision only: ${model}, enabled=${enabled}`, async () => { + const c = config(); + c.providers.zcode!.noVisionModels = []; + c.providers.eyes = { adapter: "openai-chat", baseUrl: "https://example.invalid" }; + c.visionSidecar = { enabled, backend: "routed", model: "eyes/vision-model" }; + const response = await handleResponses(new Request("http://localhost/v1/responses", { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: "zcode/" + model, stream: false, input: [{ role: "user", content: [ + {type:"input_text",text:"Describe this"}, {type:"input_image",image_url:"data:image/png;base64,aGVsbG8="}, + ] }], tools: [{type:"web_search"}, {type:"image_generation"}] }), + }), c, { model:"", provider:"" }); + await response.text(); + expect(response.status).toBe(200); + expect(sends).toBe(1); + expect(describeImages).toHaveBeenCalledTimes(enabled ? 1 : 0); + expect(JSON.stringify(received?.context)).not.toContain('"type":"image"'); + expect(JSON.stringify(received?._rawBody)).not.toContain('"type":"input_image"'); + if (enabled) expect(JSON.stringify(received?.context)).toContain("Vision caption: a red square"); + expect(forbidden).not.toHaveBeenCalled(); + }); + } +} diff --git a/tests/vision/vision-eligibility.test.ts b/tests/vision/vision-eligibility.test.ts index 2344fc4d69..529cc66e6d 100644 --- a/tests/vision/vision-eligibility.test.ts +++ b/tests/vision/vision-eligibility.test.ts @@ -293,3 +293,8 @@ describe("vision eligibility core", () => { expect(withRouted.some((o) => o.value === "cursor/cursor-vision-capable" && o.backend === "routed")).toBe(true); }); }); + +test("account-bound ZCode models cannot recursively describe their own images", () => { + const config = configWithProviders({personal:{adapter:"zcode",baseUrl:"https://zcode.z.ai",authMode:"local",zcodeAccountId:"account-test"}}); + expect(modelAcceptsImageInput(config,{provider:"personal",id:"builtin:zai-coding-plan/GLM-5.3-Flash",inputModalities:["text","image"]})).toBe(false); +}); diff --git a/tests/vision/vision-text-only-predicate.test.ts b/tests/vision/vision-text-only-predicate.test.ts index 6a78872a04..15b73872fb 100644 --- a/tests/vision/vision-text-only-predicate.test.ts +++ b/tests/vision/vision-text-only-predicate.test.ts @@ -42,3 +42,9 @@ describe("isModelTextOnly (#1024)", () => { expect(isModelTextOnly(provider({ modelInputModalities: { "base-model": ["text"] } }), "base-model:extended")).toBe(true); }); }); + +test("ZCode transport requires vision adaptation for every model even with native image metadata", () => { + for (const id of ["builtin:zai-coding-plan/GLM-5.3", "builtin:zai-coding-plan/GLM-5.3-Flash", "future-model"]) { + expect(isModelTextOnly(provider({adapter:"zcode",modelInputModalities:{[id]:["text","image"]}}),id)).toBe(true); + } +}); From b880dfe87ceb424e8a00b7f6b6826b3966b23880 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 16:53:28 -0300 Subject: [PATCH 11/43] fix(zcode): address automated review findings --- .../src/content/docs/guides/zcode-agent.md | 12 ++++-- gui/src/components/ZcodeDesktopPane.tsx | 5 ++- gui/tests/zcode-desktop-pane.test.tsx | 13 +++++++ src/adapters/zcode/adapter.ts | 2 +- src/adapters/zcode/desktop-bootstrap.cjs | 2 +- src/adapters/zcode/desktop.ts | 14 +++++-- src/adapters/zcode/quota.ts | 37 ++++++++++++------- src/adapters/zcode/settings.ts | 2 +- src/providers/quota.ts | 3 +- src/server/index.ts | 7 +++- .../management/zcode-desktop-activation.ts | 1 - src/server/management/zcode-desktop-routes.ts | 6 +-- src/server/models-capabilities.ts | 11 ++++++ structure/adapters/registry.md | 5 +++ structure/catalog.md | 5 +++ structure/clients/claude-desktop.md | 5 +++ structure/data-planes/images.md | 5 +++ structure/data-planes/inbound-compat.md | 5 +++ structure/design-methodology.md | 5 +++ structure/gui-and-management-api.md | 5 +++ structure/ops/service-and-sidecars.md | 5 +++ structure/overview.md | 5 +++ structure/providers/chat-compat.md | 5 +++ structure/providers/cursor.md | 5 +++ structure/providers/xai-grok.md | 5 +++ structure/runtime.md | 5 +++ structure/subagents.md | 5 +++ structure/transports/inventory.md | 5 +++ structure/transports/responses.md | 5 +++ structure/transports/streaming-health.md | 5 +++ tests/providers/zcode-adapter.test.ts | 37 ++++++++++++++++++- tests/providers/zcode-desktop-routes.test.ts | 34 ++++++++++++++++- tests/providers/zcode-desktop.test.ts | 23 ++++++++++++ tests/providers/zcode-quota.test.ts | 26 +++++++++---- 34 files changed, 278 insertions(+), 42 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 88afcd7dfa..9c6e36a052 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -88,8 +88,11 @@ independent official ZCode profiles. Repeat for each account (up to 20). Accounts do not form a pool: there is no automatic rotation or fallback to another ZCode account or the Desktop profile. Combos may target -explicit account providers if you configure those routes yourself. Quota bars remain -specific to each provider/account. +explicit account providers if you configure those routes yourself. A combo containing any +ZCode target is advertised without caller-managed tool support, even when its other targets +support tools. Quota bars remain specific to each provider/account; a temporary quota-probe +failure preserves a recent valid reading, while a valid entitlement with no supported windows +clears it. **Sign in again** replaces only the matching account's sign-in, retaining its provider ID and custom settings. A different identity is rejected; use **Add account** for it instead. @@ -153,8 +156,9 @@ an advanced operator-supplied launcher. Managed consent and installation/workspace paths are stored privately under `$OPENCODEX_HOME/zcode-desktop/`, separately from provider configuration. Data-plane requests -cannot choose an executable or override these paths. Connecting, disconnecting and the optional -quota-spending test require the dashboard's authenticated GUI session, not a raw API/admin token. +cannot choose an executable or override these paths. Status, folder browsing, connecting, +disconnecting and the optional quota-spending test require the dashboard's authenticated GUI +session, not a raw API/admin token. ## Advanced: operator-supplied isolated launcher diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index 94879fe3ac..976cb532fc 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -45,7 +45,10 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: }); const result = await response.json(); if (!response.ok) { setError(result.error ?? "runtime_failed"); return; } - if (action === "test") setTested(result.ok === true); + if (action === "test") { + setTested(result.ok === true); + if (result.ok !== true) setError(result.error ?? "inference_failed"); + } else { applyStatus(result as Status); setConsent(false); if ((action === "connect" || action === "activate") && result.activation === "ready" && result.providerName) onConnected?.(result.providerName); diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index a8c7781ff4..022c2c0414 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -94,6 +94,19 @@ test("one-request test is a separate explicit quota-spending action", async () = expect(host.textContent).toContain("ZCode answered successfully."); }); +test("a successful HTTP response with a failed inference shows the actionable error", async () => { + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL) => { + const path = new URL(String(input), "http://localhost").pathname; + if (path.endsWith("/test")) return Response.json({ ok: false, error: "inference_failed" }); + return Response.json({ connected: true, activation: "ready", providerName: "zcode", runtimes: ["/installed/ZCode"], + runtime: "/installed/ZCode", workspace: "/project", models: [{ id: "builtin:zai/model", label: "Model" }] }); + } }); + await mountPane(); + await click(button("Test with one request")); + expect(host.querySelector('[role="alert"]')?.textContent).toContain("did not complete the test"); + expect(host.textContent).not.toContain("ZCode answered successfully."); +}); + test("incompatible Node preflight shows safe actionable guidance without connecting", async () => { Object.defineProperty(globalThis, "fetch", { configurable: true, value: async () => Response.json({ connected: false, issue: "node_incompatible", runtimes: [], runtime: "", workspace: "/project", models: [], diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 23e7d0dea5..f8a6401b44 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -124,8 +124,8 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt let textSeen = false; active.onEvent = message => { const params = record(message.params); - if (params.sessionId && params.sessionId !== sessionId) return; if (message.method !== "session/event") return; + if (typeof params.sessionId !== "string" || params.sessionId !== sessionId) return; const payload = record(params.payload); if (params.type === "model.streaming") { if ((payload.kind === "text_delta" || payload.kind === "text_start") && typeof payload.delta === "string") { diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs index bec5528704..4772d147f6 100644 --- a/src/adapters/zcode/desktop-bootstrap.cjs +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -32,7 +32,7 @@ function normalizeDesktopConfig(input) { function desktopModelCatalog(config) { return Object.entries(config.provider).flatMap(([providerId, p]) => - Object.entries(p.models).slice(0, 200).filter(([id, m]) => id && !/[\x00-\x20]/.test(id) && m && typeof m === "object").map(([modelId, m]) => ({ + Object.entries(p.models).filter(([id, m]) => id && !/[\x00-\x20]/.test(id) && m && typeof m === "object").slice(0, 200).map(([modelId, m]) => ({ id: `${providerId}/${modelId}`, providerId, modelId, label: `${p.name} / ${typeof m.name === "string" ? m.name : modelId}`.slice(0, 240), ...(Number.isFinite(m.limit?.context) && m.limit.context > 0 ? { contextWindow: m.limit.context } : {}), diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index 1f73b76d95..9222c1544b 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -22,6 +22,9 @@ const fail = (code: string): never => { throw new DesktopSetupError(code); }; const root = (accountId?: string) => accountId ? accountRoot(accountId) : join(getConfigDir(), "zcode-desktop"); const connectionPath = (accountId?: string) => join(root(accountId), "connection.json"); export const defaultDesktopWorkspace = (accountId?: string) => join(root(accountId), "workspace"); +const canonicalExistingOrResolved = (path: string): string => { + try { return realpathSync(path); } catch { return resolve(path); } +}; function readConnection(accountId?: string): Connection | undefined { if (accountId) readAccount(accountId); @@ -95,15 +98,20 @@ export function detectDesktopRuntimes(): string[] { export function validateDesktopWorkspace(path: string): string { if (!path || !isAbsolute(path) || path.includes("\0") || path.length > 4096) return fail("workspace_invalid"); - if (resolve(path) === resolve(defaultDesktopWorkspace())) mkdirSync(path, { recursive: true, mode: 0o700 }); + const defaultPath = defaultDesktopWorkspace(); + if (resolve(path) === resolve(defaultPath)) mkdirSync(path, { recursive: true, mode: 0o700 }); let workspace: string; try { workspace = realpathSync(path); if (!statSync(workspace).isDirectory()) return fail("workspace_invalid"); } catch { return fail("workspace_invalid"); } if (!desktopSandboxEnabled()) return workspace; const home = realpathSync(homedir()); if (workspace === home || home.startsWith(workspace.endsWith(sep) ? workspace : workspace + sep)) return fail("workspace_invalid"); - if (workspace !== resolve(defaultDesktopWorkspace())) { - for (const protectedPath of [join(home, ".zcode"), join(home, ".codex"), join(home, ".ssh"), join(home, ".config"), getConfigDir(), root()]) { + const configRoot = canonicalExistingOrResolved(root()); + const managedDefault = canonicalExistingOrResolved(defaultPath); + const isManagedDefault = workspace === managedDefault && managedDefault.startsWith(configRoot + sep); + if (!isManagedDefault) { + for (const rawPath of [join(home, ".zcode"), join(home, ".codex"), join(home, ".ssh"), join(home, ".config"), getConfigDir(), root()]) { + const protectedPath = canonicalExistingOrResolved(rawPath); if (workspace === protectedPath || workspace.startsWith(protectedPath + sep) || protectedPath.startsWith(workspace + sep)) return fail("workspace_invalid"); } } diff --git a/src/adapters/zcode/quota.ts b/src/adapters/zcode/quota.ts index 32388069a7..fa0f6e2ec1 100644 --- a/src/adapters/zcode/quota.ts +++ b/src/adapters/zcode/quota.ts @@ -13,17 +13,20 @@ import { desktopProfile, desktopSandboxEnabled, desktopStatus, resolveDesktopRun import { loadZcodeSettings } from "./settings"; export interface QuotaContext { identity: string; runtimeRoot: string; config: string; credentials?: string; sourceProvider: string; managed: boolean } +export type ZcodeQuotaSnapshot = { kind: "quota"; quota: ProviderQuota } | { kind: "empty" }; const finite = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v); -/** Translate consumption into the existing shared quota contract. Missing is never zero. */ -export function parseZcodeQuota(value: unknown, now = Date.now()): ProviderQuota | null { +/** Distinguish a valid empty entitlement from an unavailable or malformed probe. */ +export function parseZcodeQuotaSnapshot(value: unknown, now = Date.now()): ZcodeQuotaSnapshot | null { if (!value || typeof value !== "object") return null; const v = value as { generatedAt?: unknown; limits?: unknown }; - if (!finite(v.generatedAt) || v.generatedAt > now + 60_000 || now - v.generatedAt > 30 * 60_000 || !Array.isArray(v.limits)) return null; + if (!finite(v.generatedAt) || v.generatedAt > now + 60_000 || now - v.generatedAt > 30 * 60_000 + || !Array.isArray(v.limits) || v.limits.length > 256) return null; const result: ProviderQuota = { updatedAt: v.generatedAt }; const seen = new Set(); - for (const row of v.limits.slice(0, 16)) { - if (!row || typeof row !== "object" || !["CREDIT_LIMIT", "TOKENS_LIMIT"].includes(row.type)) continue; + for (const row of v.limits) { + if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.type !== "string") return null; + if (!["CREDIT_LIMIT", "TOKENS_LIMIT"].includes(row.type)) continue; const key = row.unit === 3 && row.number === 5 ? "fiveHour" : row.unit === 6 && row.number === 1 ? "weekly" : undefined; if (!key) continue; if (seen.has(key)) return null; // ambiguous windows are not interchangeable @@ -34,13 +37,21 @@ export function parseZcodeQuota(value: unknown, now = Date.now()): ProviderQuota } else if (row.usage === undefined && row.remaining === undefined && finite(row.percentage) && row.percentage >= 0 && row.percentage <= 100) { percent = row.percentage; } - if (percent === undefined) continue; + if (percent === undefined) return null; result[`${key}Percent`] = percent; if (finite(row.nextResetTime) && row.nextResetTime > 0 && row.nextResetTime <= 8.64e15) { result[`${key}ResetAt`] = row.nextResetTime > 1e11 ? row.nextResetTime : row.nextResetTime * 1000; } } - return result.fiveHourPercent !== undefined || result.weeklyPercent !== undefined ? result : null; + return result.fiveHourPercent !== undefined || result.weeklyPercent !== undefined + ? { kind: "quota", quota: result } + : { kind: "empty" }; +} + +/** Translate consumption into the existing shared quota contract. Missing is never zero. */ +export function parseZcodeQuota(value: unknown, now = Date.now()): ProviderQuota | null { + const result = parseZcodeQuotaSnapshot(value, now); + return result?.kind === "quota" ? result.quota : null; } function context(provider: OcxProviderConfig): QuotaContext { @@ -87,7 +98,7 @@ function command(c: QuotaContext): string[] { } /** One short-lived official host, without a writable user workspace or persistent credentials. */ -async function probe(c: QuotaContext): Promise { +async function probe(c: QuotaContext): Promise { const sandbox = desktopSandboxEnabled(); const bwrap = sandbox ? Bun.which("bwrap", { PATH: process.env.PATH }) : undefined; if (sandbox && !bwrap) return null; @@ -121,14 +132,14 @@ async function probe(c: QuotaContext): Promise { child.once("close", code => { clearTimeout(timer); if (code || invalid) return resolve(null); - try { resolve(parseZcodeQuota(JSON.parse(output))); } catch { resolve(null); } + try { resolve(parseZcodeQuotaSnapshot(JSON.parse(output))); } catch { resolve(null); } }); }); } finally { if (temporaryHome) rmSync(temporaryHome, { recursive: true, force: true }); } } -const inflight = new Map>(); -export async function readZcodeQuota(provider: OcxProviderConfig, deps: { context?: typeof context; probe?: typeof probe } = {}): Promise<{ identity: string; quota: ProviderQuota } | null> { +const inflight = new Map>(); +export async function readZcodeQuota(provider: OcxProviderConfig, deps: { context?: typeof context; probe?: typeof probe } = {}): Promise<({ identity: string } & ZcodeQuotaSnapshot) | null> { let c: QuotaContext; try { if (provider.zcodeAccountId && !deps.context) await refreshAccount(provider.zcodeAccountId); c = (deps.context ?? context)(provider); } catch { return null; } // Coalesce per profile, without letting the first account starve every other account. @@ -139,7 +150,7 @@ export async function readZcodeQuota(provider: OcxProviderConfig, deps: { contex inflight.set(c.identity, pending); void pending.finally(() => { if (inflight.get(c.identity) === pending) inflight.delete(c.identity); }); } - const quota = await pending; - try { return quota && (deps.context ?? context)(provider).identity === c.identity ? { identity: c.identity, quota } : null; } + const snapshot = await pending; + try { return snapshot && (deps.context ?? context)(provider).identity === c.identity ? { identity: c.identity, ...snapshot } : null; } catch { return null; } } diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index a6484e7c6a..f473d047d0 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -43,7 +43,7 @@ export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env, accountI throw new Error("ZCode requires explicit absolute OCX_ZCODE_HOME and OCX_ZCODE_WORKSPACE paths."); } const realHome = realpathSync(home); - if (realHome === realpathSync(env.HOME ?? home)) throw new Error("ZCode must use a separate home, not the proxy home."); + if (env.HOME && realHome === realpathSync(env.HOME)) throw new Error("ZCode must use a separate home, not the proxy home."); if (!statSync(realHome).isDirectory()) throw new Error("ZCode home is not a directory."); const settingsPath = join(realHome, ".zcode", "cli", "config.json"); return { diff --git a/src/providers/quota.ts b/src/providers/quota.ts index cb96f19a7d..d2fb1db8ec 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -2956,7 +2956,8 @@ async function maybeFetchProviderQuota( try { if (provider.adapter === "zcode") { const result = await readZcodeQuota(provider); - if (!result) return AUTHORITATIVE_EMPTY_QUOTA; + if (!result) return null; + if (result.kind === "empty") return AUTHORITATIVE_EMPTY_QUOTA; const report: ProviderQuotaReport = { provider: name, label: "ZCode", source: "zcode-desktop", quota: result.quota, updatedAt: result.quota.updatedAt, reverseEngineered: true }; accountReportCurrent.set(report, () => zcodeQuotaIdentity(provider) === result.identity); diff --git a/src/server/index.ts b/src/server/index.ts index c1f2f56161..21a1cabb72 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -64,6 +64,7 @@ import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-st import { runModelRenameStartupMigration } from "../providers/model-rename-startup"; import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; import { providerCodexAccountMode } from "../providers/registry"; +import { comboPublicModelId } from "../combos/identifiers"; import type { StorageCleanupPolicy } from "../types"; import { MAX_CONFIGURABLE_INBOUND_BODY_BYTES, @@ -237,7 +238,7 @@ import { } from "../lib/package-tree-integrity"; import { detectInstall } from "../update/index"; import { readyProtocolMetadata } from "../remote/protocol"; -import { modelCapabilityFields } from "./models-capabilities"; +import { catalogRowSupportsToolUse, modelCapabilityFields } from "./models-capabilities"; import { recordCursorSeen } from "../integrations/cursor-seen"; import { detectCursorInstalls } from "../integrations/cursor-detect"; import { loadCursorEffortTable } from "../integrations/cursor-effort-table"; @@ -1827,6 +1828,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server + comboPublicModelId(id, value) === publicId)?.[1] : undefined; const effective = provider ? (await import("../providers/default-aliases")).effectiveModelAliases( config, @@ -1847,7 +1850,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server (readCatalog(readCodexCat function providerName(ctx: ManagementContext, accountId?: string): string | undefined { const matches = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.adapter === "zcode" && ctx.config.providers[name]?.zcodeAccountId === accountId); - if (matches.includes("zcode")) return "zcode"; return matches.length === 1 ? matches[0] : undefined; } diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts index 934fcb892f..1941fa6ac9 100644 --- a/src/server/management/zcode-desktop-routes.ts +++ b/src/server/management/zcode-desktop-routes.ts @@ -14,15 +14,15 @@ let testing = false; export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = services): Promise { const { req, url } = ctx; if (!url.pathname.startsWith("/api/zcode-desktop")) return null; + // Runtime/workspace paths are local-machine metadata. Every Desktop endpoint requires an + // actual GUI session; an admin token may manage providers but must not inspect host paths. + if (ctx.principal !== "gui-session") return jsonResponse({ error: "dashboard_required" }, 403); if (url.pathname === "/api/zcode-desktop" && req.method === "GET") return jsonResponse(desktopActivation(ctx, deps.desktopStatus(), deps.readDesktopCatalogSlugs)); if (url.pathname === "/api/zcode-desktop/folders" && req.method === "GET") { try { return jsonResponse(desktopFolders(url.searchParams.get("path") ?? undefined)); } catch { return jsonResponse({ error: "workspace_invalid" }, 400); } } if (req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405); - // Executable/workspace selection and optional quota-spending tests require an actual GUI - // principal, not forgeable Origin headers or a data/admin token. - if (ctx.principal !== "gui-session") return jsonResponse({ error: "dashboard_required" }, 403); try { const raw = await readBoundedJsonRequestBody(req, 12_000, undefined, { signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]) }); if (!raw || typeof raw !== "object" || Array.isArray(raw)) return jsonResponse({ error: "invalid_request" }, 400); diff --git a/src/server/models-capabilities.ts b/src/server/models-capabilities.ts index ab564bce49..2043814151 100644 --- a/src/server/models-capabilities.ts +++ b/src/server/models-capabilities.ts @@ -1,4 +1,5 @@ import type { CursorEffortTable } from "../integrations/cursor-effort-table"; +import type { OcxComboConfig, OcxConfig, OcxProviderConfig } from "../types"; /** * Extended capability advertisement for the OpenAI-shape `GET /v1/models` list. @@ -142,6 +143,16 @@ export interface ModelCapabilityFields { pricing?: { overrides: Array<{ min_prompt_tokens: number }> }; } +/** Agent-owned ZCode targets cannot execute the caller's tool catalog, directly or via a combo. */ +export function catalogRowSupportsToolUse( + provider: OcxProviderConfig | undefined, + combo: OcxComboConfig | undefined, + providers: OcxConfig["providers"], +): boolean { + return provider?.adapter !== "zcode" + && combo?.targets.some(target => providers[target.provider]?.adapter === "zcode") !== true; +} + function positiveInt(value: unknown): number | undefined { if (typeof value !== "number" || !Number.isFinite(value)) return undefined; const floored = Math.floor(value); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index a156919d6b..06fea5eb65 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -85,3 +85,8 @@ including renamed/account-bound providers and Flash. The shared catalog predicat sidecar-backed image input and excludes these models from describer selection. Disabled or unavailable vision uses explicit omission markers; recursion protection and quota/cancellation bounds remain in the shared vision path. This does not implement native image support in ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/catalog.md b/structure/catalog.md index 185f3fc1f6..357494670e 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -269,3 +269,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 0cc2d7fc9b..6bd54033da 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -83,3 +83,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 02c956a214..8d46bbdaa3 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -74,3 +74,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 547ed90722..468cc537e1 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -94,3 +94,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/design-methodology.md b/structure/design-methodology.md index 084c9d4dee..3754158469 100644 --- a/structure/design-methodology.md +++ b/structure/design-methodology.md @@ -40,3 +40,8 @@ surfaces, run through all 3 stages in order. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 54695e4c52..3737bac541 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -521,3 +521,8 @@ See [the ZCode account runtime contract](adapters/registry.md#zcode-saved-accoun For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index e9ca754c69..fd12ce1f95 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -140,3 +140,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/overview.md b/structure/overview.md index 15350cfee8..58469ba6f3 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -107,3 +107,8 @@ would pass while the rule was violated. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index e1db6471d5..61ec464bc6 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -276,3 +276,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 4a5262683c..3a44ba4972 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -94,3 +94,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 3f2c70241d..1673e80b64 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -49,3 +49,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/runtime.md b/structure/runtime.md index a4201014d9..615a3e5a8c 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -197,3 +197,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/subagents.md b/structure/subagents.md index 11e5169c73..dc36e7a1d2 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -203,3 +203,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index a60b9dc03f..00aae65818 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -69,3 +69,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 3c40bf017f..d01c552e64 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -478,3 +478,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 0fe52d71fe..8ec22cc35d 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -197,3 +197,8 @@ client integration exports. Their profile, catalog and transport contract is mai For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured vision provider may consume its own quota; main inference remains in official ZCode. + +The hardened ZCode boundary accepts only exact active-session events, canonicalizes protected paths in +optional sandbox mode, distinguishes unavailable quota probes from valid empty entitlements, requires +unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability +for every combo containing a ZCode target. diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index a430696bdc..44050177aa 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -6,7 +6,7 @@ import { createZcodeAdapter } from "../../src/adapters/zcode/adapter"; import { loadZcodeSettings, readZcodeModels, type JsonObject, type ZcodeSettings } from "../../src/adapters/zcode/settings"; import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../src/types"; import { createTestTranslatorBudget } from "../helpers/translator-budget"; -import { modelCapabilityFields } from "../../src/server/models-capabilities"; +import { catalogRowSupportsToolUse, modelCapabilityFields } from "../../src/server/models-capabilities"; import { deriveProviderPresets } from "../../src/providers/derive"; const roots: string[] = []; @@ -29,11 +29,22 @@ class FakeClient { onFailure: (error: Error) => void = () => {}; calls: Array<{ method: string; params: JsonObject }> = []; closed = false; - constructor(private outcome: "ok" | "failed" | "hang" = "ok") {} + constructor(private outcome: "ok" | "failed" | "hang" | "session-noise" = "ok") {} async request(method: string, params: JsonObject): Promise { this.calls.push({ method, params }); if (method === "session/create") return { session: { sessionId: "sess_test-1" } }; if (method === "session/send") { + if (this.outcome === "session-noise") { + queueMicrotask(() => { + this.onEvent({ method: "session/event", params: { type: "turn.completed", payload: { response: "wrong session" } } }); + this.onEvent({ method: "session/event", params: { sessionId: "sess_other", type: "turn.completed", payload: { response: "wrong session" } } }); + }); + setTimeout(() => { + this.event("model.streaming", { kind: "text_delta", delta: "Hello" }); + this.event("turn.completed", { response: "Hello" }); + }, 5); + return {}; + } queueMicrotask(() => { if (this.outcome === "hang") return; this.event("model.streaming", { kind: "reasoning_delta", delta: "Thinking" }); @@ -70,6 +81,9 @@ describe("ZCode local agent", () => { for (const call of client.calls.filter(c => ["session/create", "session/send"].includes(c.method))) { expect(call.params._zcodeModel).toEqual({ providerId: "test", modelId: "model" }); expect(call.params.runtimeModel).toBeUndefined(); + const serialized = JSON.stringify(call.params); + expect(serialized).not.toContain("never-log-this"); + expect(serialized).not.toContain("apiKey"); } }); test("a revoked or changed managed connection cannot start a queued child", async () => { @@ -90,12 +104,25 @@ describe("ZCode local agent", () => { expect(() => adapter.buildRequest(request(), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() })) .toThrow("not HTTP"); expect(modelCapabilityFields({ supportsToolUse: false }).capabilities.supports_tool_use).toBe(false); + const comboSupportsTools = catalogRowSupportsToolUse(undefined, { targets: [{ provider: "zcode", model: "model" }] }, { zcode: provider }); + expect(comboSupportsTools).toBe(false); + expect(modelCapabilityFields({ supportsToolUse: comboSupportsTools }).capabilities.supports_tool_use).toBe(false); + expect(catalogRowSupportsToolUse(undefined, { targets: [{ provider: "other", model: "model" }] }, { + other: { adapter: "openai-chat", baseUrl: "https://example.invalid" }, + })).toBe(true); expect(deriveProviderPresets().find(p => p.id === "zcode")).toMatchObject({ auth: "local", adapter: "zcode" }); }); test("requires explicit operator opt-in and an argv launcher, never a shell string", () => { expect(() => loadZcodeSettings({})).toThrow("disabled"); expect(() => loadZcodeSettings({ OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: "echo unsafe" })).toThrow("JSON argv"); }); + test("advanced settings allow an isolated home when the proxy HOME is absent", () => { + const settings = fixture(); + const env = { OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: JSON.stringify(["/isolated-launcher"]), + OCX_ZCODE_HOME: settings.home, OCX_ZCODE_WORKSPACE: settings.workspace }; + expect(loadZcodeSettings(env)).toMatchObject({ home: settings.home, workspace: settings.workspace }); + expect(() => loadZcodeSettings({ ...env, HOME: settings.home })).toThrow("separate home"); + }); test("catalog excludes disabled/recursive entries and preserves canonical model identity", () => { const models = readZcodeModels(fixture()); expect(models.map(m => m.id)).toEqual(["test/model"]); @@ -120,6 +147,12 @@ describe("ZCode local agent", () => { expect(client.closed).toBe(true); expect(client.calls.map(c => c.method)).toEqual(["session/create", "session/subscribe", "session/send", "session/stop"]); }); + test("ignores terminal events without the active session identity", async () => { + const events = await run(fixture(), new FakeClient("session-noise")); + expect(events.filter(e => e.type === "text_delta" && e.text === "wrong session")).toHaveLength(0); + expect(events.filter(e => e.type === "text_delta" && e.text === "Hello")).toHaveLength(1); + expect(events.at(-1)?.type).toBe("done"); + }); test("continuation resumes only owner-fenced state and sends the current delta", async () => { const settings = fixture(); const first = await run(settings, new FakeClient()); diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index c4a76dc91d..57d4d4b017 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -24,8 +24,9 @@ afterEach(() => { const models = [{ id: "builtin:zai-coding-plan/glm-5.3", providerId: "builtin:zai-coding-plan", modelId: "glm-5.3", label: "GLM-5.3" }, { id: "builtin:zai-coding-plan/glm-5.3-flash", providerId: "builtin:zai-coding-plan", modelId: "glm-5.3-flash", label: "GLM-5.3-Flash" }]; const status = { connected: false, issue: undefined, runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models, platform: "linux" as const }; -function context(path: string, body: unknown, principal?: ManagementContext["principal"]): ManagementContext { - const req = new Request(`http://localhost${path}`, { method: "POST", headers: { "content-type": "application/json", origin: "http://localhost", "x-opencodex-gui-session": "forged" }, body: JSON.stringify(body) }); +function context(path: string, body: unknown, principal?: ManagementContext["principal"], method = "POST"): ManagementContext { + const req = new Request(`http://localhost${path}`, { method, headers: { "content-type": "application/json", origin: "http://localhost", "x-opencodex-gui-session": "forged" }, + ...(method === "GET" ? {} : { body: JSON.stringify(body) }) }); const config = getDefaultConfig(); const catalog = join(home, "catalog.json"); return { req, url: new URL(req.url), principal, config, @@ -51,7 +52,23 @@ describe("ZCode Desktop management consent", () => { } expect(f.calls()).toBe(0); }); + test(`rejects Desktop path discovery from ${principal ?? "missing"} principal`, async () => { + const f = fixture(); + for (const path of ["/api/zcode-desktop", "/api/zcode-desktop/folders"]) { + const response = await handleZcodeDesktopRoutes(context(path, {}, principal, "GET"), f.deps); + expect(response?.status).toBe(403); + expect(await response?.json()).toEqual({ error: "dashboard_required" }); + } + expect(f.calls()).toBe(0); + }); } + test("GUI sessions can inspect Desktop status without triggering native execution", async () => { + const f = fixture(); + const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop", {}, "gui-session", "GET"), f.deps); + expect(response?.status).toBe(200); + expect(await response?.json()).toMatchObject({ connected: false, activation: "disconnected" }); + expect(f.calls()).toBe(0); + }); test("requires an explicit consent checkbox, not just a GUI session", async () => { const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { runtime: "/installed/ZCode", workspace: "/project" }, "gui-session"), f.deps); expect(response?.status).toBe(400); expect(f.calls()).toBe(0); @@ -126,6 +143,19 @@ test("a different provider occupying zcode is never overwritten", async () => { expect(ctx.config.providers).toEqual(before); }); +test("ambiguous legacy ZCode bindings are not collapsed or overwritten", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); + const first = { adapter: "zcode" as const, authMode: "local" as const, baseUrl: "https://zcode.z.ai", disabled: true, note: "canonical" }; + const second = { ...first, note: "legacy alias" }; + ctx.config.providers.zcode = first; + ctx.config.providers["zcode-legacy"] = second; + const before = structuredClone(ctx.config.providers); + expect(await activateDesktopProvider(ctx, { ...status, connected: true }, f.deps.readDesktopCatalogSlugs)).toMatchObject({ + activation: "provider_pending", error: "provider_registration_failed", + }); + expect(ctx.config.providers).toEqual(before); +}); + test("activation retry is consent-gated and never repeats the protocol probe", async () => { const f = fixture(), ctx = context("/api/zcode-desktop/activate", { consent: true, runtime: "/installed/ZCode", workspace: "/project", diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 1187c6f2ec..563aa566e2 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -43,6 +43,13 @@ describe("managed ZCode Desktop", () => { expect(JSON.stringify(catalog)).not.toContain("fixture-private-value"); expect(JSON.stringify(catalog)).not.toContain("apiKey"); }); + test("filters invalid model entries before applying the public catalog cap", () => { + const invalid = Object.fromEntries(Array.from({ length: 210 }, (_, i) => [`invalid ${i}`, {}])); + const config = normalizeDesktopConfig({ provider: { "builtin:zai-coding-plan": { + ...provider(), models: { ...invalid, valid: { name: "Valid" } }, + } } }); + expect(desktopModelCatalog(config).map((model: { modelId: string }) => model.modelId)).toContain("valid"); + }); test("runtime selection requires a Desktop resources layout, not arbitrary commands", () => { expect(() => resolveDesktopRuntime("node -e malicious")).toThrow("desktop_missing"); const glm = join(root, "app/resources/glm"); mkdirSync(glm, { recursive: true }); @@ -63,6 +70,21 @@ describe("managed ZCode Desktop", () => { expect(() => validateDesktopWorkspace(join(root, "alias"))).toThrow("workspace_invalid"); } }); + test("sandbox checks canonical config paths while allowing only its canonical managed workspace", () => { + if (process.platform === "win32") return; + const realConfig = join(root, "real-config"); + const linkedConfig = join(root, "linked-config"); + mkdirSync(join(realConfig, "zcode-desktop"), { recursive: true }); + symlinkSync(realConfig, linkedConfig); + process.env.OPENCODEX_HOME = linkedConfig; + expect(() => validateDesktopWorkspace(linkedConfig)).toThrow("workspace_invalid"); + expect(() => validateDesktopWorkspace(realConfig)).toThrow("workspace_invalid"); + const protectedChild = join(realConfig, "zcode-desktop", "private"); + mkdirSync(protectedChild); + expect(() => validateDesktopWorkspace(protectedChild)).toThrow("workspace_invalid"); + const managed = join(linkedConfig, "zcode-desktop", "workspace"); + expect(validateDesktopWorkspace(managed)).toBe(join(realConfig, "zcode-desktop", "workspace")); + }); test("disconnect persists revocation instead of falling back to operator env", async () => { expect(loadDesktopSettings()).toBeUndefined(); await disconnectDesktop(); @@ -80,6 +102,7 @@ describe("managed ZCode Desktop", () => { }); test("Node selection skips an incompatible nvm prefix and fails safely", () => { + if (process.platform === "win32") return; const oldPath = process.env.PATH; const old = join(root, "old/bin"), modern = join(root, "modern/bin"); mkdirSync(old, { recursive: true }); mkdirSync(modern, { recursive: true }); diff --git a/tests/providers/zcode-quota.test.ts b/tests/providers/zcode-quota.test.ts index af831c3182..4e390032ad 100644 --- a/tests/providers/zcode-quota.test.ts +++ b/tests/providers/zcode-quota.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { parseZcodeQuota, readZcodeQuota, type QuotaContext } from "../../src/adapters/zcode/quota"; +import { parseZcodeQuota, parseZcodeQuotaSnapshot, readZcodeQuota, type QuotaContext, type ZcodeQuotaSnapshot } from "../../src/adapters/zcode/quota"; import { getCachedProviderQuota, replaceCachedProviderQuotas } from "../../src/providers/quota-routing-cache"; import { readFileSync } from "node:fs"; import { repoPath } from "../helpers/repo-root"; @@ -28,17 +28,23 @@ describe("ZCode native subscription quota", () => { }); test("does not guess unnamed windows or invent missing weekly limits", () => { expect(parseZcodeQuota(snapshot([{ ...window(), unit: 7 }, { type: "TIME_LIMIT", remaining: 100 }]), now)).toBeNull(); + expect(parseZcodeQuotaSnapshot(snapshot([{ ...window(), unit: 7 }, { type: "TIME_LIMIT", remaining: 100 }]), now)).toEqual({ kind: "empty" }); + expect(parseZcodeQuotaSnapshot(snapshot([null]), now)).toBeNull(); + expect(parseZcodeQuotaSnapshot(snapshot([ + ...Array.from({ length: 20 }, () => ({ type: "TIME_LIMIT" })), window(), + ]), now)).toMatchObject({ kind: "quota", quota: { fiveHourPercent: 25 } }); + expect(parseZcodeQuotaSnapshot({ generatedAt: now - 31 * 60_000, limits: [] }, now)).toBeNull(); expect(parseZcodeQuota(snapshot([window()]), now)?.weeklyPercent).toBeUndefined(); expect(parseZcodeQuota(snapshot([{ type: "TOKENS_LIMIT", unit: 6, number: 1, percentage: 40 }]), now)?.weeklyPercent).toBe(40); }); test("coalesces native reads and discards snapshots after account change", async () => { let calls = 0; let identity = "account-a"; - let finish!: (value: ReturnType) => void; - const deps = { context: () => context(identity), probe: async () => { calls++; return new Promise>(resolve => { finish = resolve; }); } }; + let finish!: (value: ZcodeQuotaSnapshot | null) => void; + const deps = { context: () => context(identity), probe: async () => { calls++; return new Promise(resolve => { finish = resolve; }); } }; const first = readZcodeQuota(provider, deps); const second = readZcodeQuota(provider, deps); expect(calls).toBe(1); identity = "account-b"; - finish(parseZcodeQuota(snapshot([window()]), now)); + finish(parseZcodeQuotaSnapshot(snapshot([window()]), now)); expect(await first).toBeNull(); expect(await second).toBeNull(); }); test("unavailable runtime has no direct API fallback", async () => { @@ -47,6 +53,12 @@ describe("ZCode native subscription quota", () => { expect(bootstrap).toContain("getEntitlementSnapshot"); expect(bootstrap).not.toMatch(/\bfetch\s*\(|https\.request|useCodingPlanReset|session\/send/); }); + test("preserves unavailable and authoritative-empty probe states separately", async () => { + expect(await readZcodeQuota(provider, { context: () => context(), probe: async () => null })).toBeNull(); + expect(await readZcodeQuota(provider, { context: () => context(), probe: async () => ({ kind: "empty" }) })).toEqual({ + identity: "account-a", kind: "empty", + }); + }); test("display snapshots do not change automatic routing policy", () => { replaceCachedProviderQuotas([{ provider: "zcode", label: "ZCode", source: "zcode-desktop", updatedAt: now, quota: { updatedAt: now, fiveHourPercent: 100 } }]); expect(getCachedProviderQuota("zcode", now)).toBeNull(); @@ -58,11 +70,11 @@ test("concurrent saved accounts receive only their own quota snapshots", async ( let calls = 0; const deps = { context: (p: { zcodeAccountId?: string }) => context(p.zcodeAccountId!), probe: async (c: QuotaContext) => { calls++; await new Promise(r => setTimeout(r, 5)); - return { updatedAt: now, fiveHourPercent: c.identity === "account-a" ? 10 : 90 }; + return { kind: "quota" as const, quota: { updatedAt: now, fiveHourPercent: c.identity === "account-a" ? 10 : 90 } }; } }; const [first, repeated, second] = await Promise.all([readZcodeQuota(a, deps), readZcodeQuota(a, deps), readZcodeQuota(b, deps)]); expect(calls).toBe(2); expect(first).toEqual(repeated); - expect(first).toMatchObject({ identity: "account-a", quota: { fiveHourPercent: 10 } }); - expect(second).toMatchObject({ identity: "account-b", quota: { fiveHourPercent: 90 } }); + expect(first).toMatchObject({ identity: "account-a", kind: "quota", quota: { fiveHourPercent: 10 } }); + expect(second).toMatchObject({ identity: "account-b", kind: "quota", quota: { fiveHourPercent: 90 } }); }); From fdd475f15d18f63bf26e625a5596fdc8ca24c1d7 Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 17:26:58 -0300 Subject: [PATCH 12/43] fix(zcode): address follow-up hygiene findings --- gui/src/i18n/de.ts | 2 +- gui/src/i18n/en.ts | 2 +- gui/src/i18n/fr.ts | 2 +- gui/src/i18n/ja.ts | 2 +- gui/src/i18n/ko.ts | 2 +- gui/src/i18n/ru.ts | 2 +- gui/src/i18n/tr.ts | 4 +-- gui/src/i18n/zh-TW.ts | 2 +- gui/src/i18n/zh.ts | 2 +- gui/tests/zcode-desktop-pane.test.tsx | 2 +- src/adapters/zcode/account-runtime.ts | 11 ++++-- src/adapters/zcode/accounts.ts | 20 ++++++----- src/adapters/zcode/desktop.ts | 12 +++---- src/adapters/zcode/native-oauth.ts | 15 ++++---- src/adapters/zcode/oauth-bootstrap.cjs | 34 ++++++++++++++----- src/server/management/zcode-account-routes.ts | 4 +-- tests/providers/zcode-desktop-routes.test.ts | 26 ++++++++++++-- tests/providers/zcode-desktop.test.ts | 25 +++++++++++++- 18 files changed, 121 insertions(+), 48 deletions(-) diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index aae978df78..cef9cc3aba 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -22,7 +22,7 @@ export const de: Record = { "zcodeDesktop.restartNotice": "Falls Codex die Modelle nicht zeigt, starten Sie Codex nach Abschluss Ihrer Aufgaben neu. Kein automatischer Prozessneustart.", "zcodeDesktop.dashboardRequired": "Diese Aktion erfordert eine Browsersitzung über ein an Loopback gebundenes Dashboard oder einen gekoppelten Hub. Ein Admin-Token allein erlaubt keine Desktop-Einrichtung.", "zcodeDesktop.hostAccess": "ZCode läuft ohne OpenCodex-Sandbox und kann alle Dateien lesen oder ändern, die Benutzerrechte und Harness erlauben. Der Arbeitsordner ist keine Zugriffsgrenze. Dies gilt nach Updates auch für bestehende Verbindungen. Für Isolation OCX_ZCODE_SANDBOX=1 am Proxy setzen und OpenCodex neu starten.", - "zcodeDesktop.sandboxAccess": "Die optionale Bubblewrap-Isolation ist aktiv. Nur der gewählte Arbeitsordner ist eingebunden; andere Hostpfade sind nicht zugänglich.", + "zcodeDesktop.sandboxAccess": "Die optionale Bubblewrap-Isolation ist aktiv. Ausgewählte System- und Laufzeitpfade sowie vorhandene Profil- und Anmeldedateien werden schreibgeschützt eingebunden. Ein privates Home und der gewählte Arbeitsordner unter /workspace sind beschreibbar; temporäre Dateisysteme sind verfügbar. Andere Hostpfade werden nicht eingebunden.", "zcodeAccounts.title": "Gespeicherte ZCode-Konten", "zcodeAccounts.help": "Jedes Konto hat eine eigene offizielle Anmeldung, Modelle und Quote. Modell manuell in Codex wählen; kein Pool oder automatischer Wechsel. Die Desktop-Verbindung bleibt separat.", "zcodeAccounts.name": "Kontoname", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index d30f18de2e..98430ec13f 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -23,7 +23,7 @@ export const en = { "zcodeDesktop.restartNotice": "If Codex does not show the new models, restart Codex when your tasks finish. No processes will be restarted automatically.", "zcodeDesktop.dashboardRequired": "This action requires a browser session on a loopback-bound dashboard or a paired hub. An admin token alone cannot authorize Desktop setup.", "zcodeDesktop.hostAccess": "ZCode runs without an OpenCodex sandbox and can read or modify any file your operating-system user and harness allow. The workspace is only a working directory. This also applies to existing connections after upgrading. To enable isolation, set OCX_ZCODE_SANDBOX=1 on the proxy and restart OpenCodex.", - "zcodeDesktop.sandboxAccess": "Optional Bubblewrap isolation is enabled. Only the selected workspace is mounted; host absolute paths outside it are unavailable.", + "zcodeDesktop.sandboxAccess": "Optional Bubblewrap isolation is enabled. Selected system and runtime paths, plus profile and credential files when present, are mounted read-only. A private home and the selected workspace at /workspace are writable, and temporary filesystems are available. Other host paths are not mounted.", "zcodeAccounts.title": "Saved ZCode accounts", "zcodeAccounts.help": "Each account has its own official login, models and quota. Choose its model manually in Codex; there is no pool or automatic account switch. Existing Desktop connection stays separate.", "zcodeAccounts.name": "Account name", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 5bffd7453f..23f6efab29 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -21,7 +21,7 @@ export const fr: Record = { "zcodeDesktop.restartNotice": "Si Codex n’affiche pas les modèles, redémarrez-le après vos tâches. Aucun processus ne sera redémarré automatiquement.", "zcodeDesktop.dashboardRequired": "Cette action nécessite une session navigateur sur un tableau de bord lié à loopback ou un hub appairé. Le jeton administrateur seul ne permet pas la configuration Desktop.", "zcodeDesktop.hostAccess": "ZCode fonctionne sans bac à sable OpenCodex et peut lire ou modifier tout fichier autorisé par votre utilisateur système et le harness. Le dossier de travail ne limite pas cet accès. Cela concerne aussi les connexions existantes après mise à jour. Pour activer l’isolation, définissez OCX_ZCODE_SANDBOX=1 sur le proxy et redémarrez OpenCodex.", - "zcodeDesktop.sandboxAccess": "L’isolation Bubblewrap optionnelle est active. Seul le dossier de travail choisi est monté ; les autres chemins de l’hôte sont inaccessibles.", + "zcodeDesktop.sandboxAccess": "L’isolation Bubblewrap optionnelle est active. Certains chemins système et d’exécution, ainsi que les fichiers de profil et d’identifiants présents, sont montés en lecture seule. Un dossier personnel privé et l’espace de travail choisi dans /workspace sont inscriptibles, et des systèmes de fichiers temporaires sont disponibles. Les autres chemins de l’hôte ne sont pas montés.", "zcodeAccounts.title": "Comptes ZCode enregistrés", "zcodeAccounts.help": "Chaque compte possède sa connexion officielle, ses modèles et son quota. Sélection manuelle dans Codex, sans pool ni basculement automatique. La connexion Desktop reste séparée.", "zcodeAccounts.name": "Nom du compte", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 2ec279fe80..f741533991 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -21,7 +21,7 @@ export const ja: Record = { "zcodeDesktop.restartNotice": "Codex にモデルが表示されない場合は、作業終了後に Codex を再起動してください。自動再起動は行いません。", "zcodeDesktop.dashboardRequired": "この操作にはループバックにバインドされたダッシュボードまたはペアリング済みハブのブラウザーセッションが必要です。管理トークンだけでは Desktop を設定できません。", "zcodeDesktop.hostAccess": "ZCode は OpenCodex のサンドボックスなしで実行され、OS ユーザーとハーネスが許可するファイルを読み書きできます。作業ディレクトリはアクセス制限ではありません。更新後の既存接続にも適用されます。隔離するにはプロキシで OCX_ZCODE_SANDBOX=1 を設定し、OpenCodex を再起動してください。", - "zcodeDesktop.sandboxAccess": "任意の Bubblewrap 隔離が有効です。選択した作業ディレクトリのみがマウントされ、それ以外のホストパスにはアクセスできません。", + "zcodeDesktop.sandboxAccess": "任意の Bubblewrap 隔離が有効です。選択されたシステムおよびランタイムのパスと、存在するプロファイル・認証情報ファイルは読み取り専用でマウントされます。プライベートホームと /workspace の選択した作業ディレクトリは書き込み可能で、一時ファイルシステムも利用できます。その他のホストパスはマウントされません。", "zcodeAccounts.title": "保存済み ZCode アカウント", "zcodeAccounts.help": "各アカウントは公式ログイン、モデル、利用枠を個別に持ちます。Codex でモデルを手動選択します。プールや自動切替はありません。Desktop 接続は別です。", "zcodeAccounts.name": "アカウント名", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index b04a1c1d24..044b9ab13e 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -21,7 +21,7 @@ export const ko: Record = { "zcodeDesktop.restartNotice": "Codex에 모델이 표시되지 않으면 작업 완료 후 Codex를 다시 시작하세요. 프로세스를 자동으로 다시 시작하지 않습니다.", "zcodeDesktop.dashboardRequired": "이 작업에는 루프백에 바인딩된 대시보드 또는 페어링된 허브의 브라우저 세션이 필요합니다. 관리자 토큰만으로는 Desktop을 설정할 수 없습니다.", "zcodeDesktop.hostAccess": "ZCode는 OpenCodex 샌드박스 없이 실행되며 OS 사용자와 하네스가 허용하는 파일을 읽거나 수정할 수 있습니다. 작업 폴더는 접근 경계가 아닙니다. 업데이트 후 기존 연결에도 적용됩니다. 격리하려면 프록시에 OCX_ZCODE_SANDBOX=1을 설정하고 OpenCodex를 재시작하세요.", - "zcodeDesktop.sandboxAccess": "선택적 Bubblewrap 격리가 활성화되었습니다. 선택한 작업 폴더만 마운트되며 다른 호스트 경로에는 접근할 수 없습니다.", + "zcodeDesktop.sandboxAccess": "선택적 Bubblewrap 격리가 활성화되었습니다. 선택한 시스템 및 런타임 경로와 존재하는 프로필·자격 증명 파일은 읽기 전용으로 마운트됩니다. 비공개 홈과 /workspace의 선택한 작업 폴더는 쓰기 가능하며 임시 파일 시스템도 사용할 수 있습니다. 그 밖의 호스트 경로는 마운트되지 않습니다.", "zcodeAccounts.title": "저장된 ZCode 계정", "zcodeAccounts.help": "계정마다 공식 로그인, 모델, 할당량이 분리됩니다. Codex에서 모델을 직접 선택하세요. 풀이나 자동 계정 전환은 없습니다. Desktop 연결은 별도입니다.", "zcodeAccounts.name": "계정 이름", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index aa1efb5c4e..997f12c360 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -21,7 +21,7 @@ export const ru: Record = { "zcodeDesktop.restartNotice": "Если модели не появились, перезапустите Codex после завершения задач. Автоматического перезапуска процессов не будет.", "zcodeDesktop.dashboardRequired": "Требуется сеанс браузера на панели, привязанной к loopback, или сопряжённом хабе. Одного токена администратора недостаточно для настройки Desktop.", "zcodeDesktop.hostAccess": "ZCode работает без песочницы OpenCodex и может читать или менять любые файлы, разрешённые системному пользователю и средой запуска. Рабочий каталог не ограничивает доступ. Это относится и к существующим подключениям после обновления. Для изоляции задайте OCX_ZCODE_SANDBOX=1 у прокси и перезапустите OpenCodex.", - "zcodeDesktop.sandboxAccess": "Дополнительная изоляция Bubblewrap включена. Доступен только выбранный рабочий каталог; остальные пути хоста недоступны.", + "zcodeDesktop.sandboxAccess": "Дополнительная изоляция Bubblewrap включена. Выбранные системные пути и runtime, а также имеющиеся файлы профиля и учётных данных монтируются только для чтения. Приватный домашний каталог и выбранный рабочий каталог в /workspace доступны для записи; также доступны временные файловые системы. Остальные пути хоста не монтируются.", "zcodeAccounts.title": "Сохранённые аккаунты ZCode", "zcodeAccounts.help": "У каждого аккаунта свой официальный вход, модели и квота. Выбирайте модель вручную в Codex: без пула и автопереключения. Подключение Desktop остаётся отдельным.", "zcodeAccounts.name": "Название аккаунта", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 5eec93ec24..b83ba61105 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -22,7 +22,7 @@ export const tr: Record = { "zcodeDesktop.restartNotice": "Modeller görünmüyorsa görevler bitince Codex’i yeniden başlatın. Hiçbir işlem otomatik yeniden başlatılmaz.", "zcodeDesktop.dashboardRequired": "Bu işlem, loopback’e bağlı panelde veya eşleştirilmiş hub’da tarayıcı oturumu gerektirir. Yalnızca yönetici jetonu Desktop kurulumuna yetki vermez.", "zcodeDesktop.hostAccess": "ZCode, OpenCodex sanal alanı olmadan çalışır; işletim sistemi kullanıcınızın ve harness’in izin verdiği dosyaları okuyabilir veya değiştirebilir. Çalışma klasörü bir erişim sınırı değildir. Güncellemeden sonra mevcut bağlantılar için de geçerlidir. Yalıtım için proxy üzerinde OCX_ZCODE_SANDBOX=1 ayarlayıp OpenCodex’i yeniden başlatın.", - "zcodeDesktop.sandboxAccess": "İsteğe bağlı Bubblewrap yalıtımı açık. Yalnızca seçilen çalışma klasörü bağlanır; diğer ana makine yollarına erişilemez.", + "zcodeDesktop.sandboxAccess": "İsteğe bağlı Bubblewrap yalıtımı etkin. Seçili sistem ve çalışma zamanı yolları ile mevcut profil ve kimlik bilgisi dosyaları salt okunur bağlanır. Özel bir ev dizini ve /workspace altındaki seçili çalışma klasörü yazılabilir; geçici dosya sistemleri de kullanılabilir. Diğer ana makine yolları bağlanmaz.", "zcodeAccounts.title": "Kayıtlı ZCode hesapları", "zcodeAccounts.help": "Her hesabın resmi oturumu, modelleri ve kotası ayrıdır. Codex’te modeli elle seçin; havuz veya otomatik geçiş yoktur. Desktop bağlantısı ayrıdır.", "zcodeAccounts.name": "Hesap adı", @@ -30,7 +30,7 @@ export const tr: Record = { "zcodeAccounts.add": "Hesap ekle", "zcodeAccounts.loginHint": "Tarayıcıda resmi girişi tamamlayın. Başka hesap için hesap değiştirin veya özel pencere kullanın. Bağlantı süreli; bu paneli açık tutun.", "zcodeAccounts.login": "Resmi girişi aç", - "zcodeAccounts.failed": "İşlem tamamlanamadı. Süresi dolan girişi yenileyin, tekrar hesabı yeniden bağlayın, etkin işleri bekleyin ve silmeden önce yönlendirme referanslarını kaldırın.", + "zcodeAccounts.failed": "İşlem tamamlanamadı. Süresi dolan girişi yenileyin, hesabı yeniden bağlayın, etkin işleri bekleyin ve silmeden önce yönlendirme referanslarını kaldırın.", "zcodeAccounts.pending": "Hesap kurulumu eksik", "zcodeAccounts.reconnect": "Yeniden giriş yap", "zcodeAccounts.rename": "Yeniden adlandır", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 7e14b79d34..271b2ff19d 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -19,7 +19,7 @@ export const zhTW: Record = { "zcodeDesktop.restartNotice": "若 Codex 未顯示新模型,請於任務結束後重新啟動 Codex。不會自動重新啟動任何程序。", "zcodeDesktop.dashboardRequired": "此操作需要綁定至迴路位址的儀表板或已配對 Hub 的瀏覽器工作階段。僅管理員權杖無法授權 Desktop 設定。", "zcodeDesktop.hostAccess": "ZCode 預設不使用 OpenCodex 沙箱,可讀寫作業系統使用者與 harness 允許存取的任何檔案。工作目錄不是存取邊界。升級後現有連線也適用。如需隔離,請在代理上設定 OCX_ZCODE_SANDBOX=1 並重新啟動 OpenCodex。", - "zcodeDesktop.sandboxAccess": "已啟用選用的 Bubblewrap 隔離。僅掛載選定的工作目錄,無法存取其他主機路徑。", + "zcodeDesktop.sandboxAccess": "選用的 Bubblewrap 隔離已啟用。選定的系統與執行階段路徑,以及存在的設定檔與憑證檔案會以唯讀方式掛載。私有家目錄和掛載於 /workspace 的所選工作區可寫入,並提供暫存檔案系統。其他主機路徑不會掛載。", "zcodeAccounts.title": "已儲存的 ZCode 帳戶", "zcodeAccounts.help": "每個帳戶擁有獨立的官方登入、模型與額度。請在 Codex 中手動選擇模型;不使用帳戶池或自動切換。Desktop 連線保持獨立。", "zcodeAccounts.name": "帳戶名稱", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index fae24455d7..c8cf8df0a0 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -21,7 +21,7 @@ export const zh: Record = { "zcodeDesktop.restartNotice": "若 Codex 未显示新模型,请在任务结束后重启 Codex。不会自动重启任何进程。", "zcodeDesktop.dashboardRequired": "此操作需要绑定到回环地址的仪表板或已配对 Hub 的浏览器会话。仅管理员令牌无法授权 Desktop 设置。", "zcodeDesktop.hostAccess": "ZCode 默认不使用 OpenCodex 沙箱,可读写操作系统用户和 harness 允许访问的任何文件。工作目录不是访问边界。升级后现有连接也适用。如需隔离,请在代理上设置 OCX_ZCODE_SANDBOX=1 并重启 OpenCodex。", - "zcodeDesktop.sandboxAccess": "已启用可选的 Bubblewrap 隔离。仅挂载所选工作目录,无法访问其他主机路径。", + "zcodeDesktop.sandboxAccess": "可选的 Bubblewrap 隔离已启用。选定的系统和运行时路径,以及存在的配置文件和凭据文件将以只读方式挂载。私有主目录和挂载到 /workspace 的所选工作区可写,并提供临时文件系统。其他主机路径不会挂载。", "zcodeAccounts.title": "已保存的 ZCode 账户", "zcodeAccounts.help": "每个账户拥有独立的官方登录、模型和额度。请在 Codex 中手动选择模型;不使用账户池或自动切换。Desktop 连接保持独立。", "zcodeAccounts.name": "账户名称", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 022c2c0414..360488c5e1 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -179,7 +179,7 @@ test("explicit sandbox mode shows its filesystem boundary", async () => { connected: false, sandbox: true, runtimes: [], runtime: "", workspace: "/project", models: [], }) }); await mountPane(); - expect(host.querySelector('[role="note"]')?.textContent).toContain("Only the selected workspace"); + expect(host.querySelector('[role="note"]')?.textContent).toContain("Selected system and runtime paths"); expect(host.querySelector('[role="note"]')?.textContent).not.toContain("without an OpenCodex sandbox"); }); diff --git a/src/adapters/zcode/account-runtime.ts b/src/adapters/zcode/account-runtime.ts index b1326f0fc8..501952d09d 100644 --- a/src/adapters/zcode/account-runtime.ts +++ b/src/adapters/zcode/account-runtime.ts @@ -17,10 +17,15 @@ export async function refreshAccount(id: string): Promise { if (!status.connected) throw new Error("account_login_required"); const promise = (async () => { let identity: string | undefined; - await runNativeOAuth({ runtime: status.runtime, profileHome: accountProfile(id), mode: "refresh", - signal: AbortSignal.timeout(30_000), onEvent: event => { + let nativeError: string | undefined; + try { await runNativeOAuth({ runtime: status.runtime, profileHome: accountProfile(id), mode: "refresh", + expectedSubjectHash: account.subjectHash, signal: AbortSignal.timeout(30_000), onEvent: event => { if (event.type === "authenticated") identity = event.subjectHash; - } }); + if (event.type === "error") nativeError = event.code; + } }); } catch (error) { + if (nativeError === "account_identity_mismatch") throw new Error(nativeError); + throw error; + } if (identity !== account.subjectHash) throw new Error("account_identity_mismatch"); refreshed.set(id, Date.now() + 60_000); })(); diff --git a/src/adapters/zcode/accounts.ts b/src/adapters/zcode/accounts.ts index 1a16373eed..aa91b68189 100644 --- a/src/adapters/zcode/accounts.ts +++ b/src/adapters/zcode/accounts.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { randomUUID } from "node:crypto"; import { getConfigDir } from "../../config/paths"; -export interface ZcodeAccount { id: string; label: string; subjectHash?: string } +export interface ZcodeAccount { id: string; label: string; subjectHash?: string; draftFor?: string } export function accountRoot(id: string): string { if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id)) throw new Error("account_invalid"); return join(getConfigDir(), "zcode-accounts", id); @@ -16,15 +16,18 @@ export function readAccount(id: string): ZcodeAccount { if (!st.isFile() || st.size > 4096 || (st.mode & 0o077) || st.uid !== process.getuid?.()) throw new Error(); const value = JSON.parse(readFileSync(fd, "utf8")); if (value.id !== id || typeof value.label !== "string" || value.label.length > 80 - || (value.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(value.subjectHash))) throw new Error(); - return { id, label: value.label, ...(value.subjectHash ? { subjectHash: value.subjectHash } : {}) }; + || (value.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(value.subjectHash)) + || (value.draftFor !== undefined && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value.draftFor))) throw new Error(); + return { id, label: value.label, ...(value.subjectHash ? { subjectHash: value.subjectHash } : {}), + ...(value.draftFor ? { draftFor: value.draftFor } : {}) }; } finally { closeSync(fd); } } export function writeAccount(account: ZcodeAccount): void { const dir = accountRoot(account.id); const label = account.label.trim(); if (!label || label.length > 80 || /[\x00-\x1f]/.test(label) - || (account.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(account.subjectHash))) throw new Error("account_invalid"); + || (account.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(account.subjectHash)) + || (account.draftFor !== undefined && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(account.draftFor))) throw new Error("account_invalid"); mkdirSync(dir, { recursive: true, mode: 0o700 }); const temp = join(dir, randomUUID() + ".tmp"); writeFileSync(temp, JSON.stringify({ ...account, label }), { mode: 0o600, flag: "wx" }); @@ -34,12 +37,13 @@ export function listAccounts(): ZcodeAccount[] { const dir = join(getConfigDir(), "zcode-accounts"); if (!existsSync(dir)) return []; return readdirSync(dir).filter(id => /^[a-f0-9-]{36}$/.test(id)).slice(0, 100).flatMap(id => { - try { return [readAccount(id)]; } catch { return []; } + try { const account = readAccount(id); return account.draftFor ? [] : [account]; } catch { return []; } }); } -export function allocateAccount(label: string): ZcodeAccount { - if (listAccounts().length >= 20) throw new Error("account_limit"); - const account = { id: randomUUID(), label }; +export function allocateAccount(label: string, replaceId?: string): ZcodeAccount { + if (replaceId) readAccount(replaceId); + if (!replaceId && listAccounts().length >= 20) throw new Error("account_limit"); + const account = { id: randomUUID(), label, ...(replaceId ? { draftFor: replaceId } : {}) }; writeAccount(account); mkdirSync(accountProfile(account.id), { recursive: true, mode: 0o700 }); return account; diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index 9222c1544b..b9680af96c 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -96,9 +96,9 @@ export function detectDesktopRuntimes(): string[] { return [...found].slice(0, 8); } -export function validateDesktopWorkspace(path: string): string { +export function validateDesktopWorkspace(path: string, accountId?: string): string { if (!path || !isAbsolute(path) || path.includes("\0") || path.length > 4096) return fail("workspace_invalid"); - const defaultPath = defaultDesktopWorkspace(); + const defaultPath = defaultDesktopWorkspace(accountId); if (resolve(path) === resolve(defaultPath)) mkdirSync(path, { recursive: true, mode: 0o700 }); let workspace: string; try { workspace = realpathSync(path); if (!statSync(workspace).isDirectory()) return fail("workspace_invalid"); } @@ -106,7 +106,7 @@ export function validateDesktopWorkspace(path: string): string { if (!desktopSandboxEnabled()) return workspace; const home = realpathSync(homedir()); if (workspace === home || home.startsWith(workspace.endsWith(sep) ? workspace : workspace + sep)) return fail("workspace_invalid"); - const configRoot = canonicalExistingOrResolved(root()); + const configRoot = canonicalExistingOrResolved(getConfigDir()); const managedDefault = canonicalExistingOrResolved(defaultPath); const isManagedDefault = workspace === managedDefault && managedDefault.startsWith(configRoot + sep); if (!isManagedDefault) { @@ -153,7 +153,7 @@ export function desktopProfile(accountId?: string): { config: string; credential function settingsFor(connection: Connection, accountId?: string): ZcodeSettings { const { bwrap, node } = prerequisites(); const runtime = resolveDesktopRuntime(connection.runtime); - const workspace = validateDesktopWorkspace(connection.workspace); + const workspace = validateDesktopWorkspace(connection.workspace, accountId); const profile = desktopProfile(accountId); // Do not carry a native conversation across Desktop login/profile changes. Metadata-only // fencing is conservative (a refresh may start a new session) and never reads credential bytes. @@ -207,7 +207,7 @@ export function desktopStatus(accountId?: string) { try { connection = readConnection(accountId); } catch { issue = "connection_invalid"; } const runtimes = detectDesktopRuntimes(); if (connection?.connected) { - try { resolveDesktopRuntime(connection.runtime); validateDesktopWorkspace(connection.workspace); } + try { resolveDesktopRuntime(connection.runtime); validateDesktopWorkspace(connection.workspace, accountId); } catch (e) { issue = e instanceof DesktopSetupError ? e.code : "connection_invalid"; } } return { ...(accountId ? { accountId } : {}), connected: connection?.connected === true && !issue, issue, runtimes, @@ -222,7 +222,7 @@ export async function connectDesktop(runtime: string, workspace: string, account connecting.add(accountId ?? "desktop"); try { const connection: Connection = { version: 1, connected: true, generation: randomUUID(), - runtime: resolveDesktopRuntime(runtime), workspace: validateDesktopWorkspace(workspace), models: [] }; + runtime: resolveDesktopRuntime(runtime), workspace: validateDesktopWorkspace(workspace, accountId), models: [] }; const settings = settingsFor(connection, accountId); const client = new ZcodeClient(settings); try { diff --git a/src/adapters/zcode/native-oauth.ts b/src/adapters/zcode/native-oauth.ts index 0894352858..1f191db7c3 100644 --- a/src/adapters/zcode/native-oauth.ts +++ b/src/adapters/zcode/native-oauth.ts @@ -10,7 +10,7 @@ export type NativeOAuthEvent = | { type: "capabilities"; supported: true } | { type: "authorization"; url: string } | { type: "authenticated"; subjectHash: string } - | { type: "error"; code: "login_expired" | "native_oauth_failed" | "session_restore_failed" | "model_setup_failed" }; + | { type: "error"; code: "login_expired" | "native_oauth_failed" | "session_restore_failed" | "model_setup_failed" | "account_identity_mismatch" }; /** Strict projection even if a vendor/bootstrap version emits unexpected fields. */ export function parseNativeOAuthEvent(value: unknown): NativeOAuthEvent { @@ -26,13 +26,13 @@ export function parseNativeOAuthEvent(value: unknown): NativeOAuthEvent { return { type: "authenticated", subjectHash: row.subjectHash }; } if (row.type === "error") return { type: "error", code: - row.code === "login_expired" || row.code === "session_restore_failed" || row.code === "model_setup_failed" + row.code === "login_expired" || row.code === "session_restore_failed" || row.code === "model_setup_failed" || row.code === "account_identity_mismatch" ? row.code : "native_oauth_failed" }; throw new Error("native_oauth_failed"); } /** The caller supplies a newly allocated private profile, never the user's Desktop home. */ -export function nativeOAuthCommand(runtime: string, profileHome: string, mode: "login" | "capabilities" | "refresh"): string[] { +export function nativeOAuthCommand(runtime: string, profileHome: string, mode: "login" | "capabilities" | "refresh", expectedSubjectHash?: string): string[] { if (process.platform !== "linux") throw new Error("platform_unsupported"); const profile = realpathSync(profileHome), st = lstatSync(profileHome); if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid?.() || (st.mode & 0o077) || profile === realpathSync(homedir())) { @@ -40,8 +40,9 @@ export function nativeOAuthCommand(runtime: string, profileHome: string, mode: " } const runtimeRoot = dirname(dirname(dirname(resolveDesktopRuntime(runtime)))); if (!lstatSync(join(runtimeRoot, "zcode")).isFile()) throw new Error("desktop_missing"); + if (mode === "refresh" && !/^[a-f0-9]{64}$/.test(expectedSubjectHash ?? "")) throw new Error("account_identity_mismatch"); if (!desktopSandboxEnabled()) return [join(runtimeRoot, "zcode"), - fileURLToPath(new URL("./oauth-bootstrap.cjs", import.meta.url)), mode, runtimeRoot]; + fileURLToPath(new URL("./oauth-bootstrap.cjs", import.meta.url)), mode, runtimeRoot, ...(expectedSubjectHash ? [expectedSubjectHash] : [])]; const bwrap = Bun.which("bwrap", { PATH: process.env.PATH }); if (!bwrap) throw new Error("sandbox_missing"); verifyDesktopSandbox(bwrap); @@ -54,16 +55,18 @@ export function nativeOAuthCommand(runtime: string, profileHome: string, mode: " args.push("--ro-bind", runtimeRoot, "/zcode", "--bind", profile, homedir(), "--ro-bind", fileURLToPath(new URL("./oauth-bootstrap.cjs", import.meta.url)), "/bridge.cjs", "--clearenv", "--setenv", "HOME", homedir(), "--setenv", "PATH", "/usr/bin:/bin", - "--setenv", "ELECTRON_RUN_AS_NODE", "1", "--chdir", homedir(), "/zcode/zcode", "/bridge.cjs", mode); + "--setenv", "ELECTRON_RUN_AS_NODE", "1", "--chdir", homedir(), "/zcode/zcode", "/bridge.cjs", mode, "/zcode", + ...(expectedSubjectHash ? [expectedSubjectHash] : [])); return args; } export async function runNativeOAuth(options: { runtime: string; profileHome: string; mode: "login" | "capabilities" | "refresh"; + expectedSubjectHash?: string; signal: AbortSignal; onEvent: (event: NativeOAuthEvent) => void; }): Promise { if (options.signal.aborted) throw new Error("login_cancelled"); - const [executable, ...args] = nativeOAuthCommand(options.runtime, options.profileHome, options.mode); + const [executable, ...args] = nativeOAuthCommand(options.runtime, options.profileHome, options.mode, options.expectedSubjectHash); const child = spawn(executable!, args, { env: desktopSandboxEnabled() ? { PATH: "/usr/bin:/bin" } : { PATH: "/usr/bin:/bin", HOME: homedir(), ELECTRON_RUN_AS_NODE: "1", ZCODE_DATA_BASE_DIR: options.profileHome, }, stdio: ["ignore", "pipe", "pipe"] }); diff --git a/src/adapters/zcode/oauth-bootstrap.cjs b/src/adapters/zcode/oauth-bootstrap.cjs index 2f849a4cb3..2dca79e267 100644 --- a/src/adapters/zcode/oauth-bootstrap.cjs +++ b/src/adapters/zcode/oauth-bootstrap.cjs @@ -11,6 +11,18 @@ const parent = new EventEmitter(); parent.postMessage = () => {}; process.parentPort = parent; const timer = setTimeout(() => { send({ type: "error", code: "login_expired" }); process.exit(1); }, 300_000); +const subjectHash = result => { + if (result?.kind !== "session" || result.provider !== "zai" || typeof result.userInfo?.id !== "string" || !result.userInfo.id) throw new Error(); + return createHash("sha256").update("zai\0" + result.userInfo.id).digest("hex"); +}; +/** Validate identity before the official model provider can mutate this profile. */ +async function materializeSession(result, models, expectedSubjectHash) { + const identity = subjectHash(result); + if (expectedSubjectHash && identity !== expectedSubjectHash) throw new Error("account_identity_mismatch"); + await models.call("getAll", []); + await models.call("refreshCodingPlanApiKey", ["builtin:zai-coding-plan"]); + return identity; +} async function main() { fs.mkdirSync((process.env.ZCODE_DATA_BASE_DIR || process.env.HOME) + "/.zcode/v2", { recursive: true, mode: 0o700 }); const dir = (process.argv[3] || "/zcode") + "/resources/app.asar/out/host"; @@ -38,21 +50,25 @@ async function main() { if (!providers.some(p => (p.id ?? p.providerId) === "zai" && p.enabled)) throw new Error(); send({ type: "capabilities", supported: true }); return; } - const publish = async result => { + const publish = async (result, expectedSubjectHash) => { stage = "session_restore_failed"; - if (result?.kind !== "session" || result.provider !== "zai" || typeof result.userInfo?.id !== "string" || !result.userInfo.id) throw new Error(); - // Official service materializes/refreshes built-in provider credentials and model config. stage = "model_setup_failed"; const models = client.getChannel("model-provider"); - await models.call("getAll", []); - await models.call("refreshCodingPlanApiKey", ["builtin:zai-coding-plan"]); - send({ type: "authenticated", subjectHash: createHash("sha256").update("zai\0" + result.userInfo.id).digest("hex") }); + let identity; + try { identity = await materializeSession(result, models, expectedSubjectHash); } + catch (error) { + if (error?.message === "account_identity_mismatch") stage = "account_identity_mismatch"; + throw error; + } + send({ type: "authenticated", subjectHash: identity }); }; if (process.argv[2] === "refresh") { stage = "session_restore_failed"; const session = await oauth.call("restoreCachedSessionState", []); if (session?.status !== "authenticated") throw new Error(); - await publish({ kind: "session", provider: "zai", userInfo: session.userInfo }); return; + const expected = process.argv[4]; + if (!/^[a-f0-9]{64}$/.test(expected || "")) { stage = "account_identity_mismatch"; throw new Error(); } + await publish({ kind: "session", provider: "zai", userInfo: session.userInfo }, expected); return; } if (process.argv[2] !== "login") throw new Error(); const flow = await oauth.call("startOAuthWithPolling", ["zai"]); @@ -68,6 +84,8 @@ async function main() { return; } } -main().then(() => { clearTimeout(timer); process.exit(0); }).catch(() => { +module.exports = { materializeSession, subjectHash }; +if (require.main === module) main().then(() => { clearTimeout(timer); process.exit(0); }).catch(() => { send({ type: "error", code: stage }); process.exit(1); }); +else clearTimeout(timer); diff --git a/src/server/management/zcode-account-routes.ts b/src/server/management/zcode-account-routes.ts index 3fe69e863d..683f3276f9 100644 --- a/src/server/management/zcode-account-routes.ts +++ b/src/server/management/zcode-account-routes.ts @@ -63,11 +63,11 @@ export async function handleZcodeAccountRoutes(ctx: ManagementContext, deps = se if (path === "/api/zcode-accounts/login") { if ([...jobs.values()].filter(job => !["failed", "finished"].includes(job.phase)).length >= 8) return jsonResponse({ error: "account_busy" }, 409); if (typeof body.label !== "string" || typeof body.runtime !== "string" || typeof body.workspace !== "string") return fail("account_invalid"); - const runtime = resolveDesktopRuntime(body.runtime), workspace = validateDesktopWorkspace(body.workspace); if (body.accountId !== undefined && typeof body.accountId !== "string") return fail("account_invalid"); const replaceId = typeof body.accountId === "string" ? readAccount(body.accountId).id : undefined; + const runtime = resolveDesktopRuntime(body.runtime), workspace = validateDesktopWorkspace(body.workspace, replaceId); if (replaceId && (accountRuntimeBusy(replaceId) || pendingFor(replaceId))) return fail("account_busy"); - const account = allocateAccount(body.label); + const account = allocateAccount(body.label, replaceId); const job: Job = { id: randomUUID(), accountId: account.id, replaceId, runtime, workspace, phase: "waiting", controller: new AbortController(), timer: undefined! }; job.timer = setTimeout(() => { diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index 57d4d4b017..da03b61d62 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -243,6 +243,25 @@ test("duplicate identity is rejected and reconnect preserves provider settings a expect(listAccounts()).toHaveLength(1); expect(f.ctx.config.providers[ready.providerName].contextWindow).toBe(64000); }); +test("reconnect reserves a hidden draft when twenty accounts are already saved", async () => { + const f = accountFixture(); + let firstId = ""; + for (let i = 0; i < 20; i++) { + f.hash(i.toString(16).padStart(64, "0")); + const account = await f.login({ label: `Account ${i}` }); + await f.call("complete", { jobId: account.jobId }); + firstId ||= account.accountId; + } + expect(listAccounts()).toHaveLength(20); + f.hash("0".repeat(64)); + const reconnect = await f.login({ accountId: firstId }); + expect(reconnect).not.toHaveProperty("error"); + expect(listAccounts()).toHaveLength(20); + expect(await (await f.call("complete", { jobId: reconnect.jobId })).json()).toMatchObject({ activation: "ready", accountId: firstId }); + expect(listAccounts()).toHaveLength(20); + expect(await (await f.call("login", { label: "Overflow", runtime: "/runtime", workspace: "/project" })).json()) + .toMatchObject({ error: "account_limit" }); +}); test("partial catalog activation retries without login; busy and referenced accounts cannot be removed", async () => { const f = accountFixture(), a = await f.login(); f.failCatalog(true); @@ -309,11 +328,12 @@ test("failed official OAuth has a safe error and cancel removes the draft only", test("concurrent completions cannot register the same identity twice", async () => { const f = accountFixture(), a = await f.login(), b = await f.login(); const connect = f.deps.connectDesktop; - let release!: () => void; + let release!: () => void, entered!: () => void; const gate = new Promise(resolve => { release = resolve; }); - f.deps.connectDesktop = async (...args) => { await gate; return connect(...args); }; + const firstEntered = new Promise(resolve => { entered = resolve; }); + f.deps.connectDesktop = async (...args) => { entered(); await gate; return connect(...args); }; const first = f.call("complete", {jobId:a.jobId}); - await new Promise(resolve => setTimeout(resolve, 5)); + await firstEntered; expect(await (await f.call("complete", {jobId:b.jobId})).json()).toMatchObject({error:"account_busy"}); release(); expect(await (await first).json()).toMatchObject({activation:"ready"}); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 563aa566e2..3b5d540a67 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -9,7 +9,9 @@ import { readZcodeModels } from "../../src/adapters/zcode/settings"; import { parseNativeOAuthEvent, nativeOAuthCommand } from "../../src/adapters/zcode/native-oauth"; import { resolveDesktopNode } from "../../src/adapters/zcode/desktop-node"; -const { normalizeDesktopConfig, desktopModelCatalog } = createRequire(import.meta.url)("../../src/adapters/zcode/desktop-bootstrap.cjs"); +const require = createRequire(import.meta.url); +const { normalizeDesktopConfig, desktopModelCatalog } = require("../../src/adapters/zcode/desktop-bootstrap.cjs"); +const { materializeSession, subjectHash } = require("../../src/adapters/zcode/oauth-bootstrap.cjs"); let root: string; let previousHome: string | undefined; let previousSandbox: string | undefined; @@ -85,6 +87,15 @@ describe("managed ZCode Desktop", () => { const managed = join(linkedConfig, "zcode-desktop", "workspace"); expect(validateDesktopWorkspace(managed)).toBe(join(realConfig, "zcode-desktop", "workspace")); }); + test("sandbox allows only the matching account-managed workspace below the config root", async () => { + const { allocateAccount } = await import("../../src/adapters/zcode/accounts"); + const account = allocateAccount("Personal"); + const managed = join(process.env.OPENCODEX_HOME!, "zcode-accounts", account.id, "workspace"); + expect(validateDesktopWorkspace(managed, account.id)).toBe(managed); + expect(() => validateDesktopWorkspace(managed)).toThrow("workspace_invalid"); + const other = allocateAccount("Work"); + expect(() => validateDesktopWorkspace(managed, other.id)).toThrow("workspace_invalid"); + }); test("disconnect persists revocation instead of falling back to operator env", async () => { expect(loadDesktopSettings()).toBeUndefined(); await disconnectDesktop(); @@ -142,6 +153,18 @@ test("native OAuth emits only bounded public events, never tokens or vendor erro expect(() => parseNativeOAuthEvent({ type: "tokens", accessToken: "secret" })).toThrow("native_oauth_failed"); }); +test("refresh validates restored identity before model-provider profile mutation", async () => { + const profile = join(root, "refresh-profile"); mkdirSync(profile, { mode: 0o700 }); + const sentinel = join(profile, "sentinel"); writeFileSync(sentinel, "unchanged"); + const result = { kind: "session", provider: "zai", userInfo: { id: "different-user" } }; + const calls: string[] = []; + const models = { call: async (method: string) => { calls.push(method); writeFileSync(sentinel, "mutated"); } }; + await expect(materializeSession(result, models, "a".repeat(64))).rejects.toThrow("account_identity_mismatch"); + expect(calls).toEqual([]); + expect(readFileSync(sentinel, "utf8")).toBe("unchanged"); + expect(subjectHash(result)).toMatch(/^[a-f0-9]{64}$/); +}); + test("native OAuth sandbox refuses the live HOME and symlink profiles", () => { if (process.platform !== "linux") return; expect(() => nativeOAuthCommand("/not/used", homedir(), "login")).toThrow("profile_invalid"); From 2da552ac1224db92ffd648e243efe9984fd5336e Mon Sep 17 00:00:00 2001 From: FacuM Date: Fri, 11 Sep 2026 18:43:27 -0300 Subject: [PATCH 13/43] fix(zcode): expose only supported reasoning levels --- src/adapters/zcode/adapter.ts | 7 ++++-- src/adapters/zcode/reasoning.ts | 36 +++++++++++++++++++++++++++ src/codex/catalog/provider-fetch.ts | 25 +++++++++++++------ tests/providers/zcode-adapter.test.ts | 14 +++++++++++ tests/providers/zcode-desktop.test.ts | 30 ++++++++++++++++++++++ 5 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 src/adapters/zcode/reasoning.ts diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index f8a6401b44..89873f08fa 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -4,6 +4,7 @@ import type { OcxParsedRequest, OcxProviderConfig } from "../../types"; import type { ProviderAdapter } from "../base"; import { ZcodeClient } from "./client"; import { loadZcodeSettings, readZcodeModels, record, type JsonObject, type ZcodeSettings } from "./settings"; +import { zcodeThoughtLevel } from "./reasoning"; type Client = Pick; export interface ZcodeAdapterDeps { @@ -112,6 +113,8 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt const modelParams = settings.desktopModels ? { _zcodeModel: { providerId: model.providerId, modelId: model.modelId } } : { runtimeModel: model.runtimeModel }; + const thoughtLevel = zcodeThoughtLevel(model.modelId, parsed.options.reasoning); + const thoughtParams = thoughtLevel ? { thoughtLevel } : {}; const controller = Promise.withResolvers(); // Attach a handler immediately: failures can happen during session materialization. void controller.promise.catch(() => {}); @@ -147,11 +150,11 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt } }; if (sessionId) { - await active.request("session/resume", { sessionId, ...modelParams }); + await active.request("session/resume", { sessionId, ...modelParams, ...thoughtParams }); } else { const result = await active.request("session/create", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace }, - mode: "edit", ...modelParams, titleGenerationEnabled: false, + mode: "edit", ...modelParams, ...thoughtParams, titleGenerationEnabled: false, mcpServers: [], toolDenylist: ["Task", "TaskOutput", "TaskStop"], }); const id = record(result.session).sessionId; diff --git a/src/adapters/zcode/reasoning.ts b/src/adapters/zcode/reasoning.ts new file mode 100644 index 0000000000..ac0cfc7740 --- /dev/null +++ b/src/adapters/zcode/reasoning.ts @@ -0,0 +1,36 @@ +/** + * ZCode's current GLM-5.3 family exposes three effective thought levels. `ultra` + * remains a Codex orchestration tier: codex-rs delegates at `max`, while the + * official Desktop runtime receives only a real ZCode level. + */ +export const ZCODE_GLM53_REASONING_EFFORTS = ["low", "high", "max"] as const; + +export interface ZcodeReasoningContract { + reasoningEfforts: string[]; + defaultReasoningEffort: string; +} + +/** Match the native model component while retaining the Desktop provider prefix. */ +export function isZcodeGlm53Model(modelId: string): boolean { + const native = modelId.trim().toLowerCase().split("/").at(-1); + return native === "glm-5.3" || native === "glm-5.3[1m]" || native === "glm-5.3-flash"; +} + +/** Undefined means that ZCode has no audited Codex effort contract for this model. */ +export function zcodeReasoningContract(modelId: string): ZcodeReasoningContract | undefined { + if (!isZcodeGlm53Model(modelId)) return undefined; + return { reasoningEfforts: [...ZCODE_GLM53_REASONING_EFFORTS], defaultReasoningEffort: "max" }; +} + +/** + * Normalize direct/stale client labels to the three values accepted by the + * official Desktop runtime. The normal Codex path already changes ultra to max; + * handling it here as well keeps the app-server boundary fail-safe. + */ +export function zcodeThoughtLevel(modelId: string, requested: string | undefined): string | undefined { + if (!requested || !isZcodeGlm53Model(modelId)) return undefined; + if (requested === "low" || requested === "minimal") return "low"; + if (requested === "high" || requested === "medium") return "high"; + if (requested === "max" || requested === "xhigh" || requested === "ultra") return "max"; + return undefined; +} diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 0d42bef1d2..5ddba940f6 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -1,4 +1,5 @@ import { discoverZcodeModels } from "../../adapters/zcode/settings"; +import { zcodeReasoningContract } from "../../adapters/zcode/reasoning"; import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases"; import { initialModelSelectionPending } from "../../providers/initial-model-selection"; import { execFileSync } from "node:child_process"; @@ -1659,14 +1660,22 @@ async function fetchProviderModelsWithAuth( ); if (prov.adapter === "zcode") { try { - const models = discoverZcodeModels(prov.zcodeAccountId).map(model => ({ - id: model.id, provider: name, - ...catalogHintsFromProviderConfig(name, prov, model.id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias), - displayName: model.label, - ...(model.contextWindow ? { contextWindow: typeof contextCap === "number" && contextCap > 0 - ? Math.min(model.contextWindow, contextCap) : model.contextWindow } : {}), - inputModalities: ["text"], - } as CatalogModel)); + const models = discoverZcodeModels(prov.zcodeAccountId).map(model => { + const hints = catalogHintsFromProviderConfig( + name, prov, model.id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias, + ); + const reasoning = zcodeReasoningContract(model.id); + return { + id: model.id, provider: name, ...hints, + // The official Desktop catalog is authoritative for current GLM-5.3 levels. Unknown + // future models expose no generic picker unless the operator configured one explicitly. + ...(reasoning ?? (hints.reasoningEfforts === undefined ? { reasoningEfforts: [] } : {})), + displayName: model.label, + ...(model.contextWindow ? { contextWindow: typeof contextCap === "number" && contextCap > 0 + ? Math.min(model.contextWindow, contextCap) : model.contextWindow } : {}), + inputModalities: ["text"], + } as CatalogModel; + }); return observed(withConfiguredRetention(models), "authoritative"); } catch { return observed([], "degraded"); diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 44050177aa..7ed34c6885 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -86,6 +86,20 @@ describe("ZCode local agent", () => { expect(serialized).not.toContain("apiKey"); } }); + test("maps Codex effort labels to the official GLM-5.3 thought levels", async () => { + const settings = { ...fixture(), desktopModels: [{ + id: "builtin:zai-coding-plan/GLM-5.3", providerId: "builtin:zai-coding-plan", + modelId: "GLM-5.3", label: "GLM-5.3", + }] }; + for (const [requested, expected] of [["low", "low"], ["medium", "high"], ["high", "high"], + ["xhigh", "max"], ["max", "max"], ["ultra", "max"]] as const) { + const client = new FakeClient(); + const parsed = request(); parsed.modelId = settings.desktopModels[0]!.id; parsed.options.reasoning = requested; + expect((await run(settings, client, parsed)).at(-1)?.type).toBe("done"); + expect(client.calls.find(call => call.method === "session/create")?.params.thoughtLevel).toBe(expected); + expect(client.calls.find(call => call.method === "session/send")?.params.thoughtLevel).toBeUndefined(); + } + }); test("a revoked or changed managed connection cannot start a queued child", async () => { const settings = { ...fixture(), desktopModels: [{ id: "test/model", providerId: "test", modelId: "model", label: "Model" }] }; let reads = 0; let children = 0; diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 3b5d540a67..558710a2a5 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -287,3 +287,33 @@ test("ZCode catalog advertises sidecar-backed attachments for legacy and account expect(result.inputModalities).toContain("image"); } }); + +test("ZCode GLM-5.3 catalog exposes only real levels plus Codex ultra", async () => { + const { gatherRoutedModels } = await import("../../src/codex/catalog/provider-fetch"); + const { buildCatalogEntries } = await import("../../src/codex/catalog/sync"); + process.env.OCX_ZCODE_SANDBOX = "0"; + const runtimeRoot = join(root, "app"), runtime = join(runtimeRoot, "resources/glm/zcode.cjs"); + mkdirSync(join(runtimeRoot, "resources/glm"), { recursive: true }); + writeFileSync(join(runtimeRoot, "resources/app.asar"), "fixture"); writeFileSync(runtime, ""); + const workspace = join(root, "workspace"), connectionDir = join(process.env.OPENCODEX_HOME!, "zcode-desktop"); + mkdirSync(workspace); mkdirSync(connectionDir, { recursive: true }); + const ids = ["builtin:zai-coding-plan/GLM-5.3", "builtin:zai-coding-plan/GLM-5.3-Flash"]; + writeFileSync(join(connectionDir, "connection.json"), JSON.stringify({ + version: 1, connected: true, generation: crypto.randomUUID(), runtime, workspace, + models: ids.map(id => ({ id, providerId: "builtin:zai-coding-plan", modelId: id.split("/")[1], label: id })), + }), { mode: 0o600 }); + const providerName = `zcode-fixture-${crypto.randomUUID()}`; + const models = await gatherRoutedModels({ port: 0, defaultProvider: providerName, providers: { + [providerName]: { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", liveModels: true }, + } }); + for (const modelId of ids) { + const model = models.find(row => row.provider === providerName && row.id === modelId); + expect(model?.reasoningEfforts).toEqual(["low", "high", "max"]); + const [entry] = buildCatalogEntries(null, [], [model!]); + const efforts = (entry?.supported_reasoning_levels as Array<{ effort: string }>).map(level => level.effort); + expect(efforts).toEqual(["low", "high", "max", "ultra"]); + expect(efforts).not.toContain("medium"); + expect(efforts).not.toContain("xhigh"); + expect(entry?.default_reasoning_level).toBe("max"); + } +}); From 44efbef8e121134966a1b85970656b93928c308a Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 01:43:34 -0300 Subject: [PATCH 14/43] fix(zcode): accept replayed tool history --- src/adapters/zcode/adapter.ts | 52 ++++++++++++++++++++++----- tests/providers/zcode-adapter.test.ts | 32 +++++++++++++++-- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 89873f08fa..75aea7da51 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -1,6 +1,6 @@ import { refreshAccount } from "./account-runtime"; import { createHash } from "node:crypto"; -import type { OcxParsedRequest, OcxProviderConfig } from "../../types"; +import type { OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig } from "../../types"; import type { ProviderAdapter } from "../base"; import { ZcodeClient } from "./client"; import { loadZcodeSettings, readZcodeModels, record, type JsonObject, type ZcodeSettings } from "./settings"; @@ -45,17 +45,51 @@ async function lock(key: string, signal?: AbortSignal): Promise<() => void> { return release; } +function textualContent(content: string | OcxContentPart[]): string { + if (typeof content === "string") return content; + return content.map(part => { + if (part.type === "text") return part.text; + if (part.type === "image") { + throw new Error("ZCode image input was not converted to text before native dispatch."); + } + throw new Error("ZCode video input is unsupported by the native Desktop bridge."); + }).join("\n"); +} + +function safeLabel(value: string): string { + return value.replace(/\s+/g, " ").trim().slice(0, 128) || "unknown"; +} + +/** + * Project Responses history onto the app-server's text-only `session/send` wire. + * + * Starting a ZCode session after another provider means the replay can legitimately contain + * assistant reasoning, tool calls and their results. Those are history, not new client tool + * instructions: omit hidden reasoning, render calls as inert labels, and keep textual results. + * Images must already have been described/stripped by the shared vision pipeline. + */ +function transcriptLine(message: OcxMessage): string | undefined { + if (message.role === "assistant") { + const content = message.content.flatMap(part => { + if (part.type === "text") return part.text ? [part.text] : []; + if (part.type === "toolCall") return [`[historical tool call: ${safeLabel(part.name)}]`]; + // Never disclose or replay another provider's hidden chain of thought. + return []; + }).join("\n"); + return content.trim() ? `assistant: ${content}` : undefined; + } + if (message.role === "toolResult") { + const content = textualContent(message.content); + const status = message.isError ? " error" : " result"; + return `tool ${safeLabel(message.toolName)}${status}: ${content}`; + } + return `${message.role}: ${textualContent(message.content)}`; +} + function textInput(parsed: OcxParsedRequest, resumed: boolean): string { const messages = parsed.context.messages; const current = resumed ? messages.slice(parsed._continuationConversationMessageIndex ?? -1) : messages; - const lines = current.map(message => { - if (message.role === "toolResult") throw new Error("ZCode executes its own tools; client tool results are unsupported."); - const content = typeof message.content === "string" ? message.content : message.content.map(part => { - if (part.type !== "text") throw new Error("ZCode bridge currently accepts text only."); - return part.text; - }).join("\n"); - return `${message.role}: ${content}`; - }); + const lines = current.map(transcriptLine).filter((line): line is string => line !== undefined); const prompt = [...(parsed.context.systemPrompt ?? []), ...lines].join("\n\n"); if (!prompt.trim() || prompt.length > 200_000) throw new Error("ZCode input is empty or exceeds the bridge limit."); return prompt; diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 7ed34c6885..485b3f7264 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -180,6 +180,29 @@ describe("ZCode local agent", () => { const other = new FakeClient(); await run(fixture(), other, next); expect(other.calls[0]?.method).toBe("session/create"); }); + test("a fresh ZCode session safely projects replayed reasoning and tool history", async () => { + const parsed = request(); + parsed.context.messages = [ + { role: "user", content: "Inspect the repository", timestamp: 0 }, + { role: "assistant", timestamp: 1, content: [ + { type: "thinking", thinking: "private reasoning must not cross providers" }, + { type: "toolCall", id: "call_old", name: "exec_command", arguments: { command: "pwd" } }, + ] }, + { role: "toolResult", toolCallId: "call_old", toolName: "exec_command", + content: "/workspace", isError: false, timestamp: 2 }, + { role: "assistant", content: [{ type: "text", text: "The repository is available." }], timestamp: 3 }, + { role: "user", content: "Continue with the fix", timestamp: 4 }, + ]; + const client = new FakeClient(); + expect((await run(fixture(), client, parsed)).at(-1)?.type).toBe("done"); + const sent = String(client.calls.find(call => call.method === "session/send")?.params.content); + expect(sent).toContain("[historical tool call: exec_command]"); + expect(sent).toContain("tool exec_command result: /workspace"); + expect(sent).toContain("assistant: The repository is available."); + expect(sent).toContain("user: Continue with the fix"); + expect(sent).not.toContain("private reasoning"); + expect(sent).not.toContain('"command":"pwd"'); + }); test("post-send failures are non-retryable incomplete, not failover candidates", async () => { const client = new FakeClient("failed"); const events = await run(fixture(), client); expect(events.at(-1)).toMatchObject({ type: "incomplete", reason: "zcode_agent_interrupted", retryable: false }); @@ -214,10 +237,13 @@ describe("ZCode local agent", () => { const client = new FakeClient(); const events = await run(fixture(), client, request(), AbortSignal.abort()); expect(events.at(-1)?.type).toBe("error"); expect(client.calls).toHaveLength(0); }); - test("rejects images and client tool results instead of silently losing them", async () => { + test("rejects residual media that escaped text-only normalization", async () => { const parsed = request(); parsed.context.messages = [{ role: "user", timestamp: 0, - content: [{ type: "image", url: "https://example.invalid/image.png" }] }]; - const client = new FakeClient(); expect((await run(fixture(), client, parsed)).at(-1)?.type).toBe("error"); + content: [{ type: "image", imageUrl: "https://example.invalid/image.png" }] }]; + const client = new FakeClient(); + expect((await run(fixture(), client, parsed)).at(-1)).toMatchObject({ + type: "error", message: "ZCode image input was not converted to text before native dispatch.", + }); expect(client.calls).toHaveLength(0); }); }); From bf0630c6ae1190989be3d3d45d40a8938ca86a97 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 02:07:08 -0300 Subject: [PATCH 15/43] fix(zcode): bound replayed conversation history --- src/adapters/zcode/adapter.ts | 45 ++++++++++++++++++++++++--- tests/providers/zcode-adapter.test.ts | 19 +++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 75aea7da51..6bc00bb0d6 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -7,6 +7,8 @@ import { loadZcodeSettings, readZcodeModels, record, type JsonObject, type Zcode import { zcodeThoughtLevel } from "./reasoning"; type Client = Pick; +const MAX_ZCODE_INPUT_CHARS = 200_000; +const HISTORY_TRUNCATED = "[Earlier OpenCodex conversation history truncated to fit the ZCode bridge.]"; export interface ZcodeAdapterDeps { settings?: () => ZcodeSettings; client?: (settings: ZcodeSettings) => Client; @@ -86,12 +88,47 @@ function transcriptLine(message: OcxMessage): string | undefined { return `${message.role}: ${textualContent(message.content)}`; } +function transcriptLines(messages: OcxMessage[]): string[] { + return messages.map(transcriptLine).filter((line): line is string => line !== undefined && line.trim().length > 0); +} + +function joinPrompt(lines: string[]): string { + return lines.join("\n\n"); +} + +/** Retain the current turn and newest complete history entries within the native bridge cap. */ +function boundedPrompt(system: string[], history: string[], current: string[]): string { + const full = joinPrompt([...system, ...history, ...current]); + if (full.length <= MAX_ZCODE_INPUT_CHARS) return full; + + const fixed = [...system, HISTORY_TRUNCATED, ...current]; + const fixedPrompt = joinPrompt(fixed); + if (fixedPrompt.length > MAX_ZCODE_INPUT_CHARS) { + throw new Error("ZCode current input and system instructions exceed the native bridge limit."); + } + + const retained: string[] = []; + let length = fixedPrompt.length; + for (let index = history.length - 1; index >= 0; index--) { + const line = history[index]!; + // Inserting one complete history line adds its content and one prompt separator. + const added = line.length + 2; + if (length + added > MAX_ZCODE_INPUT_CHARS) break; + retained.unshift(line); + length += added; + } + return joinPrompt([...system, HISTORY_TRUNCATED, ...retained, ...current]); +} + function textInput(parsed: OcxParsedRequest, resumed: boolean): string { const messages = parsed.context.messages; - const current = resumed ? messages.slice(parsed._continuationConversationMessageIndex ?? -1) : messages; - const lines = current.map(transcriptLine).filter((line): line is string => line !== undefined); - const prompt = [...(parsed.context.systemPrompt ?? []), ...lines].join("\n\n"); - if (!prompt.trim() || prompt.length > 200_000) throw new Error("ZCode input is empty or exceeds the bridge limit."); + const continuation = parsed._continuationConversationMessageIndex; + const boundary = continuation ?? Math.max(0, messages.length - 1); + const history = resumed ? [] : transcriptLines(messages.slice(0, boundary)); + const current = transcriptLines(messages.slice(resumed ? continuation ?? -1 : boundary)); + const system = (parsed.context.systemPrompt ?? []).filter(line => line.trim().length > 0); + const prompt = boundedPrompt(system, history, current); + if (!prompt.trim()) throw new Error("ZCode input is empty."); return prompt; } diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 485b3f7264..9e2520f05b 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -203,6 +203,25 @@ describe("ZCode local agent", () => { expect(sent).not.toContain("private reasoning"); expect(sent).not.toContain('"command":"pwd"'); }); + test("a fresh session truncates oldest replay history without dropping the current turn", async () => { + const parsed = request(); + parsed._continuationConversationMessageIndex = 2; + parsed.context.systemPrompt = ["SYSTEM_SENTINEL"]; + parsed.context.messages = [ + { role: "user", content: `OLDEST_${"a".repeat(150_000)}`, timestamp: 0 }, + { role: "assistant", content: [{ type: "text", text: `NEWEST_${"b".repeat(70_000)}` }], timestamp: 1 }, + { role: "user", content: "CURRENT_REQUEST_SENTINEL", timestamp: 2 }, + ]; + const client = new FakeClient(); + expect((await run(fixture(), client, parsed)).at(-1)?.type).toBe("done"); + const sent = String(client.calls.find(call => call.method === "session/send")?.params.content); + expect(sent.length).toBeLessThanOrEqual(200_000); + expect(sent).toContain("SYSTEM_SENTINEL"); + expect(sent).toContain("CURRENT_REQUEST_SENTINEL"); + expect(sent).toContain("NEWEST_"); + expect(sent).not.toContain("OLDEST_"); + expect(sent).toContain("Earlier OpenCodex conversation history truncated"); + }); test("post-send failures are non-retryable incomplete, not failover candidates", async () => { const client = new FakeClient("failed"); const events = await run(fixture(), client); expect(events.at(-1)).toMatchObject({ type: "incomplete", reason: "zcode_agent_interrupted", retryable: false }); From 4d3bd07c8d937b7cd2dcfc36704ee42c09fb1e63 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 02:35:18 -0300 Subject: [PATCH 16/43] fix(zcode): restore native host tool access --- .../src/content/docs/guides/zcode-agent.md | 19 +++++++----- gui/src/components/ZcodeAccountsPane.tsx | 2 +- gui/tests/zcode-desktop-pane.test.tsx | 30 +++++++++++++++++++ src/adapters/zcode/adapter.ts | 19 ++++++++---- src/adapters/zcode/client.ts | 29 ++++++++++++++++-- src/adapters/zcode/desktop-bootstrap.cjs | 29 ++++++++++-------- src/adapters/zcode/desktop.ts | 6 ++-- src/adapters/zcode/settings.ts | 4 +++ src/codex/catalog/provider-fetch.ts | 10 ++++--- src/server/management/provider-routes.ts | 6 ++-- src/server/management/route-registry.ts | 22 +++++++++----- src/server/management/zcode-desktop-routes.ts | 9 +++--- structure/adapters/registry.md | 7 ++++- structure/transports/inventory.md | 4 ++- tests/providers/zcode-adapter.test.ts | 18 +++++++++-- tests/providers/zcode-desktop.test.ts | 18 ++++++++--- tests/providers/zcode-transport.test.ts | 24 +++++++++++++-- 17 files changed, 197 insertions(+), 59 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 9c6e36a052..b78de58d7a 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -126,7 +126,10 @@ permissions of the proxy's operating-system user. It can read and modify files o selected working directory, including sensitive files accessible to that user. Absolute paths retain their host meaning. The selected workspace is a starting directory, not a filesystem boundary. This default also applies to previously connected installations after -upgrading. No root elevation or permission-skipping flag is used. +upgrading. Managed connections start ZCode in its non-interactive `yolo` permission mode so +native commands can actually use that host access without stopping for an approval prompt that +the OpenCodex protocol cannot display. This changes ZCode's approval policy, not operating-system +permissions: there is no root elevation or sandbox bypass. Any isolation must be applied by the harness/operator **where the native ZCode process executes**. A sandbox in a remote calling client does not automatically constrain this @@ -235,9 +238,10 @@ rewritten. **Test connection** checks the local catalog only, not account entitl - Native execution requires either a persisted, explicitly consented Desktop connection or all four advanced environment settings. Disconnecting revokes managed execution. -- Sessions use ZCode's `edit` permission mode. Interactive permissions are denied; user-input - requests are cancelled rather than answered automatically. Continue such work in the isolated - ZCode client. No permission-skipping flag is passed. +- Managed Desktop sessions use ZCode's non-interactive `yolo` permission mode after the explicit + connection consent. Advanced operator launchers retain `edit` mode. Interactive permission + requests are still denied, and user-input requests are cancelled rather than answered + automatically; continue those workflows in the ZCode client itself. - Each turn owns a child process. A profile has one active turn at a time because ZCode writes model selection to shared settings; at most 32 active/queued bridge calls are admitted. - `previous_response_id` uses OpenCodex's owner-fenced private continuation state. Client thread @@ -253,9 +257,10 @@ rewritten. **Test connection** checks the local catalog only, not account entitl - Vendor errors and stderr are not forwarded. No raw credentials or protocol traces are included in discovery/management results. Token usage is currently unavailable, not measured as zero; model access still consumes your ZCode account quota. -- Linux was tested. Windows/macOS require an equivalent isolated launcher and are not yet - live-validated. Streaming reasoning is supported, but caller-selected reasoning levels are - not yet advertised or mapped; ZCode owns the model's configured defaults. +- Linux was tested. Windows/macOS require an equivalent advanced launcher and are not yet + live-validated. GLM-5.3 and GLM-5.3-Flash advertise and map ZCode's `low`, `high`, and `max` + thought levels; Codex also offers `ultra` as its multi-agent/max orchestration tier. Unknown + future models expose no generic reasoning picker unless the operator configures one. ## Verification diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx index 61f49b2dad..93486f7ad1 100644 --- a/gui/src/components/ZcodeAccountsPane.tsx +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -18,7 +18,7 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace }: { api method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...body, consent: true }), } : undefined); const value = await response.json(); - if (!response.ok || value.error) throw new Error(value.error || "native_oauth_failed"); + if (!response.ok) throw new Error(value.error || "native_oauth_failed"); return value; }, [apiBase]); const refresh = useCallback(async () => { diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 360488c5e1..7a61727ca7 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -236,3 +236,33 @@ test("saved account UI completes official login then shows provider/catalog read expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); expect(requests.filter(r => r.body).every(r => r.body!.consent === true)).toBe(true); }); + +test("saved account completion preserves an HTTP-200 partial activation", async () => { + let completed = false; + const prior = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { + const url = new URL(String(input), "http://localhost"); + if (!url.pathname.startsWith("/api/zcode-accounts")) return prior(input, options); + if (url.pathname.endsWith("/login")) return Response.json({ jobId: "partial-job", accountId: "partial-account", + phase: options?.method === "POST" ? "waiting" : "authenticated" }); + if (url.pathname.endsWith("/complete")) { + completed = true; + return Response.json({ activation: "catalog_pending", error: "catalog_update_failed" }); + } + return Response.json({ accounts: completed ? [{ id: "partial-account", label: "Partial fixture", + activation: "catalog_pending", busy: false }] : [] }); + } }); + await mountPane(); + const section = host.querySelector("h3")!.closest("section")!; + const input = section.querySelector('input:not([type="checkbox"])') as HTMLInputElement; + await act(async () => { + Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value")!.set!.call(input, "Partial fixture"); + input.dispatchEvent(new win.Event("input", { bubbles: true })); + }); + await click(section.querySelector('input[type="checkbox"]')!); + await click(button("Add account")); + await act(async () => { await new Promise(resolve => setTimeout(resolve, 2200)); }); + expect(host.textContent).toContain("Partial fixture"); + expect(host.textContent).toContain("catalog_update_failed"); + expect(host.textContent).not.toContain("native_oauth_failed"); +}); diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 6bc00bb0d6..d757305847 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -179,7 +179,8 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt const content = textInput(parsed, Boolean(sessionId)); client = (deps.client ?? (s => new ZcodeClient(s)))(settings); const active = client; - // The Desktop bootstrap resolves credentials INSIDE the sandbox. The parent only sends + // The Desktop bootstrap resolves credentials inside the official runtime (and the optional + // sandbox when enabled). The parent only sends // public model identity; it never receives Desktop API keys or synthesizes vendor auth. const modelParams = settings.desktopModels ? { _zcodeModel: { providerId: model.providerId, modelId: model.modelId } } @@ -214,10 +215,14 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt text: payload.kind === "started" ? "\n[ZCode: native tool started]\n" : "\n[ZCode: native tool finished]\n", phase: "commentary" }); } else if (params.type === "turn.completed") { - if (!textSeen && typeof payload.response === "string") emit({ type: "text_delta", text: payload.response, phase: "final_answer" }); - controller.resolve(); + if (!textSeen && typeof payload.response === "string" && payload.response.length > 0) { + textSeen = true; + emit({ type: "text_delta", text: payload.response, phase: "final_answer" }); + } + if (textSeen) controller.resolve(); + else controller.reject(new Error("ZCode completed without a model answer.")); } else if (params.type === "turn.failed") { - controller.reject(new Error("ZCode agent turn failed. Inspect the isolated client for authentication, quota or model errors.")); + controller.reject(new Error("ZCode agent turn failed. Inspect the interactive ZCode client for authentication, quota or model errors.")); } }; if (sessionId) { @@ -225,7 +230,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt } else { const result = await active.request("session/create", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace }, - mode: "edit", ...modelParams, ...thoughtParams, titleGenerationEnabled: false, + mode: settings.nativePermissionMode ?? "edit", ...modelParams, ...thoughtParams, titleGenerationEnabled: false, mcpServers: [], toolDenylist: ["Task", "TaskOutput", "TaskStop"], }); const id = record(result.session).sessionId; @@ -236,7 +241,9 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt if (incoming.abortSignal?.aborted) throw new Error("ZCode request cancelled before dispatch."); // Commit visible output before dispatch, so streaming combo routing cannot replay an // accepted task that already changed files. Post-send failure is non-retryable incomplete. - emit({ type: "text_delta", text: "[ZCode: running in the configured isolated workspace]\n", phase: "commentary" }); + emit({ type: "text_delta", text: settings.hostExecution + ? "[ZCode: running native tools with host-user access]\n" + : "[ZCode: running native tools in the configured launcher]\n", phase: "commentary" }); sent = true; await active.request("session/send", { sessionId, content, ...modelParams }); await controller.promise; diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index cfa18364b4..8a76463928 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -1,10 +1,34 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { homedir } from "node:os"; import { readJsonLines } from "../coding-agent/protocol"; import { record, type JsonObject, type ZcodeSettings } from "./settings"; import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks"; export type ZcodeSpawn = typeof spawn; type Pending = { resolve: (value: JsonObject) => void; reject: (error: Error) => void; timer: ReturnType }; +const HOST_TOOL_ENV = new Set([ + "ALL_PROXY", "all_proxy", "BROWSER", "COLORTERM", "DBUS_SESSION_BUS_ADDRESS", "DISPLAY", + "GIT_ASKPASS", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_SSH", "GIT_SSH_COMMAND", + "GH_CONFIG_DIR", "GPG_AGENT_INFO", "GNUPGHOME", "GPG_TTY", "HTTPS_PROXY", "https_proxy", + "HTTP_PROXY", "http_proxy", "LANG", "LC_ALL", "LOGNAME", "NODE_EXTRA_CA_CERTS", "NO_COLOR", + "NO_PROXY", "no_proxy", "PATH", "SHELL", "SSH_AGENT_PID", "SSH_ASKPASS", "SSH_ASKPASS_REQUIRE", + "SSH_AUTH_SOCK", "SSL_CERT_DIR", "SSL_CERT_FILE", "TERM", "TMPDIR", "TZ", "USER", "WAYLAND_DISPLAY", + "XAUTHORITY", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_RUNTIME_DIR", "XDG_STATE_HOME", +]); + +/** Host native tools need the user's normal config/agent sockets, but never unrelated provider secrets. */ +export function zcodeChildEnvironment(settings: ZcodeSettings, source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { + if (!settings.hostExecution) return { HOME: settings.home, PATH: source.PATH ?? "/usr/bin:/bin", + XDG_CONFIG_HOME: `${settings.home}/.config`, XDG_CACHE_HOME: `${settings.home}/.cache` }; + const result: NodeJS.ProcessEnv = {}; + for (const [key, value] of Object.entries(source)) { + if (value !== undefined && (HOST_TOOL_ENV.has(key) || key.startsWith("LC_"))) result[key] = value; + } + result.HOME = homedir(); + result.PATH = source.PATH ?? "/usr/bin:/bin"; + result.ZCODE_DATA_BASE_DIR = settings.home; + return result; +} const desktopClients = new Set(); export const hasZcodeAccountClients = (id: string) => [...desktopClients].some(client => client.accountId === id); export async function closeZcodeDesktopClients(): Promise { @@ -30,8 +54,7 @@ export class ZcodeClient { const [command, ...args] = settings.command; this.child = spawnProcess(command!, [...args, "app-server"], { cwd: settings.home, shell: false, stdio: ["pipe", "pipe", "pipe"], - env: { HOME: settings.home, PATH: process.env.PATH ?? "/usr/bin:/bin", - XDG_CONFIG_HOME: `${settings.home}/.config`, XDG_CACHE_HOME: `${settings.home}/.cache` }, + env: zcodeChildEnvironment(settings), }) as ChildProcessWithoutNullStreams; this.exited = new Promise(resolve => { this.child.once("exit", () => resolve()); @@ -81,7 +104,7 @@ export class ZcodeClient { this.write({ id: message.id, result: { decision: "deny", reason: "Interactive approval is unavailable through this bridge." } }); } else if (message.method === "interaction/requestUserInput") { this.write({ id: message.id, result: { action: "cancel" } }); - this.fail(new Error("ZCode requires user input; continue in the isolated ZCode client.")); + this.fail(new Error("ZCode requires user input; continue in the interactive ZCode client.")); } else { this.write({ id: message.id, error: { code: -32601, message: "Unsupported ZCode client request." } }); } diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs index 4772d147f6..fdf3689bb1 100644 --- a/src/adapters/zcode/desktop-bootstrap.cjs +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -40,29 +40,34 @@ function desktopModelCatalog(config) { } module.exports = { normalizeDesktopConfig, desktopModelCatalog }; if (require.main === module) { - let temporaryHome; - const cleanup = () => { if (temporaryHome) fs.rmSync(temporaryHome, { recursive: true, force: true }); }; + let temporarySettings; + const cleanup = () => { if (temporarySettings) fs.rmSync(temporarySettings, { recursive: true, force: true }); }; process.on("exit", cleanup); try { const args = process.argv.slice(2); const host = args[0] === "--host"; - if (host ? args.length !== 5 || args[4] !== "app-server" : args.length !== 1 || args[0] !== "app-server") throw new Error("unsupported command"); + if (host ? args.length !== 6 || args[5] !== "app-server" : args.length !== 1 || args[0] !== "app-server") throw new Error("unsupported command"); const path = host ? args[2] : "/desktop/config.json"; if (fs.statSync(path).size > 4 * 1024 * 1024) throw new Error("oversized"); const input = JSON.parse(fs.readFileSync(path, "utf8")); const config = normalizeDesktopConfig(input); let env = process.env; + let settingsPath = `${env.HOME}/.zcode/cli/config.json`; if (host) { - // Separate runtime state is not filesystem confinement: native tools retain host access. - const stableDb = require("node:path").join(process.env.HOME, ".zcode/cli/db"); - temporaryHome = fs.mkdtempSync(require("node:path").join(process.env.HOME, "turn-")); - fs.mkdirSync(temporaryHome + "/.zcode/cli", { recursive: true, mode: 0o700 }); - fs.symlinkSync(stableDb, temporaryHome + "/.zcode/cli/db", "dir"); - env = { ...process.env, HOME: temporaryHome, XDG_CONFIG_HOME: temporaryHome + "/.config", - XDG_CACHE_HOME: temporaryHome + "/.cache" }; + // Keep only ZCode-owned state private. HOME/XDG remain the real host environment so + // native tools behave like ordinary host tools rather than an accidental soft sandbox. + const paths = require("node:path"); + const stateHome = args[4]; + if (!paths.isAbsolute(stateHome) || stateHome.includes("\0")) throw new Error("invalid state home"); + temporarySettings = fs.mkdtempSync(paths.join(stateHome, "turn-")); + settingsPath = paths.join(temporarySettings, "config.json"); + config.storage = { dir: paths.join(stateHome, ".zcode") }; + env = { ...process.env, ZCODE_DATA_BASE_DIR: stateHome }; } - fs.writeFileSync(`${env.HOME}/.zcode/cli/config.json`, JSON.stringify(config), { mode: 0o600, flag: "wx" }); - const child = spawn(host ? process.execPath : "/usr/bin/node", [host ? args[1] : "/runtime/zcode.cjs", "app-server"], { + fs.writeFileSync(settingsPath, JSON.stringify(config), { mode: 0o600, flag: "wx" }); + const childArgs = [host ? args[1] : "/runtime/zcode.cjs", "app-server"]; + if (host) childArgs.push("--settings", settingsPath); + const child = spawn(host ? process.execPath : "/usr/bin/node", childArgs, { stdio: ["pipe", "inherit", "inherit"], shell: false, env, ...(host ? { cwd: args[3] } : {}), }); const lines = require("node:readline").createInterface({ input: process.stdin }); diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index b9680af96c..f721ffbbf7 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -165,9 +165,10 @@ function settingsFor(connection: Connection, accountId?: string): ZcodeSettings mkdirSync(db, { recursive: true, mode: 0o700 }); if (!bwrap) return { accountId, command: [node, fileURLToPath(new URL("./desktop-bootstrap.cjs", import.meta.url)), - "--host", runtime, profile.config, workspace], + "--host", runtime, profile.config, workspace, privateHome], home: privateHome, workspace, settingsPath: "", scope: `desktop:${connection.generation}:${profileStamp}:host`, desktopModels: connection.models, + hostExecution: true, nativePermissionMode: "yolo", }; const sandboxHome = homedir(); // preserve the official credential cipher's HOME/username identity const args = ["--unshare-all", "--share-net", "--die-with-parent", "--new-session", "--ro-bind", "/usr", "/usr"]; @@ -188,7 +189,8 @@ function settingsFor(connection: Connection, accountId?: string): ZcodeSettings "--setenv", "ZCODE_DATA_BASE_DIR", "/desktop", "--chdir", "/workspace", "/usr/bin/node", "/bridge/desktop-bootstrap.cjs"); return { accountId, command: [bwrap, ...args], home: privateHome, workspace: "/workspace", settingsPath: "", - scope: `desktop:${connection.generation}:${profileStamp}:sandbox`, desktopModels: connection.models }; + scope: `desktop:${connection.generation}:${profileStamp}:sandbox`, desktopModels: connection.models, + nativePermissionMode: "yolo" }; } /** Persisted GUI consent is separate from provider config; data-plane requests cannot set it. */ diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index f473d047d0..c91233c2ea 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -17,6 +17,10 @@ export interface ZcodeSettings { settingsPath: string; scope: string; desktopModels?: DesktopModel[]; + /** Managed host mode preserves the user's ordinary tool environment while keeping ZCode state private. */ + hostExecution?: boolean; + /** Managed Desktop consent authorizes non-interactive native tools; advanced launchers retain edit mode. */ + nativePermissionMode?: "edit" | "yolo"; } /** Execution authority is operator environment or persisted GUI consent, never data-plane input. */ diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 5ddba940f6..f984056d72 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -882,8 +882,11 @@ export function catalogHintsFromProviderConfig( contextCap?: number, metadataModelIdCaseFold?: boolean, effectiveAlias?: string | null, + contextWindow?: number, ): Partial { - const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias); + const hinted = applyProviderConfigHints(name, prov, { + id, provider: name, ...(contextWindow ? { contextWindow } : {}), + }, contextCap, metadataModelIdCaseFold, effectiveAlias); const { provider: _provider, id: _id, ...hints } = hinted; return hints; } @@ -1663,6 +1666,7 @@ async function fetchProviderModelsWithAuth( const models = discoverZcodeModels(prov.zcodeAccountId).map(model => { const hints = catalogHintsFromProviderConfig( name, prov, model.id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias, + model.contextWindow, ); const reasoning = zcodeReasoningContract(model.id); return { @@ -1671,9 +1675,7 @@ async function fetchProviderModelsWithAuth( // future models expose no generic picker unless the operator configured one explicitly. ...(reasoning ?? (hints.reasoningEfforts === undefined ? { reasoningEfforts: [] } : {})), displayName: model.label, - ...(model.contextWindow ? { contextWindow: typeof contextCap === "number" && contextCap > 0 - ? Math.min(model.contextWindow, contextCap) : model.contextWindow } : {}), - inputModalities: ["text"], + inputModalities: hints.inputModalities ?? ["text"], } as CatalogModel; }); return observed(withConfiguredRetention(models), "authoritative"); diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 34273db60b..71f9aa82ca 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -705,11 +705,13 @@ function canonicalOpenAiBudgetPatchError( export async function handleProviderRoutes(ctx: ManagementContext): Promise { const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx; - if (url.pathname === "/api/zcode-accounts" || url.pathname.startsWith("/api/zcode-accounts/")) { + const zcodeAccountsPrefix = "/api/zcode-accounts"; + const zcodeDesktopPrefix = "/api/zcode-desktop"; + if (url.pathname === zcodeAccountsPrefix || url.pathname.startsWith(`${zcodeAccountsPrefix}/`)) { const { handleZcodeAccountRoutes } = await import("./zcode-account-routes"); return handleZcodeAccountRoutes(ctx); } - if (url.pathname === "/api/zcode-desktop" || url.pathname.startsWith("/api/zcode-desktop/")) { + if (url.pathname === zcodeDesktopPrefix || url.pathname.startsWith(`${zcodeDesktopPrefix}/`)) { const { handleZcodeDesktopRoutes } = await import("./zcode-desktop-routes"); return handleZcodeDesktopRoutes(ctx); } diff --git a/src/server/management/route-registry.ts b/src/server/management/route-registry.ts index fd81cec4f3..a75650632b 100644 --- a/src/server/management/route-registry.ts +++ b/src/server/management/route-registry.ts @@ -216,14 +216,20 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [ { method: "GET", path: "/api/debug/injection-logs", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/debug/logs", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/debug/usage-logs", module: "server/management/logs-usage-routes", mutates: false }, - { method: "GET", path: "/api/zcode-accounts", module: "server/management/zcode-account-routes", mutates: false }, - { method: "GET", path: "/api/zcode-accounts/login", module: "server/management/zcode-account-routes", mutates: false }, - { method: "POST", path: "/api/zcode-accounts/login", module: "server/management/zcode-account-routes", mutates: true }, - { method: "POST", path: "/api/zcode-accounts/cancel", module: "server/management/zcode-account-routes", mutates: true }, - { method: "POST", path: "/api/zcode-accounts/complete", module: "server/management/zcode-account-routes", mutates: true }, - { method: "POST", path: "/api/zcode-accounts/activate", module: "server/management/zcode-account-routes", mutates: true }, - { method: "POST", path: "/api/zcode-accounts/rename", module: "server/management/zcode-account-routes", mutates: true }, - { method: "POST", path: "/api/zcode-accounts/remove", module: "server/management/zcode-account-routes", mutates: true }, + { method: "GET", path: "/api/zcode-accounts", module: "server/management/zcode-account-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "session-only", why: "Saved-account metadata and OAuth operations require an authenticated dashboard session and explicit browser consent." } }, + { method: "GET", path: "/api/zcode-accounts/login", module: "server/management/zcode-account-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "session-only", why: "OAuth job polling exposes browser-login state only to the authenticated dashboard session that initiated it." } }, + { method: "POST", path: "/api/zcode-accounts/login", module: "server/management/zcode-account-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Starting official ZCode OAuth spends the user's identity and requires authenticated dashboard consent." } }, + { method: "POST", path: "/api/zcode-accounts/cancel", module: "server/management/zcode-account-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Cancelling a browser-started OAuth job remains confined to an authenticated dashboard session." } }, + { method: "POST", path: "/api/zcode-accounts/complete", module: "server/management/zcode-account-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Completing official ZCode OAuth persists account identity only after authenticated dashboard consent." } }, + { method: "POST", path: "/api/zcode-accounts/activate", module: "server/management/zcode-account-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Retrying account provider activation is an explicitly consented authenticated-dashboard operation." } }, + { method: "POST", path: "/api/zcode-accounts/rename", module: "server/management/zcode-account-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Renaming saved account metadata is intentionally available only to the authenticated dashboard." } }, + { method: "POST", path: "/api/zcode-accounts/remove", module: "server/management/zcode-account-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Deleting saved account state is a destructive action requiring authenticated dashboard consent." } }, + { method: "GET", path: "/api/zcode-desktop", module: "server/management/zcode-desktop-routes", mutates: false, exempt: { reason: "session-only", why: "Desktop status contains local runtime and workspace paths reserved for an authenticated dashboard session." } }, + { method: "GET", path: "/api/zcode-desktop/folders", module: "server/management/zcode-desktop-routes", mutates: false, exempt: { reason: "session-only", why: "Browsing host workspace folders reveals local paths and therefore requires an authenticated dashboard session." } }, + { method: "POST", path: "/api/zcode-desktop/connect", module: "server/management/zcode-desktop-routes", mutates: true, exempt: { reason: "session-only", why: "Connecting Desktop authorizes native host execution and provider activation through explicit dashboard consent." } }, + { method: "POST", path: "/api/zcode-desktop/activate", module: "server/management/zcode-desktop-routes", mutates: true, exempt: { reason: "session-only", why: "Retrying partial Desktop activation remains an explicit authenticated-dashboard consent action." } }, + { method: "POST", path: "/api/zcode-desktop/disconnect", module: "server/management/zcode-desktop-routes", mutates: true, exempt: { reason: "session-only", why: "Disconnecting revokes persisted Desktop execution consent and is restricted to the authenticated dashboard." } }, + { method: "POST", path: "/api/zcode-desktop/test", module: "server/management/zcode-desktop-routes", mutates: true, exempt: { reason: "session-only", why: "The optional live test spends ZCode inference quota and therefore requires an authenticated dashboard action." } }, { method: "GET", path: "/api/logs", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/storage/cleanup-policy", module: "server/management/logs-usage-routes", mutates: false }, { method: "GET", path: "/api/storage/cleanup-policy/test-stream", module: "server/management/logs-usage-routes", mutates: false, exempt: { reason: "test-seam", why: "Opt-in streaming seam declared at src/storage/policy-job.ts:71." } }, diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts index 1941fa6ac9..d32af8a119 100644 --- a/src/server/management/zcode-desktop-routes.ts +++ b/src/server/management/zcode-desktop-routes.ts @@ -22,12 +22,13 @@ export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = se try { return jsonResponse(desktopFolders(url.searchParams.get("path") ?? undefined)); } catch { return jsonResponse({ error: "workspace_invalid" }, 400); } } - if (req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405); + if (req.method !== "POST") return null; try { const raw = await readBoundedJsonRequestBody(req, 12_000, undefined, { signal: AbortSignal.any([req.signal, AbortSignal.timeout(5_000)]) }); if (!raw || typeof raw !== "object" || Array.isArray(raw)) return jsonResponse({ error: "invalid_request" }, 400); const body = raw as Record; - if (url.pathname === "/api/zcode-desktop/connect" || url.pathname === "/api/zcode-desktop/activate") { + if (url.pathname === "/api/zcode-desktop/connect" && req.method === "POST" + || url.pathname === "/api/zcode-desktop/activate" && req.method === "POST") { if (body.consent !== true || typeof body.runtime !== "string" || typeof body.workspace !== "string") return jsonResponse({ error: "consent_required" }, 400); if (activating) return jsonResponse({ error: "busy" }, 409); activating = true; @@ -38,12 +39,12 @@ export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = se return jsonResponse(await activateDesktopProvider(ctx, status, deps.readDesktopCatalogSlugs)); } finally { activating = false; } } - if (url.pathname === "/api/zcode-desktop/disconnect") { + if (url.pathname === "/api/zcode-desktop/disconnect" && req.method === "POST") { if (activating) return jsonResponse({ error: "busy" }, 409); await deps.disconnectDesktop(); invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); return jsonResponse(deps.desktopStatus()); } - if (url.pathname === "/api/zcode-desktop/test") { + if (url.pathname === "/api/zcode-desktop/test" && req.method === "POST") { if (testing) return jsonResponse({ error: "busy" }, 409); const status = deps.desktopStatus(); if (body.consent !== true || !status.connected || !status.models.some(m => m.id === body.model)) return jsonResponse({ error: "invalid_request" }, 400); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 13cec9650e..78e820b9db 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -67,7 +67,12 @@ so the schema is not something a user can fix from configuration (issue #2673). ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional -Bubblewrap workspace boundary; harness restrictions apply where the native process runs. +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. Explicitly +consented managed connections use the official runtime's non-interactive `yolo` mode because the +bridge cannot project interactive approval prompts; advanced operator launchers retain `edit` mode. +The host child inherits only a named allowlist of tool environment variables and keeps ZCode-owned +credentials/session state under its private data root, so host tools work without copying unrelated +provider secrets into the child. ## ZCode saved accounts diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index c6989dfe0b..ac25b8c774 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -60,7 +60,9 @@ executor contract. Main-request migration must not treat that branch as fixed-tr ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional -Bubblewrap workspace boundary; harness restrictions apply where the native process runs. +Bubblewrap workspace boundary; harness restrictions apply where the native process runs. Managed +connections pair that explicit consent with ZCode's non-interactive permission mode and a bounded +host-tool environment; advanced launchers keep their existing `edit` semantics. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 9e2520f05b..8fd5c81d78 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -29,7 +29,7 @@ class FakeClient { onFailure: (error: Error) => void = () => {}; calls: Array<{ method: string; params: JsonObject }> = []; closed = false; - constructor(private outcome: "ok" | "failed" | "hang" | "session-noise" = "ok") {} + constructor(private outcome: "ok" | "failed" | "hang" | "session-noise" | "answerless" = "ok") {} async request(method: string, params: JsonObject): Promise { this.calls.push({ method, params }); if (method === "session/create") return { session: { sessionId: "sess_test-1" } }; @@ -51,6 +51,7 @@ class FakeClient { this.event("tool.updated", { kind: "started", toolCallId: "tool-1" }); this.event("tool.updated", { kind: "result", toolCallId: "tool-1" }); if (this.outcome === "failed") return this.event("turn.failed", { error: { message: "secret" } }); + if (this.outcome === "answerless") return this.event("turn.completed", {}); this.event("model.streaming", { kind: "text_delta", delta: "Hello" }); this.event("turn.completed", { response: "Hello" }); }); @@ -75,7 +76,8 @@ async function run(settings: ZcodeSettings, client: FakeClient, parsed = request describe("ZCode local agent", () => { test("managed requests select a public model without carrying Desktop secrets", async () => { - const settings = { ...fixture(), desktopModels: [{ id: "test/model", providerId: "test", modelId: "model", label: "Model" }] }; + const settings = { ...fixture(), hostExecution: true, nativePermissionMode: "yolo" as const, + desktopModels: [{ id: "test/model", providerId: "test", modelId: "model", label: "Model" }] }; const client = new FakeClient(); expect((await run(settings, client)).at(-1)?.type).toBe("done"); for (const call of client.calls.filter(c => ["session/create", "session/send"].includes(c.method))) { @@ -85,6 +87,11 @@ describe("ZCode local agent", () => { expect(serialized).not.toContain("never-log-this"); expect(serialized).not.toContain("apiKey"); } + expect(client.calls.find(call => call.method === "session/create")?.params.mode).toBe("yolo"); + expect((await run(fixture(), new FakeClient())).find(event => event.type === "text_delta" + && event.phase === "commentary")?.text).toContain("configured launcher"); + expect((await run(settings, new FakeClient())).find(event => event.type === "text_delta" + && event.phase === "commentary")?.text).toContain("host-user access"); }); test("maps Codex effort labels to the official GLM-5.3 thought levels", async () => { const settings = { ...fixture(), desktopModels: [{ @@ -161,6 +168,13 @@ describe("ZCode local agent", () => { expect(client.closed).toBe(true); expect(client.calls.map(c => c.method)).toEqual(["session/create", "session/subscribe", "session/send", "session/stop"]); }); + test("answerless completion is reported as an interrupted accepted turn", async () => { + const events = await run(fixture(), new FakeClient("answerless")); + expect(events.at(-1)).toMatchObject({ type: "incomplete", reason: "zcode_agent_interrupted", + retryable: false, endTurn: true }); + expect(JSON.stringify(events)).toContain("completed without a model answer"); + expect(events.some(event => event.type === "done")).toBe(false); + }); test("ignores terminal events without the active session identity", async () => { const events = await run(fixture(), new FakeClient("session-noise")); expect(events.filter(e => e.type === "text_delta" && e.text === "wrong session")).toHaveLength(0); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 558710a2a5..5ede0db095 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -217,11 +217,13 @@ test("host bootstrap reads and writes outside workspace without touching the sou const path=${JSON.stringify(external)}; const value=fs.readFileSync(path,"utf8"); fs.writeFileSync(path,value); - process.stdout.write(JSON.stringify({id:r.id,result:{read:value,written:true,cwd:process.cwd()}})+"\\n"); + process.stdout.write(JSON.stringify({id:r.id,result:{read:value,written:true,cwd:process.cwd(), + home:process.env.HOME,dataBase:process.env.ZCODE_DATA_BASE_DIR,args:process.argv.slice(2)}})+"\\n"); }); `); const settings = { command: [resolveDesktopNode(), fileURLToPath(new URL("../../src/adapters/zcode/desktop-bootstrap.cjs", import.meta.url)), - "--host", runtime, config, workspace], home, workspace, settingsPath: "", scope: "desktop:test-host" }; + "--host", runtime, config, workspace, home], home, workspace, settingsPath: "", scope: "desktop:test-host", + hostExecution: true, nativePermissionMode: "yolo" as const }; // Repeated processes must not fail on an existing config, or overwrite Desktop's file. for (let i = 0; i < 2; i++) { const client = new ZcodeClient(settings); @@ -229,7 +231,12 @@ test("host bootstrap reads and writes outside workspace without touching the sou const models = await client.request("opencodex/desktopModels", {}, 3000); expect(models.models).toHaveLength(1); const state = await client.request("workspace/readState", {}, 3000); - expect(state).toEqual({ read: "report fixture", written: true, cwd: workspace }); + const runtimeArgs = state.args as string[]; + expect(state).toMatchObject({ read: "report fixture", written: true, cwd: workspace, + home: homedir(), dataBase: home }); + expect(runtimeArgs.slice(0, 2)).toEqual(["app-server", "--settings"]); + expect(runtimeArgs[2]).toContain("/turn-"); + expect(runtimeArgs[2]).toEndWith("/config.json"); } finally { await client.close(); } } expect(readFileSync(config, "utf8")).toBe(original); @@ -304,10 +311,13 @@ test("ZCode GLM-5.3 catalog exposes only real levels plus Codex ultra", async () }), { mode: 0o600 }); const providerName = `zcode-fixture-${crypto.randomUUID()}`; const models = await gatherRoutedModels({ port: 0, defaultProvider: providerName, providers: { - [providerName]: { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", liveModels: true }, + [providerName]: { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", liveModels: true, + contextWindow: 8192 }, } }); for (const modelId of ids) { const model = models.find(row => row.provider === providerName && row.id === modelId); + expect(model?.contextWindow).toBe(8192); + expect(model?.inputModalities).toContain("image"); expect(model?.reasoningEfforts).toEqual(["low", "high", "max"]); const [entry] = buildCatalogEntries(null, [], [model!]); const efforts = (entry?.supported_reasoning_levels as Array<{ effort: string }>).map(level => level.effort); diff --git a/tests/providers/zcode-transport.test.ts b/tests/providers/zcode-transport.test.ts index 8e94ac61e9..0c36b18233 100644 --- a/tests/providers/zcode-transport.test.ts +++ b/tests/providers/zcode-transport.test.ts @@ -1,12 +1,13 @@ import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { PassThrough, Writable } from "node:stream"; +import { homedir } from "node:os"; import { ZcodeClient, closeZcodeDesktopClients, type ZcodeSpawn } from "../../src/adapters/zcode/client"; import type { JsonObject, ZcodeSettings } from "../../src/adapters/zcode/settings"; const settings: ZcodeSettings = { command: ["/isolated/launcher", "argument with spaces"], home: "/isolated/home", workspace: "/workspace", settingsPath: "/isolated/config.json", scope: "test" }; -function fixture(managed = false) { +function fixture(managed = false, hostExecution = false) { const child = new EventEmitter() as EventEmitter & { stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: () => boolean }; const writes: JsonObject[] = []; child.stdin = new Writable({ write(chunk, _encoding, callback) { writes.push(JSON.parse(chunk.toString())); callback(); } }); @@ -14,7 +15,7 @@ function fixture(managed = false) { child.kill = () => { child.stdout.end(); child.stderr.end(); queueMicrotask(() => child.emit("exit", 0)); return true; }; let invocation: unknown[] = []; const spawn = ((...args: unknown[]) => { invocation = args; return child; }) as ZcodeSpawn; - const client = new ZcodeClient(managed ? { ...settings, desktopModels: [] } : settings, spawn); + const client = new ZcodeClient(managed ? { ...settings, desktopModels: [], hostExecution } : settings, spawn); return { child, client, writes, invocation: () => invocation }; } const tick = () => new Promise(resolve => setTimeout(resolve, 5)); @@ -40,6 +41,25 @@ describe("ZCode NDJSON transport", () => { expect(Object.keys(options.env).sort()).toEqual(["HOME", "PATH", "XDG_CACHE_HOME", "XDG_CONFIG_HOME"]); await f.client.close(); }); + test("host tools inherit user configuration but not unrelated provider secrets", async () => { + const token = process.env.OPENCODEX_API_AUTH_TOKEN; + const ghToken = process.env.GH_TOKEN; + try { + process.env.OPENCODEX_API_AUTH_TOKEN = "proxy-secret"; + process.env.GH_TOKEN = "github-secret"; + const f = fixture(true, true); + const options = f.invocation()[2] as { env: Record }; + expect(options.env.HOME).toBe(homedir()); + expect(options.env.ZCODE_DATA_BASE_DIR).toBe(settings.home); + expect(options.env.PATH).toBe(process.env.PATH); + expect(options.env.OPENCODEX_API_AUTH_TOKEN).toBeUndefined(); + expect(options.env.GH_TOKEN).toBeUndefined(); + await f.client.close(); + } finally { + if (token === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; else process.env.OPENCODEX_API_AUTH_TOKEN = token; + if (ghToken === undefined) delete process.env.GH_TOKEN; else process.env.GH_TOKEN = ghToken; + } + }); test("correlates fragmented Unicode responses and services runtime preferences", async () => { const f = fixture(); const pending = f.client.request("session/create", {}); const bytes = Buffer.from(JSON.stringify({ id: 1, result: { text: "世界" } }) + "\n"); From 2c4bcc00845e5d57e46d6b8e3f09a66e59890bc2 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 04:15:12 -0300 Subject: [PATCH 17/43] fix(zcode): launch official runtime without unsupported flags --- src/adapters/zcode/desktop-bootstrap.cjs | 27 ++++++++++-------- src/adapters/zcode/desktop-host-preload.cjs | 27 ++++++++++++++++++ structure/adapters/registry.md | 6 ++-- tests/providers/zcode-desktop.test.ts | 31 +++++++++++++++------ 4 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 src/adapters/zcode/desktop-host-preload.cjs diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs index fdf3689bb1..5acdb5cb0d 100644 --- a/src/adapters/zcode/desktop-bootstrap.cjs +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -1,6 +1,6 @@ // Runs on the host by default, or inside an explicitly enabled OS sandbox. Desktop credentials/configuration are mounted read-only. -// Only the official runtime consumes provider keys. The compatible settings file lives in tmpfs, -// never in OpenCodex's data directory, management responses, logs or the Desktop profile. +// Only the official runtime consumes provider keys. The compatible settings file is private and +// turn-scoped; it never reaches management responses, logs or the Desktop source profile. const fs = require("node:fs"); const { spawn } = require("node:child_process"); @@ -40,8 +40,8 @@ function desktopModelCatalog(config) { } module.exports = { normalizeDesktopConfig, desktopModelCatalog }; if (require.main === module) { - let temporarySettings; - const cleanup = () => { if (temporarySettings) fs.rmSync(temporarySettings, { recursive: true, force: true }); }; + let temporaryHome; + const cleanup = () => { if (temporaryHome) fs.rmSync(temporaryHome, { recursive: true, force: true }); }; process.on("exit", cleanup); try { const args = process.argv.slice(2); @@ -54,19 +54,24 @@ if (require.main === module) { let env = process.env; let settingsPath = `${env.HOME}/.zcode/cli/config.json`; if (host) { - // Keep only ZCode-owned state private. HOME/XDG remain the real host environment so - // native tools behave like ordinary host tools rather than an accidental soft sandbox. + // ZCode reads user config from os.homedir() and exposes no config-path CLI flag. Patch + // that lookup only in the official runtime process; process.env.HOME stays real for + // the native tools it starts. This is state separation, not filesystem confinement. const paths = require("node:path"); const stateHome = args[4]; if (!paths.isAbsolute(stateHome) || stateHome.includes("\0")) throw new Error("invalid state home"); - temporarySettings = fs.mkdtempSync(paths.join(stateHome, "turn-")); - settingsPath = paths.join(temporarySettings, "config.json"); + const stableDb = paths.join(stateHome, ".zcode/cli/db"); + temporaryHome = fs.mkdtempSync(paths.join(stateHome, "turn-")); + fs.mkdirSync(paths.join(temporaryHome, ".zcode/cli"), { recursive: true, mode: 0o700 }); + fs.symlinkSync(stableDb, paths.join(temporaryHome, ".zcode/cli/db"), "dir"); + settingsPath = paths.join(temporaryHome, ".zcode/cli/config.json"); config.storage = { dir: paths.join(stateHome, ".zcode") }; - env = { ...process.env, ZCODE_DATA_BASE_DIR: stateHome }; + env = { ...process.env, ZCODE_DATA_BASE_DIR: stateHome, OCX_ZCODE_RUNTIME_HOME: temporaryHome }; } fs.writeFileSync(settingsPath, JSON.stringify(config), { mode: 0o600, flag: "wx" }); - const childArgs = [host ? args[1] : "/runtime/zcode.cjs", "app-server"]; - if (host) childArgs.push("--settings", settingsPath); + const childArgs = host + ? ["--require", require.resolve("./desktop-host-preload.cjs"), args[1], "app-server"] + : ["/runtime/zcode.cjs", "app-server"]; const child = spawn(host ? process.execPath : "/usr/bin/node", childArgs, { stdio: ["pipe", "inherit", "inherit"], shell: false, env, ...(host ? { cwd: args[3] } : {}), }); diff --git a/src/adapters/zcode/desktop-host-preload.cjs b/src/adapters/zcode/desktop-host-preload.cjs new file mode 100644 index 0000000000..91dd39a709 --- /dev/null +++ b/src/adapters/zcode/desktop-host-preload.cjs @@ -0,0 +1,27 @@ +// ZCode has no public CLI flag for selecting its user config. Keep the official +// runtime's state/config in OpenCodex's private home while leaving process.env.HOME +// untouched for the native tools that the official runtime starts. +const fs = require("node:fs"); +const { syncBuiltinESMExports } = require("node:module"); +const os = require("node:os"); +const path = require("node:path"); + +const key = "OCX_ZCODE_RUNTIME_HOME"; +const requested = process.env[key]; +delete process.env[key]; + +// A process spawned by ZCode may inherit execArgv in unusual launchers. The +// one-shot environment marker makes this preload a no-op outside the owned root. +if (requested !== undefined) { + if (!path.isAbsolute(requested) || requested.includes("\0")) { + throw new Error("ZCode private runtime home is invalid."); + } + const resolved = fs.realpathSync(requested); + const metadata = fs.lstatSync(resolved); + if (!metadata.isDirectory() || metadata.uid !== process.getuid?.() || (metadata.mode & 0o077) !== 0) { + throw new Error("ZCode private runtime home is invalid."); + } + Object.defineProperty(os, "homedir", { configurable: true, enumerable: true, + value: () => resolved, writable: true }); + syncBuiltinESMExports(); +} diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index fcdeac909c..50f82bfbe6 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -141,8 +141,10 @@ Bubblewrap workspace boundary; harness restrictions apply where the native proce consented managed connections use the official runtime's non-interactive `yolo` mode because the bridge cannot project interactive approval prompts; advanced operator launchers retain `edit` mode. The host child inherits only a named allowlist of tool environment variables and keeps ZCode-owned -credentials/session state under its private data root, so host tools work without copying unrelated -provider secrets into the child. +credentials/session state under its private data root. Because the official app server has no +config-path option, a one-shot Node preload redirects only its internal `os.homedir()` lookup to a +turn-scoped private home; the process environment retains the user's real `HOME`, so native host +tools work without copying unrelated provider secrets into the child. ## ZCode saved accounts diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 397c08b214..c5c6e1b81a 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; @@ -213,13 +213,23 @@ test("host bootstrap reads and writes outside workspace without touching the sou const runtime = join(root, "official-runtime-fixture.cjs"); writeFileSync(runtime, ` const fs=require("node:fs"); + const os=require("node:os"); + const path=require("node:path"); + const {spawnSync}=require("node:child_process"); require("node:readline").createInterface({input:process.stdin}).on("line",line=>{ const r=JSON.parse(line); - const path=${JSON.stringify(external)}; - const value=fs.readFileSync(path,"utf8"); - fs.writeFileSync(path,value); + const external=${JSON.stringify(external)}; + const value=fs.readFileSync(external,"utf8"); + fs.writeFileSync(external,value); + const runtimeHome=os.homedir(); + const settings=JSON.parse(fs.readFileSync(path.join(runtimeHome,".zcode/cli/config.json"),"utf8")); + const nativeHome=spawnSync(process.execPath,["-e","process.stdout.write(require('node:os').homedir())"], + {encoding:"utf8",env:process.env}).stdout; process.stdout.write(JSON.stringify({id:r.id,result:{read:value,written:true,cwd:process.cwd(), - home:process.env.HOME,dataBase:process.env.ZCODE_DATA_BASE_DIR,args:process.argv.slice(2)}})+"\\n"); + hostHome:process.env.HOME,runtimeHome,nativeHome,dataBase:process.env.ZCODE_DATA_BASE_DIR, + runtimeMarker:Object.hasOwn(process.env,"OCX_ZCODE_RUNTIME_HOME"),args:process.argv.slice(2), + providerIds:Object.keys(settings.provider),mainModel:settings.model.main, + storageDir:settings.storage.dir}})+"\\n"); }); `); const settings = { command: [resolveDesktopNode(), fileURLToPath(new URL("../../src/adapters/zcode/desktop-bootstrap.cjs", import.meta.url)), @@ -234,12 +244,15 @@ test("host bootstrap reads and writes outside workspace without touching the sou const state = await client.request("workspace/readState", {}, 3000); const runtimeArgs = state.args as string[]; expect(state).toMatchObject({ read: "report fixture", written: true, cwd: workspace, - home: homedir(), dataBase: home }); - expect(runtimeArgs.slice(0, 2)).toEqual(["app-server", "--settings"]); - expect(runtimeArgs[2]).toContain("/turn-"); - expect(runtimeArgs[2]).toEndWith("/config.json"); + hostHome: homedir(), nativeHome: homedir(), dataBase: home, runtimeMarker: false, + providerIds: ["builtin:zai-coding-plan"], mainModel: "builtin:zai-coding-plan/model", + storageDir: join(home, ".zcode") }); + expect(runtimeArgs).toEqual(["app-server"]); + expect(state.runtimeHome).toStartWith(join(home, "turn-")); + expect(state.runtimeHome).not.toBe(homedir()); } finally { await client.close(); } } + expect(readdirSync(home).filter(name => name.startsWith("turn-"))).toEqual([]); expect(readFileSync(config, "utf8")).toBe(original); }); From 577877b7ad71f55e9848223368570541a3d9c6b2 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 04:53:04 -0300 Subject: [PATCH 18/43] test(zcode): scope host bootstrap regression to Linux --- tests/providers/zcode-desktop.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index c5c6e1b81a..6ad414eb14 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -200,6 +200,7 @@ test("host execution is the default; a workspace is not a filesystem boundary", }); test("host bootstrap reads and writes outside workspace without touching the source profile", async () => { + if (process.platform !== "linux") return; // Managed Desktop prerequisites currently support Linux only. const { ZcodeClient } = await import("../../src/adapters/zcode/client"); const { fileURLToPath } = await import("node:url"); const home = join(root, "state"), workspace = join(root, "project"); From 35bed9beec2333cdd9f6212fd5b1260d3319387a Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 05:11:09 -0300 Subject: [PATCH 19/43] fix(zcode): close account and disconnect lifecycle gaps --- .../src/content/docs/guides/zcode-agent.md | 9 ++- src/adapters/zcode/adapter.ts | 11 ++- src/server/management/zcode-account-routes.ts | 18 ++++- .../management/zcode-desktop-activation.ts | 41 +++++++++- src/server/management/zcode-desktop-routes.ts | 4 +- structure/adapters/registry.md | 3 +- structure/gui-and-management-api.md | 2 +- tests/providers/zcode-adapter.test.ts | 20 +++++ tests/providers/zcode-desktop-routes.test.ts | 75 ++++++++++++++++++- 9 files changed, 166 insertions(+), 17 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index b78de58d7a..8a926f0bfa 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -56,10 +56,11 @@ or subscription terms; those remain the vendor's policy. custom Desktop providers and routes back to OpenCodex are not imported. For an existing provider, the same panel is in **Settings**. **Disconnect** revokes the managed -connection and closes its owned app-server children. It does not log out of Desktop, delete -Desktop conversations, or change its configuration. A saved disconnected state also prevents -an older environment-based setup from silently reactivating. Reconnect to refresh the model -catalog after changing Desktop's model configuration. +connection, disables that provider and removes its models from the converged Codex catalog while +preserving its customized settings for reconnection. It closes owned app-server children but does +not log out of Desktop, delete Desktop conversations, or change ZCode Desktop's own configuration. +A saved disconnected state also prevents an older environment-based setup from silently +reactivating. Reconnect to refresh the model catalog after changing Desktop's model configuration. The proxy host needs a Node.js version compatible with the installed ZCode runtime. Missing prerequisites and unsupported platforms are shown in the panel. Managed setup diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index d757305847..63f76e5f81 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -9,9 +9,11 @@ import { zcodeThoughtLevel } from "./reasoning"; type Client = Pick; const MAX_ZCODE_INPUT_CHARS = 200_000; const HISTORY_TRUNCATED = "[Earlier OpenCodex conversation history truncated to fit the ZCode bridge.]"; +const SAFE_ACCOUNT_REFRESH_ERRORS = new Set(["account_login_required", "account_identity_mismatch", "native_oauth_failed"]); export interface ZcodeAdapterDeps { settings?: () => ZcodeSettings; client?: (settings: ZcodeSettings) => Client; + refreshAccount?: (id: string) => Promise; timeoutMs?: number; } @@ -151,7 +153,14 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt let stop = () => {}; const cancelled = () => stop(); try { - if (provider.zcodeAccountId && !deps.settings) await refreshAccount(provider.zcodeAccountId); + if (provider.zcodeAccountId && !deps.settings) { + try { await (deps.refreshAccount ?? refreshAccount)(provider.zcodeAccountId); } + catch (error) { + const code = error instanceof Error && SAFE_ACCOUNT_REFRESH_ERRORS.has(error.message) + ? error.message : "account_refresh_failed"; + throw new Error(code); + } + } let settings: ZcodeSettings; try { settings = (deps.settings ?? (() => loadZcodeSettings(process.env, provider.zcodeAccountId)))(); } catch { throw new Error("ZCode native execution is unavailable. Configure the isolated launcher, home, workspace and explicit opt-in."); } diff --git a/src/server/management/zcode-account-routes.ts b/src/server/management/zcode-account-routes.ts index 683f3276f9..5e2bbe7ef8 100644 --- a/src/server/management/zcode-account-routes.ts +++ b/src/server/management/zcode-account-routes.ts @@ -1,9 +1,9 @@ import { randomUUID } from "node:crypto"; import { existsSync, renameSync, rmSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; import { accountProfile, accountRoot, allocateAccount, listAccounts, readAccount, removeAccountFiles, writeAccount } from "../../adapters/zcode/accounts"; import { accountRuntimeBusy, invalidateAccountRefresh } from "../../adapters/zcode/account-runtime"; -import { connectDesktop, desktopStatus, disconnectDesktop, resolveDesktopRuntime, validateDesktopWorkspace } from "../../adapters/zcode/desktop"; +import { connectDesktop, defaultDesktopWorkspace, desktopStatus, disconnectDesktop, resolveDesktopRuntime, validateDesktopWorkspace } from "../../adapters/zcode/desktop"; import { runNativeOAuth } from "../../adapters/zcode/native-oauth"; import { saveConfigPreservingClaudeCode, withConfigMutationLockSync } from "../../config"; import { clearModelCache } from "../../codex/model-cache"; @@ -65,9 +65,21 @@ export async function handleZcodeAccountRoutes(ctx: ManagementContext, deps = se if (typeof body.label !== "string" || typeof body.runtime !== "string" || typeof body.workspace !== "string") return fail("account_invalid"); if (body.accountId !== undefined && typeof body.accountId !== "string") return fail("account_invalid"); const replaceId = typeof body.accountId === "string" ? readAccount(body.accountId).id : undefined; - const runtime = resolveDesktopRuntime(body.runtime), workspace = validateDesktopWorkspace(body.workspace, replaceId); + const runtime = resolveDesktopRuntime(body.runtime); if (replaceId && (accountRuntimeBusy(replaceId) || pendingFor(replaceId))) return fail("account_busy"); const account = allocateAccount(body.label, replaceId); + const targetId = replaceId ?? account.id; + let workspace: string; + try { + const requested = resolve(body.workspace) === resolve(defaultDesktopWorkspace()) + ? defaultDesktopWorkspace(targetId) : body.workspace; + // Validate against the account that connectDesktop will eventually use. In optional + // sandbox mode the global managed workspace is not an account workspace. + workspace = validateDesktopWorkspace(requested, targetId); + } catch (error) { + removeAccountFiles(account.id); + throw error; + } const job: Job = { id: randomUUID(), accountId: account.id, replaceId, runtime, workspace, phase: "waiting", controller: new AbortController(), timer: undefined! }; job.timer = setTimeout(() => { diff --git a/src/server/management/zcode-desktop-activation.ts b/src/server/management/zcode-desktop-activation.ts index 70bfd911bd..1686f434e4 100644 --- a/src/server/management/zcode-desktop-activation.ts +++ b/src/server/management/zcode-desktop-activation.ts @@ -10,8 +10,12 @@ type DesktopStatus = ReturnType; export const readDesktopCatalogSlugs = (): string[] => (readCatalog(readCodexCatalogPath())?.models ?? []) .filter(row => row.visibility === "list").map(row => String(row.slug)); +function providerNames(ctx: ManagementContext, accountId?: string): string[] { + return Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.adapter === "zcode" && ctx.config.providers[name]?.zcodeAccountId === accountId); +} + function providerName(ctx: ManagementContext, accountId?: string): string | undefined { - const matches = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.adapter === "zcode" && ctx.config.providers[name]?.zcodeAccountId === accountId); + const matches = providerNames(ctx, accountId); return matches.length === 1 ? matches[0] : undefined; } @@ -68,3 +72,38 @@ export async function activateDesktopProvider(ctx: ManagementContext, status: De return { ...desktopActivation(ctx, status, readSlugs), activation: "catalog_pending", error: "catalog_update_failed" }; } } + +/** Revoke catalog visibility without deleting customized provider settings. */ +export async function deactivateDesktopProvider(ctx: ManagementContext, status: DesktopStatus, readSlugs = readDesktopCatalogSlugs) { + const names = providerNames(ctx, status.accountId); + if (names.length) { + try { + const enabled = names.filter(name => ctx.config.providers[name]?.disabled !== true); + if (enabled.length) { + withConfigMutationLockSync(() => { + const previous = new Map(enabled.map(name => [name, ctx.config.providers[name]!])); + try { + for (const [name, provider] of previous) ctx.config.providers[name] = { ...provider, disabled: true }; + (ctx.deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(ctx.config); + } catch (error) { + for (const [name, provider] of previous) ctx.config.providers[name] = provider; + throw error; + } + }); + reconcileLiveStateStores(); + } + for (const name of names) clearModelCache(name); + } catch { + return { ...desktopActivation(ctx, status, readSlugs), error: "provider_registration_failed" }; + } + } + try { + const result = await ctx.convergeCodexCatalog(); + if (result.status !== "committed") { + return { ...desktopActivation(ctx, status, readSlugs), error: "catalog_update_failed" }; + } + return desktopActivation(ctx, status, readSlugs); + } catch { + return { ...desktopActivation(ctx, status, readSlugs), error: "catalog_update_failed" }; + } +} diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts index d32af8a119..048e9eb1a4 100644 --- a/src/server/management/zcode-desktop-routes.ts +++ b/src/server/management/zcode-desktop-routes.ts @@ -6,7 +6,7 @@ import { createTranslatorBudget } from "../../lib/translator-budget"; import { invalidateCodexModelsCache } from "../../codex/catalog"; import { clearGatherRoutedModelsInflight } from "../../codex/catalog/provider-fetch"; import { readBoundedJsonRequestBody } from "../request-decompress"; -import { activateDesktopProvider, desktopActivation, readDesktopCatalogSlugs } from "./zcode-desktop-activation"; +import { activateDesktopProvider, deactivateDesktopProvider, desktopActivation, readDesktopCatalogSlugs } from "./zcode-desktop-activation"; const services = { connectDesktop, desktopStatus, disconnectDesktop, readDesktopCatalogSlugs }; let activating = false; @@ -42,7 +42,7 @@ export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = se if (url.pathname === "/api/zcode-desktop/disconnect" && req.method === "POST") { if (activating) return jsonResponse({ error: "busy" }, 409); await deps.disconnectDesktop(); invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); - return jsonResponse(deps.desktopStatus()); + return jsonResponse(await deactivateDesktopProvider(ctx, deps.desktopStatus(), deps.readDesktopCatalogSlugs)); } if (url.pathname === "/api/zcode-desktop/test" && req.method === "POST") { if (testing) return jsonResponse({ error: "busy" }, 409); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index dbb2142229..680f94db99 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -164,7 +164,8 @@ app-server settings, session/DB scope and quota reads. An invalid or revoked bin closed, never to the legacy Desktop profile or another account. Legacy unbound `zcode` retains its previous profile. Account providers do not participate in an implicit pool. Native tools still run on the host by default; optional OS sandboxing is independent of -profile separation and does not turn the latter into a security boundary. +profile separation and does not turn the latter into a security boundary. Refresh failures +cross the adapter boundary only as bounded public codes, never filesystem or account paths. ## ZCode vision input adaptation diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index af1f801c1e..faffd43bbb 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and only transient OAuth jobs expire on restart. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and only transient OAuth jobs expire on restart. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 8fd5c81d78..9cfff48526 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -137,6 +137,26 @@ describe("ZCode local agent", () => { expect(() => loadZcodeSettings({})).toThrow("disabled"); expect(() => loadZcodeSettings({ OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: "echo unsafe" })).toThrow("JSON argv"); }); + test("saved-account refresh failures emit only bounded public codes", async () => { + const events: AdapterEvent[] = []; + const adapter = createZcodeAdapter({ ...provider, zcodeAccountId: crypto.randomUUID() }, { + refreshAccount: async () => { throw new Error("/private/config/zcode-accounts/identity/account.json"); }, + }); + await adapter.runTurn!(request(), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, event => events.push(event)); + expect(events.at(-1)).toMatchObject({ type: "error", message: "account_refresh_failed", retryable: false }); + expect(JSON.stringify(events)).not.toContain("/private/"); + expect(JSON.stringify(events)).not.toContain("account.json"); + }); + test("saved-account refresh preserves actionable public codes", async () => { + for (const code of ["account_login_required", "account_identity_mismatch", "native_oauth_failed"]) { + const events: AdapterEvent[] = []; + const adapter = createZcodeAdapter({ ...provider, zcodeAccountId: crypto.randomUUID() }, { + refreshAccount: async () => { throw new Error(code); }, + }); + await adapter.runTurn!(request(), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, event => events.push(event)); + expect(events.at(-1)).toMatchObject({ type: "error", message: code, retryable: false }); + } + }); test("advanced settings allow an isolated home when the proxy HOME is absent", () => { const settings = fixture(); const env = { OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: JSON.stringify(["/isolated-launcher"]), diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index da03b61d62..d12a39dae7 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -1,10 +1,11 @@ import { handleZcodeAccountRoutes, resetZcodeAccountJobsForTests } from "../../src/server/management/zcode-account-routes"; import { listAccounts, accountProfile } from "../../src/adapters/zcode/accounts"; +import { defaultDesktopWorkspace } from "../../src/adapters/zcode/desktop"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { handleZcodeDesktopRoutes } from "../../src/server/management/zcode-desktop-routes"; import type { ManagementContext } from "../../src/server/management/context"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getDefaultConfig } from "../../src/config"; @@ -32,7 +33,9 @@ function context(path: string, body: unknown, principal?: ManagementContext["pri return { req, url: new URL(req.url), principal, config, deps: { saveConfigPreservingClaudeCode: c => writeFileSync(join(home, "config.json"), JSON.stringify(c)) }, convergeCodexCatalog: async () => { - writeFileSync(catalog, JSON.stringify(models.map(m => "zcode/" + m.id.replaceAll("/", "-")))); + const slugs = Object.entries(config.providers).filter(([, provider]) => provider.adapter === "zcode" && provider.disabled !== true) + .flatMap(([name]) => models.map(model => name + "/" + model.id.replaceAll("/", "-"))); + writeFileSync(catalog, JSON.stringify(slugs)); return { status: "committed", changed: true, degraded: false, notices: [] }; }, } as ManagementContext; @@ -118,6 +121,41 @@ test("registration failure rolls back config, preserves protocol state and allow expect((await activateDesktopProvider(ctx, connected, f.deps.readDesktopCatalogSlugs)).activation).toBe("ready"); }); +test("disconnect disables the provider, preserves customization and removes its catalog rows", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/disconnect", {}, "gui-session"); + const beforeDefault = ctx.config.defaultProvider; + ctx.config.providers.zcode = { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", + disabled: false, defaultModel: models[0]!.id, note: "keep custom settings", contextWindow: 64_000 }; + await ctx.convergeCodexCatalog(); + expect(f.deps.readDesktopCatalogSlugs()).toHaveLength(2); + const response = await handleZcodeDesktopRoutes(ctx, f.deps); + expect(await response?.json()).toMatchObject({ connected: false, activation: "disconnected", providerRegistered: false }); + expect(ctx.config.providers.zcode).toEqual({ adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", + disabled: true, defaultModel: models[0]!.id, note: "keep custom settings", contextWindow: 64_000 }); + expect(JSON.parse(readFileSync(join(home, "config.json"), "utf8")).providers.zcode.disabled).toBe(true); + expect(f.deps.readDesktopCatalogSlugs()).toEqual([]); + expect(ctx.config.defaultProvider).toBe(beforeDefault); +}); + +test("partial disconnect cleanup is explicit and idempotently retryable", async () => { + const f = fixture(), ctx = context("/api/zcode-desktop/disconnect", {}, "gui-session"); + ctx.config.providers.zcode = { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", disabled: false }; + await ctx.convergeCodexCatalog(); + const converge = ctx.convergeCodexCatalog; + ctx.convergeCodexCatalog = async () => ({ status: "failed", reason: "disk", phase: "commit", retryable: true, partialWrite: false }); + expect(await (await handleZcodeDesktopRoutes(ctx, f.deps))?.json()).toMatchObject({ + connected: false, providerRegistered: false, error: "catalog_update_failed", + }); + expect(f.deps.readDesktopCatalogSlugs()).toHaveLength(2); + ctx.convergeCodexCatalog = converge; + const retry = context("/api/zcode-desktop/disconnect", {}, "gui-session"); + ctx.req = retry.req; ctx.url = retry.url; + expect(await (await handleZcodeDesktopRoutes(ctx, f.deps))?.json()).toMatchObject({ + connected: false, providerRegistered: false, activation: "disconnected", + }); + expect(f.deps.readDesktopCatalogSlugs()).toEqual([]); +}); + test("failed or skipped catalog and committed-but-missing models remain partial", async () => { const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); const converge = ctx.convergeCodexCatalog, connected = { ...status, connected: true }; @@ -181,9 +219,10 @@ function accountFixture() { const connected = new Set(); let hash = "a".repeat(64), failCatalog = false, active = false; let slugs: string[] = []; + const workspaceValidations: Array<{ path: string; accountId?: string }> = []; const deps = { resolveDesktopRuntime: (path: string) => path, - validateDesktopWorkspace: (path: string) => path, + validateDesktopWorkspace: (path: string, accountId?: string) => { workspaceValidations.push({ path, accountId }); return path; }, accountRuntimeBusy: () => active, desktopStatus: (accountId?: string) => ({ ...status, sandbox: false, accountId, connected: !!accountId && connected.has(accountId) }), connectDesktop: async (_r: string, _w: string, accountId?: string) => { @@ -211,9 +250,37 @@ function accountFixture() { await Promise.resolve(); await Promise.resolve(); return result; }; - return { ctx, deps, call, login, slugs: () => slugs, hash: (h: string) => { hash = h; }, + return { ctx, deps, call, login, slugs: () => slugs, workspaceValidations: () => workspaceValidations, + hash: (h: string) => { hash = h; }, failCatalog: (v: boolean) => { failCatalog = v; }, active: (v: boolean) => { active = v; } }; } +test("new accounts validate the displayed managed workspace in their eventual account scope", async () => { + const f = accountFixture(); + const account = await f.login({ workspace: defaultDesktopWorkspace() }); + expect(f.workspaceValidations()).toEqual([{ path: defaultDesktopWorkspace(account.accountId), accountId: account.accountId }]); + expect(await (await f.call("complete", { jobId: account.jobId })).json()).toMatchObject({ activation: "ready" }); +}); + +test("workspace validation failure removes the newly allocated draft account", async () => { + const f = accountFixture(); + f.deps.validateDesktopWorkspace = () => { throw new Error("workspace_invalid"); }; + expect(await (await f.call("login", { label: "Invalid", runtime: "/runtime", workspace: "/denied" })).json()) + .toEqual({ error: "workspace_invalid" }); + expect(listAccounts()).toEqual([]); + expect(readdirSync(join(home, "zcode-accounts"))).toEqual([]); +}); +test("reconnect validation failure removes only its draft and preserves the saved account", async () => { + const f = accountFixture(); + const account = await f.login(); + expect(await (await f.call("complete", { jobId: account.jobId })).json()).toMatchObject({ activation: "ready" }); + writeFileSync(join(accountProfile(account.accountId), "sentinel"), "original"); + f.deps.validateDesktopWorkspace = () => { throw new Error("workspace_invalid"); }; + expect(await f.login({ accountId: account.accountId, workspace: "/denied" })) + .toEqual({ error: "workspace_invalid" }); + expect(listAccounts().map(saved => saved.id)).toEqual([account.accountId]); + expect(readdirSync(join(home, "zcode-accounts"))).toEqual([account.accountId]); + expect(readFileSync(join(accountProfile(account.accountId), "sentinel"), "utf8")).toBe("original"); +}); test("manual accounts require GUI consent; account login enables an independent provider/catalog", async () => { const f = accountFixture(), originalDefault = f.ctx.config.defaultProvider; expect((await f.call("login", {}, "admin-token")).status).toBe(403); From f4a10958de732e0711e2c38de000d427eb883e35 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 05:25:14 -0300 Subject: [PATCH 20/43] fix(zcode): harden model and protocol verification --- .../src/content/docs/guides/zcode-agent.md | 10 +-- gui/src/components/ZcodeDesktopPane.tsx | 2 +- gui/src/i18n/de.ts | 8 +-- gui/src/i18n/en.ts | 8 +-- gui/src/i18n/fr.ts | 8 +-- gui/src/i18n/ja.ts | 8 +-- gui/src/i18n/ko.ts | 8 +-- gui/src/i18n/ru.ts | 8 +-- gui/src/i18n/tr.ts | 8 +-- gui/src/i18n/zh-TW.ts | 8 +-- gui/src/i18n/zh.ts | 8 +-- gui/tests/zcode-desktop-pane.test.tsx | 18 ++--- src/adapters/zcode/desktop-bootstrap.cjs | 6 +- src/adapters/zcode/desktop.ts | 65 ++++++++++++++----- src/adapters/zcode/settings.ts | 2 +- src/server/management/provider-routes.ts | 4 +- src/server/management/zcode-desktop-routes.ts | 26 ++------ structure/adapters/registry.md | 4 ++ structure/gui-and-management-api.md | 4 +- tests/providers/zcode-adapter.test.ts | 3 +- tests/providers/zcode-desktop-routes.test.ts | 53 ++++++++++++++- tests/providers/zcode-desktop.test.ts | 1 + 22 files changed, 175 insertions(+), 95 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 8a926f0bfa..e473055aa9 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -48,9 +48,9 @@ or subscription terms; those remain the vendor's policy. 5. Review the native file/command execution notice, check the consent box and click **Connect Desktop**. OpenCodex constructs the native launcher, verifies the protocol, enables the ZCode provider and updates the Codex catalog; no environment variables, API-key entry, token import or separate CLI login is required. -6. Optionally select a model and click **Test with one request**. This sends one brief prompt - through the official ZCode runtime and consumes account quota. Connecting alone checks the - local protocol and catalog, **not** account entitlement or inference. +6. Optionally select a model and click **Verify protocol again**. This repeats the official + model-catalog and `workspace/readState` checks without creating a model turn, running native + tools or consuming account quota. Only a real task can prove account entitlement and inference. 7. The provider is now available immediately. Select a ZCode model in your calling client. Only configured, enabled built-in Z.AI profiles are exposed by this workflow; custom Desktop providers and routes back to OpenCodex are not imported. @@ -161,8 +161,8 @@ an advanced operator-supplied launcher. Managed consent and installation/workspace paths are stored privately under `$OPENCODEX_HOME/zcode-desktop/`, separately from provider configuration. Data-plane requests cannot choose an executable or override these paths. Status, folder browsing, connecting, -disconnecting and the optional quota-spending test require the dashboard's authenticated GUI -session, not a raw API/admin token. +disconnecting and the optional tool-free protocol recheck require the dashboard's authenticated +GUI session, not a raw API/admin token. ## Advanced: operator-supplied isolated launcher diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index 976cb532fc..bc21205d11 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -72,7 +72,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: : error === "platform_unsupported" ? t("zcodeDesktop.platformUnsupported") : error === "profile_missing" || error === "models_missing" ? t("zcodeDesktop.loginNeeded") : error === "workspace_invalid" ? t("zcodeDesktop.workspaceInvalid") - : error === "inference_failed" ? t("zcodeDesktop.inferenceFailed") + : error === "protocol_failed" ? t("zcodeDesktop.protocolFailed") : error === "provider_registration_failed" ? t("zcodeDesktop.providerPending") : error === "catalog_update_failed" ? t("zcodeDesktop.catalogPending") : error ? t("zcodeDesktop.failed") : ""; diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 255eeeb952..82e8a5ca31 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -51,10 +51,10 @@ export const de: Record = { "zcodeDesktop.back": "Zurück", "zcodeDesktop.connect": "Desktop verbinden", "zcodeDesktop.disconnect": "Trennen", - "zcodeDesktop.testHint": "Optional: Eine kurze Anfrage über ZCode prüft den Modellzugriff und verbraucht Kontingent. Die Verbindung allein verbraucht keines.", + "zcodeDesktop.testHint": "Optional: Prüfen Sie das offizielle ZCode-Protokoll und das ausgewählte Modell erneut, ohne einen Modelllauf oder native Werkzeuge zu starten oder Kontingent zu verbrauchen.", "zcodeDesktop.model": "Modell", - "zcodeDesktop.test": "Mit einer Anfrage testen", - "zcodeDesktop.testPassed": "ZCode hat erfolgreich geantwortet.", + "zcodeDesktop.test": "Protokoll erneut prüfen", + "zcodeDesktop.testPassed": "Die ZCode-Protokollprüfung war erfolgreich.", "zcodeDesktop.missing": "Öffnen Sie ZCode Desktop auf dem Proxy-Computer und starten Sie die Erkennung erneut, oder wählen Sie den Installationsordner.", "zcodeDesktop.sandboxUnavailable": "Bubblewrap kann im Sicherheitskontext des Servers keine Sandbox erstellen. Lassen Sie User-Namespaces und AppArmor prüfen und starten Sie die Erkennung erneut. Die Isolation bleibt aktiv.", "zcodeDesktop.sandboxMissing": "Installieren Sie bubblewrap mit dem Linux-Paketmanager für die Isolation und starten Sie die Erkennung erneut.", @@ -63,7 +63,7 @@ export const de: Record = { "zcodeDesktop.platformUnsupported": "Die verwaltete Desktop-Verbindung unterstützt derzeit nur Linux. Andere Plattformen benötigen den erweiterten isolierten Starter.", "zcodeDesktop.loginNeeded": "Melden Sie sich in ZCode Desktop an und richten Sie ein Z.AI-Modell ein. Verbinden Sie danach erneut.", "zcodeDesktop.workspaceInvalid": "Wählen Sie einen vorhandenen Projektordner, nicht den Home-, System- oder Zugangsdatenordner.", - "zcodeDesktop.inferenceFailed": "Der ZCode-Test ist fehlgeschlagen. Prüfen Sie Anmeldung und Modellzugriff in Desktop. Keine direkte API wurde als Ersatz verwendet.", + "zcodeDesktop.protocolFailed": "Die ZCode-Protokollprüfung wurde nicht abgeschlossen. Prüfen Sie Anmeldung und Modellzugriff in Desktop und verbinden Sie erneut. Es wurden weder Modelllauf noch natives Werkzeug noch direkter API-Ersatz verwendet.", "zcodeDesktop.failed": "Die offizielle Laufzeit konnte nicht verbunden werden. Prüfen Sie Desktop, den Ordner und die Isolationsvoraussetzungen.", "usage.incomplete": "Einige Nutzungsdatensätze konnten nicht berücksichtigt werden. Anzahlen, Datumsangaben und Ranglisten beruhen nur auf lesbaren Datensätzen.", "models.pickerOrder.usageIncomplete": "Die Reihenfolge nach Nutzung kann wegen unvollständiger Nutzungsdaten nicht gespeichert werden. Wählen Sie eine andere Reihenfolge oder reparieren Sie zuerst den Verlauf.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 7fd4e963a4..8d47c6e0b9 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -52,10 +52,10 @@ export const en = { "zcodeDesktop.back": "Back", "zcodeDesktop.connect": "Connect Desktop", "zcodeDesktop.disconnect": "Disconnect", - "zcodeDesktop.testHint": "Optional: send one short request through ZCode to verify model access. This consumes account quota; connecting alone does not.", + "zcodeDesktop.testHint": "Optional: recheck the official ZCode protocol and selected model without starting a model turn, running native tools, or consuming quota.", "zcodeDesktop.model": "Model", - "zcodeDesktop.test": "Test with one request", - "zcodeDesktop.testPassed": "ZCode answered successfully.", + "zcodeDesktop.test": "Verify protocol again", + "zcodeDesktop.testPassed": "ZCode protocol verification passed.", "zcodeDesktop.missing": "Open ZCode Desktop on the proxy computer, then detect again. You can also select its installed or extracted application folder.", "zcodeDesktop.sandboxUnavailable": "Bubblewrap cannot create the required sandbox in the server’s security context. Ask your administrator to check user namespace and AppArmor policy, then detect again. Isolation will not be disabled.", "zcodeDesktop.sandboxMissing": "Bubblewrap is required for isolation. Install bubblewrap using your Linux package manager, then detect again.", @@ -64,7 +64,7 @@ export const en = { "zcodeDesktop.platformUnsupported": "Managed Desktop connection currently supports Linux only. Other platforms can use the advanced isolated-launcher setup.", "zcodeDesktop.loginNeeded": "Sign in and configure a Z.AI model in ZCode Desktop, then detect and connect again.", "zcodeDesktop.workspaceInvalid": "Choose an existing project folder, not your home, a system folder or a credentials/configuration directory.", - "zcodeDesktop.inferenceFailed": "ZCode did not complete the test. Check login and model access in Desktop, then reconnect. No direct API fallback was used.", + "zcodeDesktop.protocolFailed": "ZCode protocol verification did not complete. Check login and model access in Desktop, then reconnect. No model turn, native tool, or direct API fallback was used.", "zcodeDesktop.failed": "Could not connect to the official runtime. Check Desktop, the selected folder and the isolation prerequisites, then retry.", "usage.incomplete": "Some usage records could not be included. Counts, dates, and rankings reflect readable records only.", "models.pickerOrder.usageIncomplete": "Cannot save most-used order because usage history is incomplete. Choose another order or repair the history first.", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index aa8023d43d..5216cf7ac5 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -50,10 +50,10 @@ export const fr: Record = { "zcodeDesktop.back": "Retour", "zcodeDesktop.connect": "Connecter Desktop", "zcodeDesktop.disconnect": "Déconnecter", - "zcodeDesktop.testHint": "Facultatif : une courte requête via ZCode vérifie l’accès au modèle et consomme du quota. La connexion seule ne consomme pas de quota.", + "zcodeDesktop.testHint": "Facultatif : revérifiez le protocole officiel ZCode et le modèle sélectionné sans lancer de tour de modèle, d’outil natif ni consommer de quota.", "zcodeDesktop.model": "Modèle", - "zcodeDesktop.test": "Tester avec une requête", - "zcodeDesktop.testPassed": "ZCode a répondu correctement.", + "zcodeDesktop.test": "Revérifier le protocole", + "zcodeDesktop.testPassed": "La vérification du protocole ZCode a réussi.", "zcodeDesktop.missing": "Ouvrez ZCode Desktop sur l’ordinateur du proxy, puis relancez la détection, ou indiquez son dossier installé ou extrait.", "zcodeDesktop.sandboxUnavailable": "Bubblewrap ne peut pas créer le bac à sable dans le contexte de sécurité du serveur. Faites vérifier les espaces de noms utilisateur et AppArmor, puis relancez la détection. L’isolation ne sera pas désactivée.", "zcodeDesktop.sandboxMissing": "Installez bubblewrap avec le gestionnaire de paquets Linux pour activer l’isolation, puis relancez la détection.", @@ -62,7 +62,7 @@ export const fr: Record = { "zcodeDesktop.platformUnsupported": "La connexion Desktop gérée prend actuellement en charge Linux uniquement. Les autres plateformes nécessitent le lanceur isolé avancé.", "zcodeDesktop.loginNeeded": "Connectez-vous et configurez un modèle Z.AI dans ZCode Desktop, puis reconnectez le fournisseur.", "zcodeDesktop.workspaceInvalid": "Choisissez un dossier de projet existant, pas le dossier personnel, système ou des identifiants.", - "zcodeDesktop.inferenceFailed": "Le test ZCode a échoué. Vérifiez la connexion et l’accès au modèle dans Desktop, puis reconnectez. Aucun repli vers une API directe.", + "zcodeDesktop.protocolFailed": "La vérification du protocole ZCode n’a pas abouti. Vérifiez la connexion et l’accès au modèle dans Desktop, puis reconnectez. Aucun tour de modèle, outil natif ou repli vers une API directe n’a été utilisé.", "zcodeDesktop.failed": "Connexion au runtime officiel impossible. Vérifiez Desktop, le dossier et les prérequis d’isolation.", "usage.incomplete": "Certains enregistrements d’utilisation n’ont pas pu être inclus. Les totaux, dates et classements reposent uniquement sur les enregistrements lisibles.", "models.pickerOrder.usageIncomplete": "Impossible d’enregistrer l’ordre par utilisation : l’historique est incomplet. Choisissez un autre ordre ou réparez d’abord l’historique.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 2f4caad844..75ac5f3ac8 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -50,10 +50,10 @@ export const ja: Record = { "zcodeDesktop.back": "戻る", "zcodeDesktop.connect": "Desktop に接続", "zcodeDesktop.disconnect": "切断", - "zcodeDesktop.testHint": "任意:ZCode 経由で短いリクエストを1回送信し、モデルへのアクセスを確認します。割り当てを消費します。接続だけでは消費しません。", + "zcodeDesktop.testHint": "任意:モデルターンやネイティブツールを開始せず、クォータも消費せずに、公式 ZCode プロトコルと選択したモデルを再確認します。", "zcodeDesktop.model": "モデル", - "zcodeDesktop.test": "1回のリクエストでテスト", - "zcodeDesktop.testPassed": "ZCode が正常に応答しました。", + "zcodeDesktop.test": "プロトコルを再確認", + "zcodeDesktop.testPassed": "ZCode プロトコルの確認に成功しました。", "zcodeDesktop.missing": "プロキシのコンピューターで ZCode Desktop を開いて再検出するか、インストールまたは展開先を選択してください。", "zcodeDesktop.sandboxUnavailable": "サーバーのセキュリティコンテキストで Bubblewrap の隔離環境を作成できません。管理者にユーザー名前空間と AppArmor の設定を確認してもらい、再検出してください。隔離は無効化されません。", "zcodeDesktop.sandboxMissing": "隔離には Bubblewrap が必要です。Linux のパッケージマネージャーでインストールして再検出してください。", @@ -62,7 +62,7 @@ export const ja: Record = { "zcodeDesktop.platformUnsupported": "管理された Desktop 接続は現在 Linux のみ対応です。他の環境では詳細な隔離ランチャー設定を使用してください。", "zcodeDesktop.loginNeeded": "ZCode Desktop にサインインして Z.AI モデルを設定し、再接続してください。", "zcodeDesktop.workspaceInvalid": "ホーム、システム、認証情報のフォルダーではなく、既存のプロジェクトフォルダーを選択してください。", - "zcodeDesktop.inferenceFailed": "ZCode のテストが完了しませんでした。Desktop でログインとモデルの権限を確認して再接続してください。直接 API へのフォールバックはありません。", + "zcodeDesktop.protocolFailed": "ZCode プロトコルの確認が完了しませんでした。Desktop でログインとモデルの権限を確認して再接続してください。モデルターン、ネイティブツール、直接 API へのフォールバックは使用されていません。", "zcodeDesktop.failed": "公式ランタイムに接続できません。Desktop、フォルダー、隔離の前提条件を確認してください。", "usage.incomplete": "一部の使用履歴を集計できませんでした。回数、日付、順位は読み取れる記録のみを反映しています。", "models.pickerOrder.usageIncomplete": "使用履歴が不完全なため、使用回数順を保存できません。別の順序を選ぶか、履歴を修復してください。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index a7cb7594a8..fcdb3a8833 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -50,10 +50,10 @@ export const ko: Record = { "zcodeDesktop.back": "뒤로", "zcodeDesktop.connect": "Desktop 연결", "zcodeDesktop.disconnect": "연결 해제", - "zcodeDesktop.testHint": "선택 사항: ZCode를 통해 짧은 요청을 한 번 보내 모델 접근을 확인합니다. 계정 할당량을 사용합니다. 연결만으로는 사용하지 않습니다.", + "zcodeDesktop.testHint": "선택 사항: 모델 턴이나 네이티브 도구를 시작하거나 할당량을 사용하지 않고 공식 ZCode 프로토콜과 선택한 모델을 다시 확인합니다.", "zcodeDesktop.model": "모델", - "zcodeDesktop.test": "요청 한 번으로 테스트", - "zcodeDesktop.testPassed": "ZCode가 정상적으로 응답했습니다.", + "zcodeDesktop.test": "프로토콜 다시 확인", + "zcodeDesktop.testPassed": "ZCode 프로토콜 확인에 성공했습니다.", "zcodeDesktop.missing": "프록시 컴퓨터에서 ZCode Desktop을 열고 다시 감지하거나 설치 또는 압축 해제 폴더를 선택하세요.", "zcodeDesktop.sandboxUnavailable": "서버 보안 컨텍스트에서 Bubblewrap 격리 환경을 만들 수 없습니다. 관리자에게 사용자 네임스페이스와 AppArmor 정책 확인을 요청한 뒤 다시 감지하세요. 격리는 비활성화되지 않습니다.", "zcodeDesktop.sandboxMissing": "격리를 위해 Linux 패키지 관리자로 bubblewrap을 설치한 뒤 다시 감지하세요.", @@ -62,7 +62,7 @@ export const ko: Record = { "zcodeDesktop.platformUnsupported": "관리형 Desktop 연결은 현재 Linux만 지원합니다. 다른 플랫폼에서는 고급 격리 실행기 설정을 사용하세요.", "zcodeDesktop.loginNeeded": "ZCode Desktop에서 로그인하고 Z.AI 모델을 설정한 뒤 다시 연결하세요.", "zcodeDesktop.workspaceInvalid": "홈, 시스템 또는 인증 정보 폴더가 아닌 기존 프로젝트 폴더를 선택하세요.", - "zcodeDesktop.inferenceFailed": "ZCode 테스트가 완료되지 않았습니다. Desktop 로그인과 모델 권한을 확인하고 다시 연결하세요. 직접 API로 대체하지 않았습니다.", + "zcodeDesktop.protocolFailed": "ZCode 프로토콜 확인이 완료되지 않았습니다. Desktop 로그인과 모델 권한을 확인하고 다시 연결하세요. 모델 턴, 네이티브 도구 또는 직접 API 대체는 사용되지 않았습니다.", "zcodeDesktop.failed": "공식 런타임에 연결할 수 없습니다. Desktop, 폴더 및 격리 요구 사항을 확인하세요.", "usage.incomplete": "일부 사용량 기록을 집계하지 못했습니다. 횟수, 날짜, 순위는 읽을 수 있는 기록만 반영합니다.", "models.pickerOrder.usageIncomplete": "사용량 이력이 불완전해 많이 사용한 순서를 저장할 수 없습니다. 다른 순서를 선택하거나 이력을 복구하세요.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 2f53822ae8..2a34fa6315 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -50,10 +50,10 @@ export const ru: Record = { "zcodeDesktop.back": "Назад", "zcodeDesktop.connect": "Подключить Desktop", "zcodeDesktop.disconnect": "Отключить", - "zcodeDesktop.testHint": "Необязательно: один короткий запрос через ZCode проверит доступ к модели и потратит квоту. Само подключение квоту не расходует.", + "zcodeDesktop.testHint": "Необязательно: повторно проверьте официальный протокол ZCode и выбранную модель без запуска модельного запроса, нативных инструментов и расхода квоты.", "zcodeDesktop.model": "Модель", - "zcodeDesktop.test": "Проверить одним запросом", - "zcodeDesktop.testPassed": "ZCode успешно ответил.", + "zcodeDesktop.test": "Повторно проверить протокол", + "zcodeDesktop.testPassed": "Проверка протокола ZCode прошла успешно.", "zcodeDesktop.missing": "Откройте ZCode Desktop на компьютере прокси и повторите поиск либо выберите папку установки или распаковки.", "zcodeDesktop.sandboxUnavailable": "Bubblewrap не может создать изоляцию в контексте безопасности сервера. Попросите администратора проверить пользовательские пространства имён и AppArmor, затем повторите поиск. Изоляция не будет отключена.", "zcodeDesktop.sandboxMissing": "Для изоляции установите bubblewrap через менеджер пакетов Linux и повторите поиск.", @@ -62,7 +62,7 @@ export const ru: Record = { "zcodeDesktop.platformUnsupported": "Управляемое подключение Desktop пока поддерживает только Linux. Для других платформ используйте расширенную настройку изолированного запуска.", "zcodeDesktop.loginNeeded": "Войдите и настройте модель Z.AI в ZCode Desktop, затем подключитесь снова.", "zcodeDesktop.workspaceInvalid": "Выберите существующую папку проекта, а не домашнюю, системную или папку с учётными данными.", - "zcodeDesktop.inferenceFailed": "ZCode не завершил проверку. Проверьте вход и доступ к модели в Desktop. Прямая API-подмена не использовалась.", + "zcodeDesktop.protocolFailed": "Проверка протокола ZCode не завершилась. Проверьте вход и доступ к модели в Desktop и подключитесь снова. Модельный запрос, нативные инструменты и прямой резервный API не использовались.", "zcodeDesktop.failed": "Не удалось подключить официальный runtime. Проверьте Desktop, папку и требования изоляции.", "usage.incomplete": "Часть записей об использовании не удалось учесть. Счётчики, даты и рейтинги основаны только на читаемых записях.", "models.pickerOrder.usageIncomplete": "Нельзя сохранить порядок по частоте использования: история неполная. Выберите другой порядок или сначала восстановите историю.", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 3c21dc7a34..495495616a 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -51,10 +51,10 @@ export const tr: Record = { "zcodeDesktop.back": "Geri", "zcodeDesktop.connect": "Desktop bağla", "zcodeDesktop.disconnect": "Bağlantıyı kes", - "zcodeDesktop.testHint": "İsteğe bağlı: ZCode üzerinden kısa bir istek model erişimini doğrular ve hesap kotasını kullanır. Yalnızca bağlantı kurmak kota kullanmaz.", + "zcodeDesktop.testHint": "İsteğe bağlı: Model turu veya yerel araç başlatmadan ve kota tüketmeden resmi ZCode protokolünü ve seçili modeli yeniden doğrulayın.", "zcodeDesktop.model": "Model", - "zcodeDesktop.test": "Bir istekle test et", - "zcodeDesktop.testPassed": "ZCode başarıyla yanıt verdi.", + "zcodeDesktop.test": "Protokolü yeniden doğrula", + "zcodeDesktop.testPassed": "ZCode protokol doğrulaması başarılı.", "zcodeDesktop.missing": "Proxy bilgisayarında ZCode Desktop açıp yeniden algılayın veya kurulum ya da çıkarılmış uygulama klasörünü seçin.", "zcodeDesktop.sandboxUnavailable": "Bubblewrap sunucunun güvenlik bağlamında yalıtım oluşturamıyor. Yöneticinizden kullanıcı ad alanı ve AppArmor politikasını kontrol etmesini isteyip yeniden algılayın. Yalıtım kapatılmayacaktır.", "zcodeDesktop.sandboxMissing": "Yalıtım için Linux paket yöneticisiyle bubblewrap kurup yeniden algılayın.", @@ -63,7 +63,7 @@ export const tr: Record = { "zcodeDesktop.platformUnsupported": "Yönetilen Desktop bağlantısı şu anda yalnızca Linux destekler. Diğer platformlarda gelişmiş yalıtılmış başlatıcıyı kullanın.", "zcodeDesktop.loginNeeded": "ZCode Desktop içinde oturum açıp bir Z.AI modeli yapılandırın, ardından yeniden bağlanın.", "zcodeDesktop.workspaceInvalid": "Ev, sistem veya kimlik bilgileri klasörü yerine mevcut bir proje klasörü seçin.", - "zcodeDesktop.inferenceFailed": "ZCode testi tamamlayamadı. Desktop oturumunu ve model erişimini kontrol edip yeniden bağlanın. Doğrudan API yedeği kullanılmadı.", + "zcodeDesktop.protocolFailed": "ZCode protokol doğrulaması tamamlanmadı. Desktop oturumunu ve model erişimini kontrol edip yeniden bağlanın. Model turu, yerel araç veya doğrudan API yedeği kullanılmadı.", "zcodeDesktop.failed": "Resmî çalışma zamanına bağlanılamadı. Desktop, klasör ve yalıtım gereksinimlerini kontrol edin.", "usage.incomplete": "Bazı kullanım kayıtları dahil edilemedi. Sayılar, tarihler ve sıralamalar yalnızca okunabilir kayıtlara dayanır.", "models.pickerOrder.usageIncomplete": "Kullanım geçmişi eksik olduğundan en çok kullanılan sıralaması kaydedilemiyor. Başka bir sıralama seçin veya önce geçmişi onarın.", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 5589daca78..c0f8817f93 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -48,10 +48,10 @@ export const zhTW: Record = { "zcodeDesktop.back": "返回", "zcodeDesktop.connect": "連接 Desktop", "zcodeDesktop.disconnect": "中斷連接", - "zcodeDesktop.testHint": "選用:透過 ZCode 傳送一次簡短請求以驗證模型存取權限。這會消耗帳戶配額;僅連接不會消耗。", + "zcodeDesktop.testHint": "選用:重新驗證官方 ZCode 協定和所選模型,不會啟動模型回合、執行原生工具或消耗配額。", "zcodeDesktop.model": "模型", - "zcodeDesktop.test": "傳送一次測試請求", - "zcodeDesktop.testPassed": "ZCode 已成功回應。", + "zcodeDesktop.test": "重新驗證協定", + "zcodeDesktop.testPassed": "ZCode 協定驗證成功。", "zcodeDesktop.missing": "請在代理所在電腦開啟 ZCode Desktop 後重新偵測,或選取安裝或解壓縮目錄。", "zcodeDesktop.sandboxUnavailable": "Bubblewrap 無法在伺服器的安全環境中建立隔離。請管理員檢查使用者命名空間與 AppArmor 政策後重新偵測。不會停用隔離。", "zcodeDesktop.sandboxMissing": "隔離需要 Bubblewrap。請透過 Linux 套件管理員安裝 bubblewrap 後重新偵測。", @@ -60,7 +60,7 @@ export const zhTW: Record = { "zcodeDesktop.platformUnsupported": "受管理的 Desktop 連接目前僅支援 Linux。其他平台可使用進階隔離啟動器設定。", "zcodeDesktop.loginNeeded": "請在 ZCode Desktop 登入並設定 Z.AI 模型,然後重新連接。", "zcodeDesktop.workspaceInvalid": "請選取現有專案目錄,而非家目錄、系統目錄或憑證設定目錄。", - "zcodeDesktop.inferenceFailed": "ZCode 未完成測試。請在 Desktop 檢查登入狀態及模型權限後重新連接。未使用直接 API 備援。", + "zcodeDesktop.protocolFailed": "ZCode 協定驗證未完成。請在 Desktop 檢查登入狀態及模型權限後重新連接。未啟動模型回合、原生工具或直接 API 備援。", "zcodeDesktop.failed": "無法連接官方執行環境。請檢查 Desktop、目錄與隔離依賴後重試。", "usage.incomplete": "部分用量記錄無法納入。次數、日期和排名僅反映可讀取的記錄。", "models.pickerOrder.usageIncomplete": "用量歷史不完整,無法儲存最常用排序。請選擇其他排序或先修復歷史記錄。", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index d6b60a38ca..620e34a233 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -50,10 +50,10 @@ export const zh: Record = { "zcodeDesktop.back": "返回", "zcodeDesktop.connect": "连接 Desktop", "zcodeDesktop.disconnect": "断开连接", - "zcodeDesktop.testHint": "可选:通过 ZCode 发送一次简短请求以验证模型访问权限。这会消耗账户配额;仅连接不会消耗。", + "zcodeDesktop.testHint": "可选:重新验证官方 ZCode 协议和所选模型,不会启动模型轮次、运行原生工具或消耗配额。", "zcodeDesktop.model": "模型", - "zcodeDesktop.test": "发送一次测试请求", - "zcodeDesktop.testPassed": "ZCode 已成功响应。", + "zcodeDesktop.test": "重新验证协议", + "zcodeDesktop.testPassed": "ZCode 协议验证成功。", "zcodeDesktop.missing": "请在代理所在计算机上打开 ZCode Desktop 后重新检测,或选择其安装或解压目录。", "zcodeDesktop.sandboxUnavailable": "Bubblewrap 无法在服务器的安全上下文中创建隔离环境。请管理员检查用户命名空间和 AppArmor 策略,然后重新检测。不会禁用隔离。", "zcodeDesktop.sandboxMissing": "隔离需要 Bubblewrap。请通过 Linux 包管理器安装 bubblewrap 后重新检测。", @@ -62,7 +62,7 @@ export const zh: Record = { "zcodeDesktop.platformUnsupported": "托管 Desktop 连接目前仅支持 Linux。其他平台可以使用高级隔离启动器配置。", "zcodeDesktop.loginNeeded": "请在 ZCode Desktop 中登录并配置 Z.AI 模型,然后重新连接。", "zcodeDesktop.workspaceInvalid": "请选择现有项目目录,而非主目录、系统目录或凭据配置目录。", - "zcodeDesktop.inferenceFailed": "ZCode 未完成测试。请在 Desktop 中检查登录状态及模型权限后重新连接。未使用直接 API 回退。", + "zcodeDesktop.protocolFailed": "ZCode 协议验证未完成。请在 Desktop 中检查登录状态及模型权限后重新连接。未启动模型轮次、原生工具或直接 API 回退。", "zcodeDesktop.failed": "无法连接官方运行时。请检查 Desktop、所选目录和隔离依赖后重试。", "usage.incomplete": "部分用量记录无法计入。次数、日期和排名仅反映可读取的记录。", "models.pickerOrder.usageIncomplete": "用量历史不完整,无法保存最常用排序。请选择其他排序或先修复历史记录。", diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 7a61727ca7..14aa10d033 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -85,26 +85,26 @@ test("Desktop connect requires explicit consent and does not automatically spend expect(host.textContent).not.toContain("Use this provider"); expect(requests.some(r => r.path === "/api/providers")).toBe(false); }); -test("one-request test is a separate explicit quota-spending action", async () => { +test("protocol recheck is a separate tool-free action that does not claim quota use", async () => { await mountPane(); await click(host.querySelector('input[type="checkbox"]')!); await click(button("Connect Desktop")); - expect(host.textContent).toContain("consumes account quota"); - await click(button("Test with one request")); + expect(host.textContent).toContain("without starting a model turn, running native tools, or consuming quota"); + await click(button("Verify protocol again")); expect(requests.find(r => r.path.endsWith("/test"))?.body).toEqual({ model: "builtin:zai/model", consent: true }); - expect(host.textContent).toContain("ZCode answered successfully."); + expect(host.textContent).toContain("ZCode protocol verification passed."); }); -test("a successful HTTP response with a failed inference shows the actionable error", async () => { +test("a successful HTTP response with a failed protocol recheck shows the actionable error", async () => { Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL) => { const path = new URL(String(input), "http://localhost").pathname; - if (path.endsWith("/test")) return Response.json({ ok: false, error: "inference_failed" }); + if (path.endsWith("/test")) return Response.json({ ok: false, error: "protocol_failed" }, { status: 400 }); return Response.json({ connected: true, activation: "ready", providerName: "zcode", runtimes: ["/installed/ZCode"], runtime: "/installed/ZCode", workspace: "/project", models: [{ id: "builtin:zai/model", label: "Model" }] }); } }); await mountPane(); - await click(button("Test with one request")); - expect(host.querySelector('[role="alert"]')?.textContent).toContain("did not complete the test"); - expect(host.textContent).not.toContain("ZCode answered successfully."); + await click(button("Verify protocol again")); + expect(host.querySelector('[role="alert"]')?.textContent).toContain("protocol verification did not complete"); + expect(host.textContent).not.toContain("ZCode protocol verification passed."); }); test("incompatible Node preflight shows safe actionable guidance without connecting", async () => { diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs index 5acdb5cb0d..6bc68b9147 100644 --- a/src/adapters/zcode/desktop-bootstrap.cjs +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -25,9 +25,9 @@ function normalizeDesktopConfig(input) { models: raw.models && typeof raw.models === "object" && !Array.isArray(raw.models) ? raw.models : {}, }; } - const first = Object.entries(provider).find(([, p]) => Object.keys(p.models).length); - const model = first ? `${first[0]}/${Object.keys(first[1].models)[0]}` : undefined; - return { provider, ...(model ? { model: { main: model, lite: model } } : {}) }; + const config = { provider }; + const model = desktopModelCatalog(config)[0]?.id; + return { ...config, ...(model ? { model: { main: model, lite: model } } : {}) }; } function desktopModelCatalog(config) { diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index f721ffbbf7..b3409d7764 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -1,5 +1,5 @@ import { accountRoot, accountProfile, readAccount } from "./accounts"; -import { constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs"; +import { constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path"; import { homedir } from "node:os"; import { createHash, randomUUID } from "node:crypto"; @@ -218,6 +218,34 @@ export function desktopStatus(accountId?: string) { } const connecting = new Set(); + +function normalizeProtocolModels(value: unknown): DesktopModel[] { + if (!Array.isArray(value) || !value.length || value.length > 1000) return fail("models_missing"); + const models = value.filter((m: DesktopModel) => typeof m.id === "string" && typeof m.providerId === "string" + && m.providerId.startsWith("builtin:zai") && typeof m.modelId === "string" && !/[\x00-\x20]/.test(m.id) + && m.id === `${m.providerId}/${m.modelId}`).map((m: DesktopModel) => ({ + id: m.id, providerId: m.providerId, modelId: m.modelId, label: String(m.label).slice(0, 240), + ...(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow) && m.contextWindow > 0 ? { contextWindow: m.contextWindow } : {}), + })); + if (!models.length) return fail("models_missing"); + return models; +} + +async function verifyProtocol(settings: ZcodeSettings, requiredModel?: string, signal?: AbortSignal): Promise { + const client = new ZcodeClient(settings); + const cancel = () => { void client.close(); }; + signal?.addEventListener("abort", cancel, { once: true }); + try { + if (signal?.aborted) return fail("runtime_failed"); + const result = await client.request("opencodex/desktopModels", {}, 15_000); + const models = normalizeProtocolModels(result.models); + if (requiredModel && !models.some(model => model.id === requiredModel)) return fail("models_missing"); + // Protocol metadata only: never create/send a model turn or permit native tool execution. + await client.request("workspace/readState", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace } }, 15_000); + return models; + } finally { signal?.removeEventListener("abort", cancel); await client.close(); } +} + export async function connectDesktop(runtime: string, workspace: string, accountId?: string): Promise> { if (connecting.has(accountId ?? "desktop")) return fail("busy"); if (accountId && hasZcodeAccountClients(accountId)) return fail("busy"); @@ -226,20 +254,7 @@ export async function connectDesktop(runtime: string, workspace: string, account const connection: Connection = { version: 1, connected: true, generation: randomUUID(), runtime: resolveDesktopRuntime(runtime), workspace: validateDesktopWorkspace(workspace, accountId), models: [] }; const settings = settingsFor(connection, accountId); - const client = new ZcodeClient(settings); - try { - const result = await client.request("opencodex/desktopModels", {}, 15_000); - if (!Array.isArray(result.models) || !result.models.length || result.models.length > 1000) return fail("models_missing"); - connection.models = result.models.filter((m: DesktopModel) => typeof m.id === "string" && typeof m.providerId === "string" - && m.providerId.startsWith("builtin:zai") && typeof m.modelId === "string" && !/[\x00-\x20]/.test(m.id) - && m.id === `${m.providerId}/${m.modelId}`).map((m: DesktopModel) => ({ - id: m.id, providerId: m.providerId, modelId: m.modelId, label: String(m.label).slice(0, 240), - ...(typeof m.contextWindow === "number" && Number.isFinite(m.contextWindow) && m.contextWindow > 0 ? { contextWindow: m.contextWindow } : {}), - })); - if (!connection.models.length) return fail("models_missing"); - // Official protocol readiness only. This does not send a prompt or spend inference quota. - await client.request("workspace/readState", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace } }, 15_000); - } finally { await client.close(); } + connection.models = await verifyProtocol(settings); if (!accountId) await closeZcodeDesktopClients(); persist(connection, accountId); return desktopStatus(accountId); @@ -247,6 +262,26 @@ export async function connectDesktop(runtime: string, workspace: string, account finally { connecting.delete(accountId ?? "desktop"); } } +/** Recheck the official protocol without creating a model turn or exposing native tools. */ +export async function verifyDesktopProtocol(requiredModel: string, signal?: AbortSignal, accountId?: string): Promise { + const key = accountId ?? "desktop"; + if (connecting.has(key) || (accountId && hasZcodeAccountClients(accountId))) return fail("busy"); + connecting.add(key); + const generation = randomUUID(); + try { + const connection = readConnection(accountId); + if (!connection?.connected) return fail("disconnected"); + const settings = settingsFor({ ...connection, generation }, accountId); + await verifyProtocol(settings, requiredModel, signal); + } catch (error) { + if (error instanceof DesktopSetupError) throw error; + return fail("runtime_failed"); + } finally { + rmSync(join(root(accountId), "home", generation), { recursive: true, force: true }); + connecting.delete(key); + } +} + export async function disconnectDesktop(accountId?: string): Promise { if (connecting.has(accountId ?? "desktop")) return fail("busy"); if (accountId && hasZcodeAccountClients(accountId)) return fail("busy"); diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index c91233c2ea..7c8338011d 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -108,7 +108,7 @@ function readModels(settings: ZcodeSettings): ZcodeModel[] { let url: URL; try { url = new URL(options.baseURL); } catch { continue; } if (url.protocol !== "https:" || url.username || url.password - || /^(localhost|127\.|0\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|\[|.*\.localhost$)/i.test(url.hostname)) continue; + || /^(localhost|127\.|0\.|10\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|\[|.*\.localhost$)/i.test(url.hostname)) continue; } const models = Object.entries(record(provider.models)); for (const [modelId, raw] of models) { diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index f948106f79..bce23ae2d0 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -1554,7 +1554,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise 0, models: models.length, latencyMs: 0, + if (!models.length) return jsonResponse({ ok: false, models: 0, latencyMs: 0, + error: "ZCode local catalog has no available models." }); + return jsonResponse({ ok: true, models: models.length, latencyMs: 0, message: "Local catalog loaded. Account inference is validated only by an explicit agent turn." }); } catch { return jsonResponse({ ok: false, latencyMs: 0, diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts index 048e9eb1a4..6b56995387 100644 --- a/src/server/management/zcode-desktop-routes.ts +++ b/src/server/management/zcode-desktop-routes.ts @@ -1,14 +1,12 @@ import { jsonResponse } from "../auth-cors"; import type { ManagementContext } from "./context"; -import { connectDesktop, desktopStatus, desktopFolders, DesktopSetupError, disconnectDesktop } from "../../adapters/zcode/desktop"; -import { createZcodeAdapter } from "../../adapters/zcode/adapter"; -import { createTranslatorBudget } from "../../lib/translator-budget"; +import { connectDesktop, desktopStatus, desktopFolders, DesktopSetupError, disconnectDesktop, verifyDesktopProtocol } from "../../adapters/zcode/desktop"; import { invalidateCodexModelsCache } from "../../codex/catalog"; import { clearGatherRoutedModelsInflight } from "../../codex/catalog/provider-fetch"; import { readBoundedJsonRequestBody } from "../request-decompress"; import { activateDesktopProvider, deactivateDesktopProvider, desktopActivation, readDesktopCatalogSlugs } from "./zcode-desktop-activation"; -const services = { connectDesktop, desktopStatus, disconnectDesktop, readDesktopCatalogSlugs }; +const services = { connectDesktop, desktopStatus, disconnectDesktop, verifyDesktopProtocol, readDesktopCatalogSlugs }; let activating = false; let testing = false; export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = services): Promise { @@ -49,24 +47,14 @@ export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = se const status = deps.desktopStatus(); if (body.consent !== true || !status.connected || !status.models.some(m => m.id === body.model)) return jsonResponse({ error: "invalid_request" }, 400); testing = true; - const budget = createTranslatorBudget(); - const deadline = AbortSignal.timeout(60_000); - let completed = false; - let textSeen = false; try { - await createZcodeAdapter({ adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai" }).runTurn!({ - modelId: String(body.model), stream: false, options: {}, context: { messages: [ - { role: "user", timestamp: Date.now(), content: "Reply with exactly ZCODE_CONNECTION_OK. Do not use any tools." }, - ] }, - }, { headers: new Headers(), abortSignal: AbortSignal.any([req.signal, deadline]), translatorBudget: budget }, event => { - if (event.type === "text_delta" && event.phase !== "commentary" && event.text.trim()) textSeen = true; - if (event.type === "done") completed = true; - }); - return jsonResponse({ ok: completed && textSeen, ...(!completed || !textSeen ? { error: "inference_failed" } : {}) }); - } finally { budget.dispose(); testing = false; } + await deps.verifyDesktopProtocol(String(body.model), AbortSignal.any([req.signal, AbortSignal.timeout(35_000)])); + return jsonResponse({ ok: true }); + } finally { testing = false; } } return jsonResponse({ error: "not_found" }, 404); } catch (e) { - return jsonResponse({ error: e instanceof DesktopSetupError ? e.code : "runtime_failed" }, 400); + return jsonResponse({ error: e instanceof DesktopSetupError ? e.code + : url.pathname === "/api/zcode-desktop/test" ? "protocol_failed" : "runtime_failed" }, 400); } } diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 680f94db99..fe3dfd7eba 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -150,6 +150,8 @@ credentials/session state under its private data root. Because the official app config-path option, a one-shot Node preload redirects only its internal `os.homedir()` lookup to a turn-scoped private home; the process environment retains the user's real `HOME`, so native host tools work without copying unrelated provider secrets into the child. +The dashboard's optional recheck repeats only model-catalog and `workspace/readState` protocol +operations; it never creates or sends a model turn, starts a native tool or consumes inference quota. ## ZCode saved accounts @@ -166,6 +168,8 @@ retains its previous profile. Account providers do not participate in an implici Native tools still run on the host by default; optional OS sandboxing is independent of profile separation and does not turn the latter into a security boundary. Refresh failures cross the adapter boundary only as bounded public codes, never filesystem or account paths. +Advanced model descriptors reject loopback, private and link-local destinations before the +official runtime receives them. ## ZCode vision input adaptation diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index faffd43bbb..562bb0cba9 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -131,7 +131,7 @@ this document owns is which module holds which area and what invariant that area | Diagnostics/sync | `src/server/management/config-routes.ts` — `GET /api/diagnostics/project-config` reports project-level Codex config that bypasses managed routing; `POST /api/sync` re-runs catalog/config sync. The diagnostic reports the bypass; it does not rewrite the project file. | | Sidecar/shadow-call settings | `src/server/management/config-routes.ts` — `GET/PUT /api/sidecar-settings` and `GET/PUT /api/shadow-call-settings`. PUT accepts model and backend (web-search union: openai/anthropic/xai/gemini/exa; xAI is live through stored Grok OAuth, while Gemini/Exa remain inert until their executors ship) plus validated `webSearch.xSearch`, optional `webSearch.exaApiKey` (write/clear only — never echoed by GET or the PUT response; redact.ts strips it from logs), `webSearch.reasoning`, `vision.reasoning`, `vision.enabled`, `vision.maxDescriptionsPerTurn`, and `vision.timeoutMs`; the read and PUT-response payload reports model, backend, reasoning, enabled, the vision per-turn limit, and timeout. `timeoutMs` is validated against the runtime integer bounds in `src/vision/timeout-bounds.ts`. Provider/OAuth credentials live in their stores; `exaApiKey` is the one sidecar-owned secret and follows the write-only contract above. Both shadow-call responses also report the resolved `sourceModels` — the prefixes the runtime actually intercepts (`src/lib/shadow-call.ts`, default `gpt-5.6-luna`; the retired `gpt-5.4-mini` stays available as an explicit `sourceModels` entry for 0.144.x clients), so no client hard-codes a helper slug that a Codex release can invalidate. | | Storage | `src/server/management/logs-usage-routes.ts` — `GET /api/storage`, `POST /api/storage/cleanup/preview` and `/api/storage/cleanup`, `GET /api/storage/trash`, `POST /api/storage/trash/restore`, and `GET/PUT /api/storage/cleanup-policy` plus `POST /api/storage/cleanup-policy/run`. `GET /api/storage/cleanup-policy/test-stream` and `GET /api/storage/trash/restore/test-stream` exist for progress-stream testing. Cleanup takes an explicit `mode`: `quarantine` moves to trash and is restorable, `permanent` is not. The caller must name the mode — there is no default that silently deletes. | -| Provider quotas and tests | `src/server/management/provider-routes.ts` — `GET /api/provider-quotas`, `POST /api/providers/test`, `GET/PUT /api/provider-context-caps`, `GET /api/provider-presets`. A quota read may be served from cache or force-refreshed; absent quota data is reported as unknown rather than as a measured zero. | +| Provider quotas and tests | `src/server/management/provider-routes.ts` — `GET /api/provider-quotas`, `POST /api/providers/test`, `GET/PUT /api/provider-context-caps`, `GET /api/provider-presets`. A quota read may be served from cache or force-refreshed; absent quota data is reported as unknown rather than as a measured zero. A ZCode test with an empty local model catalog reports explicit failure and no success message. | | Models and visibility | `src/server/management/model-routes.ts` — `GET /api/models`, `PUT /api/disabled-models`, `PUT /api/model-visibility`, `PUT /api/selected-models`, `GET/POST /api/custom-models`. Visibility writes trigger catalog sync through the owning server path. | | Effort and fallback | `src/server/management/agent-settings-routes.ts` — `GET/PUT /api/effort-caps`, `/api/subagent-models`, `/api/subagent-model-fallback`. Caps clamp; they do not reject. | | Grok and Claude integrations | `src/server/management/agent-settings-routes.ts` — `GET /api/grok`, `PUT /api/grok/selection`, `POST /api/grok/apply`, `GET/PUT /api/claude-desktop`, `POST /api/claude-desktop/apply`, `GET /api/claude-desktop/status`, `GET/PUT /api/claude-code`. Apply writes an external app's profile, so its status probe must read the same resolved path it writes (see [`responses.md`](transports/responses.md)). | @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and only transient OAuth jobs expire on restart. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and only transient OAuth jobs expire on restart. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings; its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 9cfff48526..5f68b4197b 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -20,6 +20,7 @@ function fixture(): ZcodeSettings { models: { model: { limit: { context: 1000 } } } }, opencodex: { models: { recursive: {} } }, alias: { options: { baseURL: "https://127.0.0.1/v1" }, models: { recursive: {} } }, + linkLocal: { options: { baseURL: "https://169.254.1.2/v1" }, models: { metadata: {} } }, disabled: { enabled: false, models: { hidden: {} } }, } })); return { command: ["/isolated-launcher"], home, workspace: "/workspace", settingsPath, scope: home }; @@ -164,7 +165,7 @@ describe("ZCode local agent", () => { expect(loadZcodeSettings(env)).toMatchObject({ home: settings.home, workspace: settings.workspace }); expect(() => loadZcodeSettings({ ...env, HOME: settings.home })).toThrow("separate home"); }); - test("catalog excludes disabled/recursive entries and preserves canonical model identity", () => { + test("catalog excludes disabled, recursive and link-local entries and preserves canonical model identity", () => { const models = readZcodeModels(fixture()); expect(models.map(m => m.id)).toEqual(["test/model"]); expect(models[0]?.contextWindow).toBe(1000); diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index d12a39dae7..aef4fc2813 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -10,6 +10,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { getDefaultConfig } from "../../src/config"; import { activateDesktopProvider, desktopActivation } from "../../src/server/management/zcode-desktop-activation"; +import { handleProviderRoutes } from "../../src/server/management/provider-routes"; let home: string; let previousHome: string | undefined; beforeEach(() => { @@ -42,8 +43,11 @@ function context(path: string, body: unknown, principal?: ManagementContext["pri } function fixture() { let calls = 0; - const deps = { desktopStatus: () => status, connectDesktop: async () => { calls++; return { ...status, connected: true }; }, disconnectDesktop: async () => { calls++; }, readDesktopCatalogSlugs: () => { try { return JSON.parse(readFileSync(join(home, "catalog.json"), "utf8")); } catch { return []; } } }; - return { deps, calls: () => calls }; + const probed: string[] = []; + const deps = { desktopStatus: () => status, connectDesktop: async () => { calls++; return { ...status, connected: true }; }, + disconnectDesktop: async () => { calls++; }, verifyDesktopProtocol: async (model: string) => { probed.push(model); }, + readDesktopCatalogSlugs: () => { try { return JSON.parse(readFileSync(join(home, "catalog.json"), "utf8")); } catch { return []; } } }; + return { deps, calls: () => calls, probed: () => probed }; } describe("ZCode Desktop management consent", () => { for (const principal of [undefined, "admin-token"] as const) { @@ -90,6 +94,51 @@ describe("ZCode Desktop management consent", () => { const f = fixture(); const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { consent: true, runtime: "x".repeat(14000), workspace: "/project" }, "gui-session"), f.deps); expect(response?.status).toBe(400); expect(f.calls()).toBe(0); }); + test("optional verification uses protocol metadata without starting an inference turn", async () => { + const f = fixture(); + f.deps.desktopStatus = () => ({ ...status, connected: true }); + const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/test", { + consent: true, model: models[0]!.id, + }, "gui-session"), f.deps); + expect(await response?.json()).toEqual({ ok: true }); + expect(f.probed()).toEqual([models[0]!.id]); + expect(f.calls()).toBe(0); + }); + test("protocol verification failures stay bounded and never expose runtime details", async () => { + const f = fixture(); + f.deps.desktopStatus = () => ({ ...status, connected: true }); + f.deps.verifyDesktopProtocol = async () => { throw new Error("private runtime and profile path"); }; + const response = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/test", { + consent: true, model: models[0]!.id, + }, "gui-session"), f.deps); + expect(response?.status).toBe(400); + expect(await response?.json()).toEqual({ error: "protocol_failed" }); + }); +}); + +test("generic ZCode provider test reports an empty local catalog as a failure", async () => { + const zcodeHome = join(home, "advanced-home"); + const workspace = join(home, "workspace"); + mkdirSync(join(zcodeHome, ".zcode/cli"), { recursive: true }); mkdirSync(workspace); + writeFileSync(join(zcodeHome, ".zcode/cli/config.json"), JSON.stringify({ provider: {} })); + const keys = ["OCX_ZCODE_NATIVE_TOOLS", "OCX_ZCODE_COMMAND", "OCX_ZCODE_HOME", "OCX_ZCODE_WORKSPACE"] as const; + const previous = Object.fromEntries(keys.map(key => [key, process.env[key]])); + try { + process.env.OCX_ZCODE_NATIVE_TOOLS = "1"; + process.env.OCX_ZCODE_COMMAND = JSON.stringify(["/fixture-launcher"]); + process.env.OCX_ZCODE_HOME = zcodeHome; + process.env.OCX_ZCODE_WORKSPACE = workspace; + const ctx = context("/api/providers/test?name=zcode", {}, "gui-session"); + ctx.config.providers.zcode = { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai" }; + const result = await (await handleProviderRoutes(ctx))?.json(); + expect(result).toEqual({ ok: false, models: 0, latencyMs: 0, error: "ZCode local catalog has no available models." }); + expect(result).not.toHaveProperty("message"); + } finally { + for (const key of keys) { + const value = previous[key]; + if (value === undefined) delete process.env[key]; else process.env[key] = value; + } + } }); test("reconnection preserves custom settings and restart observes actual persisted catalog", async () => { diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 6ad414eb14..3fe0705d47 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -52,6 +52,7 @@ describe("managed ZCode Desktop", () => { ...provider(), models: { ...invalid, valid: { name: "Valid" } }, } } }); expect(desktopModelCatalog(config).map((model: { modelId: string }) => model.modelId)).toContain("valid"); + expect(config.model).toEqual({ main: "builtin:zai-coding-plan/valid", lite: "builtin:zai-coding-plan/valid" }); }); test("runtime selection requires a Desktop resources layout, not arbitrary commands", () => { expect(() => resolveDesktopRuntime("node -e malicious")).toThrow("desktop_missing"); From 44c5504c0aa5f1f31e0f2f1c84b3916c91b155ce Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 05:41:33 -0300 Subject: [PATCH 21/43] fix(zcode): terminate host process trees --- src/adapters/zcode/client.ts | 6 ++-- src/adapters/zcode/desktop-bootstrap.cjs | 42 +++++++++++++++++++----- structure/adapters/registry.md | 4 ++- structure/data-planes/inbound-compat.md | 2 ++ structure/providers/chat-compat.md | 2 ++ structure/providers/cursor.md | 2 ++ structure/runtime.md | 2 ++ structure/transports/inventory.md | 4 ++- structure/transports/responses.md | 2 +- tests/providers/zcode-desktop.test.ts | 41 +++++++++++++++++++++++ 10 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index 8a76463928..0e5bd7dd5b 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -45,12 +45,14 @@ export class ZcodeClient { private reading: Promise; private exited: Promise; private detachShutdown: () => void; + private closeGraceMs: number; onEvent: (message: JsonObject) => void = () => {}; onFailure: (error: Error) => void = () => {}; readonly accountId?: string; constructor(settings: ZcodeSettings, spawnProcess: ZcodeSpawn = spawn) { this.accountId = settings.accountId; + this.closeGraceMs = settings.hostExecution ? 1_000 : 500; const [command, ...args] = settings.command; this.child = spawnProcess(command!, [...args, "app-server"], { cwd: settings.home, shell: false, stdio: ["pipe", "pipe", "pipe"], @@ -148,9 +150,9 @@ export class ZcodeClient { this.pending.clear(); this.child.stdin.destroy(); this.child.kill("SIGTERM"); - const killTimer = setTimeout(() => this.child.kill("SIGKILL"), 500); + const killTimer = setTimeout(() => this.child.kill("SIGKILL"), this.closeGraceMs); // Never await an uncooperative descendant that inherited stdout indefinitely. - await Promise.race([this.exited, new Promise(resolve => setTimeout(resolve, 750))]); + await Promise.race([this.exited, new Promise(resolve => setTimeout(resolve, this.closeGraceMs + 250))]); clearTimeout(killTimer); this.child.stdout.destroy(); this.child.stderr.destroy(); diff --git a/src/adapters/zcode/desktop-bootstrap.cjs b/src/adapters/zcode/desktop-bootstrap.cjs index 6bc68b9147..1df9466064 100644 --- a/src/adapters/zcode/desktop-bootstrap.cjs +++ b/src/adapters/zcode/desktop-bootstrap.cjs @@ -41,11 +41,33 @@ function desktopModelCatalog(config) { module.exports = { normalizeDesktopConfig, desktopModelCatalog }; if (require.main === module) { let temporaryHome; + let child; + let host = false; + let terminating = false; + let forceTimer; + let exitTimer; const cleanup = () => { if (temporaryHome) fs.rmSync(temporaryHome, { recursive: true, force: true }); }; - process.on("exit", cleanup); + const signalChildTree = signal => { + if (!child) return; + if (host && process.platform !== "win32" && child.pid) { + try { process.kill(-child.pid, signal); return; } catch { /* fall back to the direct child */ } + } + try { child.kill(signal); } catch { /* the child already exited */ } + }; + const terminateChildTree = () => { + if (terminating) return; + terminating = true; + try { child?.stdin.destroy(); } catch { /* already closed */ } + signalChildTree("SIGTERM"); + // The official runtime and every non-detached native tool share this process group. + // Escalate before the outer client can force-kill this bootstrap, so its exit cleanup runs. + forceTimer = setTimeout(() => signalChildTree("SIGKILL"), 250); + exitTimer = setTimeout(() => process.exit(1), 450); + }; + process.on("exit", () => { signalChildTree("SIGKILL"); cleanup(); }); try { const args = process.argv.slice(2); - const host = args[0] === "--host"; + host = args[0] === "--host"; if (host ? args.length !== 6 || args[5] !== "app-server" : args.length !== 1 || args[0] !== "app-server") throw new Error("unsupported command"); const path = host ? args[2] : "/desktop/config.json"; if (fs.statSync(path).size > 4 * 1024 * 1024) throw new Error("oversized"); @@ -72,8 +94,9 @@ if (require.main === module) { const childArgs = host ? ["--require", require.resolve("./desktop-host-preload.cjs"), args[1], "app-server"] : ["/runtime/zcode.cjs", "app-server"]; - const child = spawn(host ? process.execPath : "/usr/bin/node", childArgs, { - stdio: ["pipe", "inherit", "inherit"], shell: false, env, ...(host ? { cwd: args[3] } : {}), + child = spawn(host ? process.execPath : "/usr/bin/node", childArgs, { + stdio: ["pipe", "inherit", "inherit"], shell: false, env, + ...(host ? { cwd: args[3], detached: process.platform !== "win32" } : {}), }); const lines = require("node:readline").createInterface({ input: process.stdin }); lines.on("line", line => { @@ -105,13 +128,16 @@ if (require.main === module) { frame.params = params; } child.stdin.write(JSON.stringify(frame) + "\n"); - } catch { child.kill("SIGTERM"); } + } catch { terminateChildTree(); } }); lines.on("close", () => child.stdin.end()); - child.stdin.on("error", () => child.kill("SIGTERM")); - process.on("SIGTERM", () => child.kill("SIGTERM")); + child.stdin.on("error", terminateChildTree); + process.on("SIGTERM", terminateChildTree); child.on("error", () => process.exit(1)); - child.on("exit", code => process.exit(code ?? 1)); + child.on("exit", code => { + clearTimeout(forceTimer); clearTimeout(exitTimer); + process.exit(code ?? 1); + }); } catch { // Never expose parser excerpts, provider keys or paths from Desktop's configuration. process.stderr.write("ZCode Desktop configuration is unavailable or incompatible.\n"); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index fe3dfd7eba..3e9abae88e 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -149,7 +149,9 @@ The host child inherits only a named allowlist of tool environment variables and credentials/session state under its private data root. Because the official app server has no config-path option, a one-shot Node preload redirects only its internal `os.homedir()` lookup to a turn-scoped private home; the process environment retains the user's real `HOME`, so native host -tools work without copying unrelated provider secrets into the child. +tools work without copying unrelated provider secrets into the child. On cancellation, the host +bootstrap signals the official runtime and inherited native tools as one POSIX process group, +escalates to a bounded forced stop, and removes the disposable turn home before returning. The dashboard's optional recheck repeats only model-catalog and `workspace/readState` protocol operations; it never creates or sends a model turn, starts a native tool or consumes inference quota. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index f6d341f9ae..b9aa68456c 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -231,6 +231,8 @@ Provider-scoped approval reviewer settings are projected by the [catalog owner]( ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. +Managed host cancellation uses a bounded process-group TERM-to-KILL ladder for the official runtime +and inherited native tools, then removes the disposable turn home before returning. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 3b75988cdf..5cf06b8316 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -281,6 +281,8 @@ Translated Chat request construction uses the [inline-image budget](../transport ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. +Managed host cancellation stops the official runtime and inherited native tools as one process +group and cleans the disposable turn home before the outer client can force-stop the bootstrap. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 42d9b643ce..09432b405c 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -105,6 +105,8 @@ Translated Chat request construction uses the [inline-image budget](../transport ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. +Managed host cancellation stops the official runtime and inherited native tools as one process +group and cleans the disposable turn home before the outer client can force-stop the bootstrap. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in diff --git a/structure/runtime.md b/structure/runtime.md index 324f7ce2d3..0a83fa9211 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -318,6 +318,8 @@ Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#rena ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user permissions by default, not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. +Managed host cancellation signals the official runtime and its inherited native-tool process group, +escalates to a bounded forced stop, and cleans the disposable turn home before the client returns. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index e1d4417aa7..17b53021a2 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -119,7 +119,9 @@ ZCode native tool execution in `src/adapters/zcode/desktop.ts` uses host user pe not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the optional Bubblewrap workspace boundary; harness restrictions apply where the native process runs. Managed connections pair that explicit consent with ZCode's non-interactive permission mode and a bounded -host-tool environment; advanced launchers keep their existing `edit` semantics. +host-tool environment; advanced launchers keep their existing `edit` semantics. Managed host +cancellation terminates the official runtime and inherited tools as one process group, then cleans +the disposable turn home before the outer client hard-stop deadline. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 11bedc06e3..bf4065fb1f 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 3fe0705d47..a1f3168706 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -258,6 +258,47 @@ test("host bootstrap reads and writes outside workspace without touching the sou expect(readFileSync(config, "utf8")).toBe(original); }); +test("host cancellation terminates the official runtime tree and cleans its turn home", async () => { + if (process.platform !== "linux") return; // Managed Desktop prerequisites currently support Linux only. + const { ZcodeClient } = await import("../../src/adapters/zcode/client"); + const { fileURLToPath } = await import("node:url"); + const home = join(root, "cancel-state"), workspace = join(root, "cancel-project"); + mkdirSync(join(home, ".zcode/cli/db"), { recursive: true }); + mkdirSync(workspace); + const config = join(root, "cancel-desktop-config.json"); + writeFileSync(config, JSON.stringify({ provider: { "builtin:zai-coding-plan": provider() } })); + const runtime = join(root, "uncooperative-runtime-fixture.cjs"); + writeFileSync(runtime, ` + const {spawn}=require("node:child_process"); + process.on("SIGTERM",()=>{}); + const tool=spawn(process.execPath,["-e","process.on('SIGTERM',()=>{});setInterval(()=>{},1000)"],{stdio:"ignore"}); + require("node:readline").createInterface({input:process.stdin}).on("line",line=>{ + const request=JSON.parse(line); + process.stdout.write(JSON.stringify({id:request.id,result:{runtimePid:process.pid,toolPid:tool.pid}})+"\\n"); + }); + setInterval(()=>{},1000); + `); + const settings = { command: [resolveDesktopNode(), fileURLToPath(new URL("../../src/adapters/zcode/desktop-bootstrap.cjs", import.meta.url)), + "--host", runtime, config, workspace, home], home, workspace, settingsPath: "", scope: "desktop:test-cancel", + hostExecution: true, nativePermissionMode: "yolo" as const }; + const alive = (pid: number) => { try { process.kill(pid, 0); return true; } catch { return false; } }; + let runtimePid = 0, toolPid = 0; + const client = new ZcodeClient(settings); + try { + const state = await client.request("workspace/readState", {}, 3000); + runtimePid = state.runtimePid as number; toolPid = state.toolPid as number; + expect(alive(runtimePid)).toBe(true); expect(alive(toolPid)).toBe(true); + await client.close(); + await Bun.sleep(100); + expect(alive(runtimePid)).toBe(false); + expect(alive(toolPid)).toBe(false); + expect(readdirSync(home).filter(name => name.startsWith("turn-"))).toEqual([]); + } finally { + await client.close(); + for (const pid of [runtimePid, toolPid]) if (pid && alive(pid)) try { process.kill(pid, "SIGKILL"); } catch { /* already exited */ } + } +}); + test("missing or denied optional sandbox never falls back, while default host mode needs no bwrap", () => { if (process.platform !== "linux") return; const path = process.env.PATH; From 56a13d0f70accb8dfcbf208ebe5901fb06c7dd1b Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 06:08:40 -0300 Subject: [PATCH 22/43] fix(zcode): refresh providers after account activation --- gui/src/components/ZcodeAccountsPane.tsx | 11 +++++++--- gui/src/components/ZcodeDesktopPane.tsx | 2 +- gui/tests/zcode-desktop-pane.test.tsx | 28 +++++++++++++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx index 93486f7ad1..e9fa635edd 100644 --- a/gui/src/components/ZcodeAccountsPane.tsx +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -3,7 +3,10 @@ import { useT } from "../i18n/shared"; type Account = { id: string; label: string; activation: string; busy: boolean; providerName?: string }; type Job = { jobId: string; accountId: string; phase: string; url?: string; error?: string }; -export default function ZcodeAccountsPane({ apiBase, runtime, workspace }: { apiBase: string; runtime: string; workspace: string }) { +type Activation = { activation?: string; providerName?: string }; +export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProviderActivated }: { + apiBase: string; runtime: string; workspace: string; onProviderActivated?: (name: string) => void; +}) { const t = useT(); const [accounts, setAccounts] = useState([]); const [label, setLabel] = useState(""); @@ -42,18 +45,19 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace }: { api const next: Job = await read("/login?jobId=" + encodeURIComponent(jobId)); if (stopped) return; if (next.phase === "authenticated") { - const result = await read("/complete", { jobId: next.jobId }); + const result = await read("/complete", { jobId: next.jobId }) as Activation; if (stopped) return; setJob({ ...next, phase: "finished", url: undefined }); if (result.activation !== "ready") setError("catalog_update_failed"); await refresh(); + if (!stopped && result.activation === "ready" && result.providerName) onProviderActivated?.(result.providerName); } else { setJob(next); if (next.phase === "failed") { setError(next.error || "native_oauth_failed"); await refresh(); } } } catch (e) { if (!stopped) { setError(e instanceof Error ? e.message : "native_oauth_failed"); setJob(j => j ? { ...j, phase: "failed", url: undefined } : null); } } finally { pending = false; } }; const timer = setInterval(() => void poll(), 2000); return () => { stopped = true; clearInterval(timer); }; - }, [jobId, jobPhase, consent, read, refresh]); + }, [jobId, jobPhase, consent, read, refresh, onProviderActivated]); const action = async (path: string, body: Record) => { if ((!consent && path !== "/cancel") || busy) return; setBusy(true); setError(""); @@ -63,6 +67,7 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace }: { api else if (path === "/cancel") setJob(null); else if (result.activation && result.activation !== "ready") setError("catalog_update_failed"); await refresh(); + if (path === "/activate" && result.activation === "ready" && result.providerName) onProviderActivated?.(result.providerName); } catch (e) { setError(e instanceof Error ? e.message : "native_oauth_failed"); } finally { setBusy(false); } }; diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index bc21205d11..5851c66d87 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -130,6 +130,6 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: } - + ; } diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 14aa10d033..ed646bd43d 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -215,7 +215,7 @@ test("saved account UI completes official login then shows provider/catalog read if (!url.pathname.startsWith("/api/zcode-accounts")) return prior(input, options); requests.push({ path: url.pathname, body: options?.body ? JSON.parse(String(options.body)) : undefined }); if (url.pathname.endsWith("/login")) return Response.json({jobId:"fixture-job",accountId:"fixture-account",phase:options?.method === "POST" ? "waiting" : "authenticated"}); - if (url.pathname.endsWith("/complete")) { complete++; return Response.json({activation:"ready"}); } + if (url.pathname.endsWith("/complete")) { complete++; return Response.json({activation:"ready",providerName:"zcode-fixture"}); } return Response.json({accounts:complete ? [{id:"fixture-account",label:"Personal fixture",activation:"ready",providerName:"zcode-fixture",busy:false}] : []}); } }); await mountPane(); @@ -235,6 +235,7 @@ test("saved account UI completes official login then shows provider/catalog read expect(section.textContent).toContain("No processes will be restarted automatically"); expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); expect(requests.filter(r => r.body).every(r => r.body!.consent === true)).toBe(true); + expect(closeCalls).toBe(1); }); test("saved account completion preserves an HTTP-200 partial activation", async () => { @@ -265,4 +266,29 @@ test("saved account completion preserves an HTTP-200 partial activation", async expect(host.textContent).toContain("Partial fixture"); expect(host.textContent).toContain("catalog_update_failed"); expect(host.textContent).not.toContain("native_oauth_failed"); + expect(closeCalls).toBe(0); +}); + +test("saved account activation refreshes the parent only after provider and catalog readiness", async () => { + let activation = "catalog_pending"; + let attempts = 0; + const prior = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { + const url = new URL(String(input), "http://localhost"); + if (!url.pathname.startsWith("/api/zcode-accounts")) return prior(input, options); + if (url.pathname.endsWith("/activate")) { + attempts++; + activation = attempts === 1 ? "catalog_pending" : "ready"; + return Response.json({ activation, providerName: "zcode-saved", ...(activation === "ready" ? {} : { error: "catalog_update_failed" }) }); + } + return Response.json({ accounts: [{ id: "saved", label: "Saved fixture", activation, providerName: "zcode-saved", busy: false }] }); + } }); + await mountPane(); + const section = host.querySelector("h3")!.closest("section")!; + await click(section.querySelector('input[type="checkbox"]')!); + await click(button("Retry activation")); + expect(closeCalls).toBe(0); + expect(host.textContent).toContain("catalog_update_failed"); + await click(button("Retry activation")); + expect(closeCalls).toBe(1); }); From 333f26247af139bce8cf468283a17046e6c3022a Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 06:22:54 -0300 Subject: [PATCH 23/43] fix(zcode): preserve account activation refresh --- gui/src/components/ZcodeAccountsPane.tsx | 18 ++++++++++++------ gui/tests/zcode-desktop-pane.test.tsx | 3 +++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx index e9fa635edd..1afe528157 100644 --- a/gui/src/components/ZcodeAccountsPane.tsx +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useT } from "../i18n/shared"; type Account = { id: string; label: string; activation: string; busy: boolean; providerName?: string }; @@ -16,6 +16,8 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi const [job, setJob] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); + const onProviderActivatedRef = useRef(onProviderActivated); + useEffect(() => { onProviderActivatedRef.current = onProviderActivated; }, [onProviderActivated]); const read = useCallback(async (path: string, body?: Record) => { const response = await fetch(apiBase + "/api/zcode-accounts" + path, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...body, consent: true }), @@ -49,15 +51,17 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi if (stopped) return; setJob({ ...next, phase: "finished", url: undefined }); if (result.activation !== "ready") setError("catalog_update_failed"); - await refresh(); - if (!stopped && result.activation === "ready" && result.providerName) onProviderActivated?.(result.providerName); + const activation = result.activation === "ready" && result.providerName && onProviderActivatedRef.current + ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; + try { await refresh(); } catch (error) { if (!activation) throw error; } + if (!stopped && activation) activation.notify(activation.name); } else { setJob(next); if (next.phase === "failed") { setError(next.error || "native_oauth_failed"); await refresh(); } } } catch (e) { if (!stopped) { setError(e instanceof Error ? e.message : "native_oauth_failed"); setJob(j => j ? { ...j, phase: "failed", url: undefined } : null); } } finally { pending = false; } }; const timer = setInterval(() => void poll(), 2000); return () => { stopped = true; clearInterval(timer); }; - }, [jobId, jobPhase, consent, read, refresh, onProviderActivated]); + }, [jobId, jobPhase, consent, read, refresh]); const action = async (path: string, body: Record) => { if ((!consent && path !== "/cancel") || busy) return; setBusy(true); setError(""); @@ -66,8 +70,10 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi if (path === "/login") setJob(result); else if (path === "/cancel") setJob(null); else if (result.activation && result.activation !== "ready") setError("catalog_update_failed"); - await refresh(); - if (path === "/activate" && result.activation === "ready" && result.providerName) onProviderActivated?.(result.providerName); + const activation = path === "/activate" && result.activation === "ready" && result.providerName && onProviderActivatedRef.current + ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; + try { await refresh(); } catch (error) { if (!activation) throw error; } + if (activation) activation.notify(activation.name); } catch (e) { setError(e instanceof Error ? e.message : "native_oauth_failed"); } finally { setBusy(false); } }; diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index ed646bd43d..5a70d22f1e 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -272,6 +272,7 @@ test("saved account completion preserves an HTTP-200 partial activation", async test("saved account activation refreshes the parent only after provider and catalog readiness", async () => { let activation = "catalog_pending"; let attempts = 0; + let failNextRefresh = false; const prior = globalThis.fetch; Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { const url = new URL(String(input), "http://localhost"); @@ -279,8 +280,10 @@ test("saved account activation refreshes the parent only after provider and cata if (url.pathname.endsWith("/activate")) { attempts++; activation = attempts === 1 ? "catalog_pending" : "ready"; + failNextRefresh = activation === "ready"; return Response.json({ activation, providerName: "zcode-saved", ...(activation === "ready" ? {} : { error: "catalog_update_failed" }) }); } + if (failNextRefresh) { failNextRefresh = false; return Response.json({ error: "refresh_failed" }, { status: 500 }); } return Response.json({ accounts: [{ id: "saved", label: "Saved fixture", activation, providerName: "zcode-saved", busy: false }] }); } }); await mountPane(); From 921bb026b0cdeff2c38225436b96e879af9a4127 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 06:29:10 -0300 Subject: [PATCH 24/43] fix(zcode): cancel shared refresh waits promptly --- src/adapters/zcode/adapter.ts | 31 ++++++++++++++++++++----- structure/adapters/registry.md | 2 +- structure/data-planes/inbound-compat.md | 2 +- structure/providers/chat-compat.md | 2 +- structure/providers/cursor.md | 2 +- structure/runtime.md | 2 +- structure/transports/byte-accounting.md | 2 ++ structure/transports/inventory.md | 2 +- structure/transports/responses.md | 2 +- tests/providers/zcode-adapter.test.ts | 21 +++++++++++++++++ 10 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 63f76e5f81..406b693e63 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -9,6 +9,7 @@ import { zcodeThoughtLevel } from "./reasoning"; type Client = Pick; const MAX_ZCODE_INPUT_CHARS = 200_000; const HISTORY_TRUNCATED = "[Earlier OpenCodex conversation history truncated to fit the ZCode bridge.]"; +const CANCELLED_BEFORE_DISPATCH = "ZCode request cancelled before dispatch."; const SAFE_ACCOUNT_REFRESH_ERRORS = new Set(["account_login_required", "account_identity_mismatch", "native_oauth_failed"]); export interface ZcodeAdapterDeps { settings?: () => ZcodeSettings; @@ -22,8 +23,21 @@ export interface ZcodeAdapterDeps { const locks = new Map>(); const sessions = new Map(); let reservations = 0; +/** Stop only this caller's wait; the official refresh may be shared by another request. */ +function waitForSharedRefresh(work: Promise, signal?: AbortSignal): Promise { + if (!signal) return work; + if (signal.aborted) { void work.catch(() => {}); return Promise.reject(new Error(CANCELLED_BEFORE_DISPATCH)); } + return new Promise((resolve, reject) => { + const onAbort = () => reject(new Error(CANCELLED_BEFORE_DISPATCH)); + signal.addEventListener("abort", onAbort, { once: true }); + work.then( + () => { signal.removeEventListener("abort", onAbort); resolve(); }, + error => { signal.removeEventListener("abort", onAbort); reject(error); }, + ); + }); +} async function lock(key: string, signal?: AbortSignal): Promise<() => void> { - if (signal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + if (signal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); if (reservations >= 32) throw new Error("ZCode profile queue is full."); reservations++; const previous = locks.get(key) ?? Promise.resolve(); @@ -40,10 +54,10 @@ async function lock(key: string, signal?: AbortSignal): Promise<() => void> { let onAbort = () => {}; try { await Promise.race([previous, new Promise((_, reject) => { - onAbort = () => reject(new Error("ZCode request cancelled before dispatch.")); + onAbort = () => reject(new Error(CANCELLED_BEFORE_DISPATCH)); signal?.addEventListener("abort", onAbort, { once: true }); })]); - if (signal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + if (signal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); } catch (error) { release(); throw error; } finally { signal?.removeEventListener("abort", onAbort); } return release; @@ -153,9 +167,14 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt let stop = () => {}; const cancelled = () => stop(); try { + if (incoming.abortSignal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); if (provider.zcodeAccountId && !deps.settings) { - try { await (deps.refreshAccount ?? refreshAccount)(provider.zcodeAccountId); } + try { + const refresh = (deps.refreshAccount ?? refreshAccount)(provider.zcodeAccountId); + await waitForSharedRefresh(refresh, incoming.abortSignal); + } catch (error) { + if (incoming.abortSignal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); const code = error instanceof Error && SAFE_ACCOUNT_REFRESH_ERRORS.has(error.message) ? error.message : "account_refresh_failed"; throw new Error(code); @@ -201,7 +220,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt void controller.promise.catch(() => {}); stop = () => { controller.reject(new Error("ZCode turn cancelled or timed out.")); void active.close(); }; incoming.abortSignal?.addEventListener("abort", cancelled, { once: true }); - if (incoming.abortSignal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + if (incoming.abortSignal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); timer = setTimeout(stop, deps.timeoutMs ?? 300_000); heartbeat = setInterval(() => emit({ type: "heartbeat" }), 5_000); active.onFailure = error => controller.reject(error); @@ -247,7 +266,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt sessionId = id; } await active.request("session/subscribe", { sessionId, deliveryKind: "desktop-continuous" }); - if (incoming.abortSignal?.aborted) throw new Error("ZCode request cancelled before dispatch."); + if (incoming.abortSignal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); // Commit visible output before dispatch, so streaming combo routing cannot replay an // accepted task that already changed files. Post-send failure is non-retryable incomplete. emit({ type: "text_delta", text: settings.hostExecution diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index fb5e613a1f..1922c55350 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -183,7 +183,7 @@ closed, never to the legacy Desktop profile or another account. Legacy unbound ` retains its previous profile. Account providers do not participate in an implicit pool. Native tools still run on the host by default; optional OS sandboxing is independent of profile separation and does not turn the latter into a security boundary. Refresh failures -cross the adapter boundary only as bounded public codes, never filesystem or account paths. +cross the adapter boundary only as bounded public codes, never filesystem or account paths. A caller waiting on the shared official refresh races only its own abort signal and returns promptly without cancelling that refresh for sibling requests. Advanced model descriptors reject loopback, private and link-local destinations before the official runtime receives them. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index a97a015bbb..af8def6abf 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -256,7 +256,7 @@ and inherited native tools, then removes the disposable turn home before returni ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index a743a3d902..0b968e9045 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -301,7 +301,7 @@ group and cleans the disposable turn home before the outer client can force-stop ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 3765c4f7fe..4982524201 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -115,7 +115,7 @@ group and cleans the disposable turn home before the outer client can force-stop ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/runtime.md b/structure/runtime.md index a057a609fb..34a52d2730 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -348,7 +348,7 @@ escalates to a bounded forced stop, and cleans the disposable turn home before t ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 830d1af7a9..dc93933231 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -22,6 +22,8 @@ partial-event, injection/drop, and EOF behavior. Output admission precedes its s failed enqueue and cancellation release the reservation without re-entering a disposed rewrite. Old/new buffer overlap remains charged against the same translator cap. +A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests. + `src/adapters/openai-responses.ts` counts new compaction fragments, including surrogate pairs formed across deltas, while retaining snapshot/done/delta precedence and existing terminal ownership. Serialized request and buffered-response observations use byte counts without measurement arrays. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 37ccebd957..0397f20982 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -148,7 +148,7 @@ the disposable turn home before the outer client hard-stop deadline. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 768e31afd1..909cbfc12f 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 5f68b4197b..96ac95d95d 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -158,6 +158,27 @@ describe("ZCode local agent", () => { expect(events.at(-1)).toMatchObject({ type: "error", message: code, retryable: false }); } }); + test("saved-account cancellation stops only the caller waiting on a shared refresh", async () => { + const shared = Promise.withResolvers(); + let refreshFinished = false; let children = 0; + const work = shared.promise.finally(() => { refreshFinished = true; }); + const adapter = createZcodeAdapter({ ...provider, zcodeAccountId: crypto.randomUUID() }, { + refreshAccount: async () => work, + client: () => { children++; return new FakeClient(); }, + }); + const events: AdapterEvent[] = []; + const controller = new AbortController(); + const pending = adapter.runTurn!(request(), { headers: new Headers(), abortSignal: controller.signal, + translatorBudget: createTestTranslatorBudget() }, event => events.push(event)); + controller.abort(); + const stopped = await Promise.race([pending.then(() => true), Bun.sleep(50).then(() => false)]); + expect(stopped).toBe(true); + expect(refreshFinished).toBe(false); + expect(children).toBe(0); + expect(events.at(-1)).toMatchObject({ type: "error", message: "ZCode request cancelled before dispatch.", retryable: false }); + shared.resolve(); await work; + expect(refreshFinished).toBe(true); + }); test("advanced settings allow an isolated home when the proxy HOME is absent", () => { const settings = fixture(); const env = { OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: JSON.stringify(["/isolated-launcher"]), From 49a9f738aa6b56ce4d3c31ddaf8bd40ee7f410f8 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 06:37:16 -0300 Subject: [PATCH 25/43] fix(zcode): prune orphaned reconnect drafts --- src/adapters/zcode/accounts.ts | 18 +++++++++++---- src/server/management/zcode-account-routes.ts | 5 +++- structure/adapters/registry.md | 2 +- structure/data-planes/inbound-compat.md | 2 +- structure/gui-and-management-api.md | 2 +- structure/providers/chat-compat.md | 2 +- structure/providers/cursor.md | 2 +- structure/runtime.md | 2 +- structure/transports/byte-accounting.md | 2 +- structure/transports/inventory.md | 2 +- structure/transports/responses.md | 2 +- tests/providers/zcode-desktop-routes.test.ts | 23 +++++++++++++++++++ 12 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/adapters/zcode/accounts.ts b/src/adapters/zcode/accounts.ts index aa91b68189..8cb7a7a309 100644 --- a/src/adapters/zcode/accounts.ts +++ b/src/adapters/zcode/accounts.ts @@ -4,8 +4,9 @@ import { randomUUID } from "node:crypto"; import { getConfigDir } from "../../config/paths"; export interface ZcodeAccount { id: string; label: string; subjectHash?: string; draftFor?: string } +const ACCOUNT_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/; export function accountRoot(id: string): string { - if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id)) throw new Error("account_invalid"); + if (!ACCOUNT_ID.test(id)) throw new Error("account_invalid"); return join(getConfigDir(), "zcode-accounts", id); } export const accountProfile = (id: string) => join(accountRoot(id), "profile"); @@ -17,7 +18,7 @@ export function readAccount(id: string): ZcodeAccount { const value = JSON.parse(readFileSync(fd, "utf8")); if (value.id !== id || typeof value.label !== "string" || value.label.length > 80 || (value.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(value.subjectHash)) - || (value.draftFor !== undefined && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value.draftFor))) throw new Error(); + || (value.draftFor !== undefined && !ACCOUNT_ID.test(value.draftFor))) throw new Error(); return { id, label: value.label, ...(value.subjectHash ? { subjectHash: value.subjectHash } : {}), ...(value.draftFor ? { draftFor: value.draftFor } : {}) }; } finally { closeSync(fd); } @@ -27,7 +28,7 @@ export function writeAccount(account: ZcodeAccount): void { const label = account.label.trim(); if (!label || label.length > 80 || /[\x00-\x1f]/.test(label) || (account.subjectHash !== undefined && !/^[a-f0-9]{64}$/.test(account.subjectHash)) - || (account.draftFor !== undefined && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(account.draftFor))) throw new Error("account_invalid"); + || (account.draftFor !== undefined && !ACCOUNT_ID.test(account.draftFor))) throw new Error("account_invalid"); mkdirSync(dir, { recursive: true, mode: 0o700 }); const temp = join(dir, randomUUID() + ".tmp"); writeFileSync(temp, JSON.stringify({ ...account, label }), { mode: 0o600, flag: "wx" }); @@ -36,10 +37,19 @@ export function writeAccount(account: ZcodeAccount): void { export function listAccounts(): ZcodeAccount[] { const dir = join(getConfigDir(), "zcode-accounts"); if (!existsSync(dir)) return []; - return readdirSync(dir).filter(id => /^[a-f0-9-]{36}$/.test(id)).slice(0, 100).flatMap(id => { + return readdirSync(dir).filter(id => ACCOUNT_ID.test(id)).slice(0, 100).flatMap(id => { try { const account = readAccount(id); return account.draftFor ? [] : [account]; } catch { return []; } }); } +/** Remove reconnect-only profiles whose in-memory OAuth jobs vanished after a process restart. */ +export function reconcileAccountDrafts(activeDraftIds: ReadonlySet): void { + const dir = join(getConfigDir(), "zcode-accounts"); + if (!existsSync(dir)) return; + for (const id of readdirSync(dir).filter(id => ACCOUNT_ID.test(id))) { + if (activeDraftIds.has(id)) continue; + try { if (readAccount(id).draftFor) removeAccountFiles(id); } catch { /* Invalid state is never deleted implicitly. */ } + } +} export function allocateAccount(label: string, replaceId?: string): ZcodeAccount { if (replaceId) readAccount(replaceId); if (!replaceId && listAccounts().length >= 20) throw new Error("account_limit"); diff --git a/src/server/management/zcode-account-routes.ts b/src/server/management/zcode-account-routes.ts index 5e2bbe7ef8..da38a00bf0 100644 --- a/src/server/management/zcode-account-routes.ts +++ b/src/server/management/zcode-account-routes.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { existsSync, renameSync, rmSync } from "node:fs"; import { join, resolve } from "node:path"; -import { accountProfile, accountRoot, allocateAccount, listAccounts, readAccount, removeAccountFiles, writeAccount } from "../../adapters/zcode/accounts"; +import { accountProfile, accountRoot, allocateAccount, listAccounts, readAccount, reconcileAccountDrafts, removeAccountFiles, writeAccount } from "../../adapters/zcode/accounts"; import { accountRuntimeBusy, invalidateAccountRefresh } from "../../adapters/zcode/account-runtime"; import { connectDesktop, defaultDesktopWorkspace, desktopStatus, disconnectDesktop, resolveDesktopRuntime, validateDesktopWorkspace } from "../../adapters/zcode/desktop"; import { runNativeOAuth } from "../../adapters/zcode/native-oauth"; @@ -42,6 +42,9 @@ export async function handleZcodeAccountRoutes(ctx: ManagementContext, deps = se // URLs and account operations belong to a real GUI principal, not data/admin-token callers. if (ctx.principal !== "gui-session") return jsonResponse({ error: "dashboard_required" }, 403); try { + // Reconnect drafts are intentionally hidden and owned by in-memory jobs. A restart drops those + // jobs, so reconcile before every authenticated account operation without touching active jobs. + reconcileAccountDrafts(new Set([...jobs.values()].map(job => job.accountId))); if (path === "/api/zcode-accounts" && ctx.req.method === "GET") { return jsonResponse({ accounts: listAccounts().map(({ id, label }) => { const status = desktopActivation(ctx, desktopStatus(id), readDesktopCatalogSlugs); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 1922c55350..ffc65455d0 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -183,7 +183,7 @@ closed, never to the legacy Desktop profile or another account. Legacy unbound ` retains its previous profile. Account providers do not participate in an implicit pool. Native tools still run on the host by default; optional OS sandboxing is independent of profile separation and does not turn the latter into a security boundary. Refresh failures -cross the adapter boundary only as bounded public codes, never filesystem or account paths. A caller waiting on the shared official refresh races only its own abort signal and returns promptly without cancelling that refresh for sibling requests. +cross the adapter boundary only as bounded public codes, never filesystem or account paths. A caller waiting on the shared official refresh races only its own abort signal and returns promptly without cancelling that refresh for sibling requests. Authenticated account operations also delete valid hidden reconnect drafts whose in-memory job disappeared after restart, while preserving active drafts and visible accounts. Advanced model descriptors reject loopback, private and link-local destinations before the official runtime receives them. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index af8def6abf..45a9ac481a 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -256,7 +256,7 @@ and inherited native tools, then removes the disposable turn home before returni ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 3d743e86a3..a0f3c16cb4 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and only transient OAuth jobs expire on restart. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings; its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings; its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 0b968e9045..cdf3cce078 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -301,7 +301,7 @@ group and cleans the disposable turn home before the outer client can force-stop ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 4982524201..64f61c932c 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -115,7 +115,7 @@ group and cleans the disposable turn home before the outer client can force-stop ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/runtime.md b/structure/runtime.md index 34a52d2730..2cb1a4ccf1 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -348,7 +348,7 @@ escalates to a bounded forced stop, and cleans the disposable turn home before t ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. +[ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index dc93933231..2acdd2b6c3 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -22,7 +22,7 @@ partial-event, injection/drop, and EOF behavior. Output admission precedes its s failed enqueue and cancellation release the reservation without re-entering a disposed rewrite. Old/new buffer overlap remains charged against the same translator cap. -A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests. +A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only. `src/adapters/openai-responses.ts` counts new compaction fragments, including surrogate pairs formed across deltas, while retaining snapshot/done/delta precedence and existing terminal ownership. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 0397f20982..a802b2f35a 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -148,7 +148,7 @@ the disposable turn home before the outer client hard-stop deadline. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 909cbfc12f..485935ec61 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests; the next authenticated account operation removes reconnect drafts orphaned by restart. diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index aef4fc2813..421d59d322 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -330,6 +330,29 @@ test("reconnect validation failure removes only its draft and preserves the save expect(readdirSync(join(home, "zcode-accounts"))).toEqual([account.accountId]); expect(readFileSync(join(accountProfile(account.accountId), "sentinel"), "utf8")).toBe("original"); }); +test("account listing after restart removes only orphaned hidden reconnect drafts", async () => { + const f = accountFixture(); + const account = await f.login(); + expect(await (await f.call("complete", { jobId: account.jobId })).json()).toMatchObject({ activation: "ready" }); + const reconnect = await f.login({ accountId: account.accountId }); + expect(reconnect).not.toHaveProperty("error"); + expect(readdirSync(join(home, "zcode-accounts"))).toHaveLength(2); + + const liveRequest = context("/api/zcode-accounts", {}, "gui-session", "GET"); + f.ctx.req = liveRequest.req; f.ctx.url = liveRequest.url; + expect(await (await handleZcodeAccountRoutes(f.ctx, f.deps))!.json()).toMatchObject({ + accounts: [{ id: account.accountId }], + }); + expect(readdirSync(join(home, "zcode-accounts"))).toHaveLength(2); + + resetZcodeAccountJobsForTests(); + const restartedRequest = context("/api/zcode-accounts", {}, "gui-session", "GET"); + f.ctx.req = restartedRequest.req; f.ctx.url = restartedRequest.url; + expect(await (await handleZcodeAccountRoutes(f.ctx, f.deps))!.json()).toMatchObject({ + accounts: [{ id: account.accountId }], + }); + expect(readdirSync(join(home, "zcode-accounts"))).toEqual([account.accountId]); +}); test("manual accounts require GUI consent; account login enables an independent provider/catalog", async () => { const f = accountFixture(), originalDefault = f.ctx.config.defaultProvider; expect((await f.call("login", {}, "admin-token")).status).toBe(403); From 582f4343bbeab17e9ee0bd506b25e0faa1ae15ac Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 06:45:43 -0300 Subject: [PATCH 26/43] fix(zcode): keep ready activations successful --- gui/src/components/ZcodeAccountsPane.tsx | 12 +++-- gui/tests/zcode-desktop-pane.test.tsx | 65 +++++++++++++++++++++++- structure/gui-and-management-api.md | 2 +- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx index 1afe528157..6068168769 100644 --- a/gui/src/components/ZcodeAccountsPane.tsx +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -50,10 +50,11 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi const result = await read("/complete", { jobId: next.jobId }) as Activation; if (stopped) return; setJob({ ...next, phase: "finished", url: undefined }); - if (result.activation !== "ready") setError("catalog_update_failed"); - const activation = result.activation === "ready" && result.providerName && onProviderActivatedRef.current + const ready = result.activation === "ready"; + if (!ready) setError("catalog_update_failed"); + const activation = ready && result.providerName && onProviderActivatedRef.current ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; - try { await refresh(); } catch (error) { if (!activation) throw error; } + try { await refresh(); } catch (error) { if (!ready) throw error; } if (!stopped && activation) activation.notify(activation.name); } else { setJob(next); if (next.phase === "failed") { setError(next.error || "native_oauth_failed"); await refresh(); } } } catch (e) { if (!stopped) { setError(e instanceof Error ? e.message : "native_oauth_failed"); setJob(j => j ? { ...j, phase: "failed", url: undefined } : null); } } @@ -70,9 +71,10 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi if (path === "/login") setJob(result); else if (path === "/cancel") setJob(null); else if (result.activation && result.activation !== "ready") setError("catalog_update_failed"); - const activation = path === "/activate" && result.activation === "ready" && result.providerName && onProviderActivatedRef.current + const ready = path === "/activate" && result.activation === "ready"; + const activation = ready && result.providerName && onProviderActivatedRef.current ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; - try { await refresh(); } catch (error) { if (!activation) throw error; } + try { await refresh(); } catch (error) { if (!ready) throw error; } if (activation) activation.notify(activation.name); } catch (e) { setError(e instanceof Error ? e.message : "native_oauth_failed"); } finally { setBusy(false); } diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 5a70d22f1e..878694e063 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -57,11 +57,12 @@ afterEach(async () => { await win.happyDOM?.close?.(); }); -async function mountPane() { +async function mountPane(withConnectionCallback = true) { const { createRoot } = await import("react-dom/client"); await act(async () => { root = createRoot(host); - root.render( { closeCalls++; }} />); + root.render( { closeCalls++; } : undefined} />); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); }); } @@ -295,3 +296,63 @@ test("saved account activation refreshes the parent only after provider and cata await click(button("Retry activation")); expect(closeCalls).toBe(1); }); + +test("ready account activation treats its local refresh as best effort without a parent callback", async () => { + let failNextRefresh = false; + let activations = 0; + const prior = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { + const url = new URL(String(input), "http://localhost"); + if (!url.pathname.startsWith("/api/zcode-accounts")) return prior(input, options); + if (url.pathname.endsWith("/activate")) { + activations++; + failNextRefresh = true; + return Response.json({ activation: "ready", providerName: "zcode-saved" }); + } + if (failNextRefresh) return Response.json({ error: "refresh_failed" }, { status: 500 }); + return Response.json({ accounts: [{ id: "saved", label: "Saved fixture", activation: "catalog_pending", + providerName: "zcode-saved", busy: false }] }); + } }); + await mountPane(false); + const section = host.querySelector("h3")!.closest("section")!; + await click(section.querySelector('input[type="checkbox"]')!); + await click(button("Retry activation")); + expect(activations).toBe(1); + expect(host.textContent).not.toContain("native_oauth_failed"); + expect(host.textContent).not.toContain("refresh_failed"); + expect(host.querySelector('[role="alert"]')).toBeNull(); +}); + +test("ready account completion stays finished when its local refresh fails without a parent callback", async () => { + let completions = 0; + let failNextRefresh = false; + const prior = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { + const url = new URL(String(input), "http://localhost"); + if (!url.pathname.startsWith("/api/zcode-accounts")) return prior(input, options); + if (url.pathname.endsWith("/login")) return Response.json({ jobId: "fixture-job", accountId: "fixture-account", + phase: options?.method === "POST" ? "waiting" : "authenticated" }); + if (url.pathname.endsWith("/complete")) { + completions++; + failNextRefresh = true; + return Response.json({ activation: "ready", providerName: "zcode-saved" }); + } + if (failNextRefresh) return Response.json({ error: "refresh_failed" }, { status: 500 }); + return Response.json({ accounts: [] }); + } }); + await mountPane(false); + const section = host.querySelector("h3")!.closest("section")!; + const input = section.querySelector('input:not([type="checkbox"])') as HTMLInputElement; + await act(async () => { + Object.getOwnPropertyDescriptor(win.HTMLInputElement.prototype, "value")!.set!.call(input, "No callback fixture"); + input.dispatchEvent(new win.Event("input", { bubbles: true })); + }); + await click(section.querySelector('input[type="checkbox"]')!); + await click(button("Add account")); + await act(async () => { await new Promise(resolve => setTimeout(resolve, 2200)); }); + expect(completions).toBe(1); + expect(button("Add account").disabled).toBe(false); + expect(host.textContent).not.toContain("native_oauth_failed"); + expect(host.textContent).not.toContain("refresh_failed"); + expect(host.querySelector('[role="alert"]')).toBeNull(); +}); diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index a0f3c16cb4..9e82d0594d 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings; its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; the displayed managed workspace is rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. A ready activation treats the secondary local account-list refresh as best effort independently of any parent callback; partial activation remains visible and retryable, reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings; its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). From ccae94f8df21fe48df39072334f7272be57fbfdf Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 07:14:23 -0300 Subject: [PATCH 27/43] fix(zcode): finish saved-account review findings --- .../src/content/docs/guides/zcode-agent.md | 19 ++++++++++------- gui/src/components/ZcodeAccountsPane.tsx | 17 +++++++++++---- .../provider-workspace/ProviderSettings.tsx | 2 +- gui/src/provider-workspace/catalog.ts | 2 ++ gui/tests/zcode-desktop-pane.test.tsx | 21 ++++++++++++++++++- src/adapters/zcode/desktop.ts | 2 ++ src/server/management/zcode-account-routes.ts | 6 +++--- structure/adapters/registry.md | 6 +++--- structure/catalog.md | 4 ++-- structure/clients/claude-desktop.md | 4 ++-- structure/data-planes/images.md | 4 ++-- structure/data-planes/inbound-compat.md | 4 ++-- structure/design-methodology.md | 4 ++-- structure/gui-and-management-api.md | 2 +- structure/ops/service-and-sidecars.md | 4 ++-- structure/overview.md | 4 ++-- structure/providers/chat-compat.md | 4 ++-- structure/providers/cursor.md | 4 ++-- structure/providers/xai-grok.md | 4 ++-- structure/runtime.md | 4 ++-- structure/subagents.md | 4 ++-- structure/transports/byte-accounting.md | 2 +- structure/transports/inventory.md | 4 ++-- structure/transports/responses.md | 2 +- structure/transports/streaming-health.md | 4 ++-- tests/providers/zcode-desktop-routes.test.ts | 18 +++++++++++++++- tests/providers/zcode-desktop.test.ts | 6 +++++- 27 files changed, 108 insertions(+), 53 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index e473055aa9..5cc9dab582 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -70,10 +70,11 @@ computer running OpenCodex. ### Add another account (manual selection) -In **Providers → Add Provider → ZCode (local agent)**, or the existing provider's -**Settings**, scroll to **Saved ZCode accounts**. This is separate from **Connect Desktop**: -connecting Desktop continues to use its current local sign-in; saved accounts retain -independent official ZCode profiles. +In **Providers → Add Provider → ZCode (local agent)**, or an unbound ZCode Desktop +provider's **Settings**, scroll to **Saved ZCode accounts**. This is separate from +**Connect Desktop**: connecting Desktop continues to use its current local sign-in; +saved accounts retain independent official ZCode profiles. Settings for an account-bound +provider intentionally hide the unrelated global Desktop Connect/Disconnect controls. 1. Select the installed Desktop runtime and working directory above the accounts section. 2. Give the account a recognizable name, such as **Personal** or **Work**. @@ -103,10 +104,12 @@ ZCode Desktop's original profile. Remove references from defaults/combos before account, and wait for its active tasks. No task is stopped automatically. A protocol/registration/catalog failure is displayed as pending, not complete success. -Use **Retry provider activation** for a saved connection whose catalog is pending. For a -failed or expired login, cancel the draft and start again. Saved accounts and provider -bindings persist across OpenCodex restarts; pending OAuth jobs do not. If a restart or -closed panel leaves an unfinished account, remove that draft before trying again. +Use **Retry provider activation** for a saved connection whose catalog is pending. A +transient completion error keeps the authenticated job and the open panel retries it; +for a failed or expired login, cancel the draft and start again. Saved accounts and +provider bindings persist across OpenCodex restarts; pending OAuth jobs do not. If a +restart or closed panel leaves an unfinished account, the next authenticated account +operation removes only that orphaned draft. Native OAuth and Coding Plan credential setup run in the official installed ZCode host. The bridge only carries a short-lived authorization URL and safe status codes to the diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx index 6068168769..f1a0142a80 100644 --- a/gui/src/components/ZcodeAccountsPane.tsx +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -47,14 +47,23 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi const next: Job = await read("/login?jobId=" + encodeURIComponent(jobId)); if (stopped) return; if (next.phase === "authenticated") { - const result = await read("/complete", { jobId: next.jobId }) as Activation; + let result: Activation; + try { + result = await read("/complete", { jobId: next.jobId }) as Activation; + } catch (e) { + if (!stopped) { + setError(e instanceof Error ? e.message : "native_oauth_failed"); + setJob({ ...next, phase: "authenticated", url: undefined }); + } + return; + } if (stopped) return; setJob({ ...next, phase: "finished", url: undefined }); const ready = result.activation === "ready"; - if (!ready) setError("catalog_update_failed"); + setError(ready ? "" : "catalog_update_failed"); const activation = ready && result.providerName && onProviderActivatedRef.current ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; - try { await refresh(); } catch (error) { if (!ready) throw error; } + try { await refresh(); } catch { /* Server completion is authoritative; this refresh is local only. */ } if (!stopped && activation) activation.notify(activation.name); } else { setJob(next); if (next.phase === "failed") { setError(next.error || "native_oauth_failed"); await refresh(); } } } catch (e) { if (!stopped) { setError(e instanceof Error ? e.message : "native_oauth_failed"); setJob(j => j ? { ...j, phase: "failed", url: undefined } : null); } } @@ -74,7 +83,7 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi const ready = path === "/activate" && result.activation === "ready"; const activation = ready && result.providerName && onProviderActivatedRef.current ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; - try { await refresh(); } catch (error) { if (!ready) throw error; } + try { await refresh(); } catch (error) { if (path !== "/activate") throw error; } if (activation) activation.notify(activation.name); } catch (e) { setError(e instanceof Error ? e.message : "native_oauth_failed"); } finally { setBusy(false); } diff --git a/gui/src/components/provider-workspace/ProviderSettings.tsx b/gui/src/components/provider-workspace/ProviderSettings.tsx index 91dea28914..ca99bb92c8 100644 --- a/gui/src/components/provider-workspace/ProviderSettings.tsx +++ b/gui/src/components/provider-workspace/ProviderSettings.tsx @@ -327,7 +327,7 @@ export default function ProviderSettings({ return (
- {item.adapter === "zcode" && apiBase !== undefined && } + {item.adapter === "zcode" && item.zcodeAccountId === undefined && apiBase !== undefined && } } - + ; } diff --git a/gui/src/pages/Providers.tsx b/gui/src/pages/Providers.tsx index c0ccbdc8dc..a1ccc54f3d 100644 --- a/gui/src/pages/Providers.tsx +++ b/gui/src/pages/Providers.tsx @@ -1,6 +1,7 @@ import { usageSummary30dResourceKey } from "../usage-summary-resource"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import ProviderWorkspaceShell, { type AddProviderIntent } from "../components/provider-workspace/ProviderWorkspaceShell"; +import { forceQuotaRefreshAfterProviderAddition } from "../provider-addition"; import ProviderDetails from "../components/provider-workspace/ProviderDetails"; import { matchingWorkspacePreset, type CatalogPreset } from "../components/provider-catalog/provider-presets"; import { isAccountProvider, type WorkspaceProvider } from "../provider-workspace/catalog"; @@ -691,7 +692,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { setAdding(false); setAddIntent(null); }} - onAdded={(name) => { + onAdded={(name, metadata) => { setAdding(false); setAddIntent(null); clearStatus(); @@ -699,7 +700,7 @@ export default function Providers({ apiBase }: { apiBase: string }) { fetchConfig(); fetchOauth(); // Desktop connection is protocol/catalog only, not consent to validate accounts. - fetchProviderQuotas(name !== "zcode" && config.providers[name]?.adapter !== "zcode"); + fetchProviderQuotas(forceQuotaRefreshAfterProviderAddition(metadata)); bumpModelsRefresh(); }} onAccountLogin={onAccountLogin} diff --git a/gui/src/pages/providers-page-modals.tsx b/gui/src/pages/providers-page-modals.tsx index ba5964675e..7439e99f0a 100644 --- a/gui/src/pages/providers-page-modals.tsx +++ b/gui/src/pages/providers-page-modals.tsx @@ -8,6 +8,7 @@ import type { AccountLoginRow, AccountLoginStatus } from "../components/provider import type { ProvidersConfig } from "./providers-shared"; import { oauthLabel } from "./providers-shared"; import type { CodexAccountMutationCompletion } from "../codex-account-mutation"; +import type { ProviderAdditionMetadata } from "../provider-addition"; export function ProvidersPageModals({ apiBase, @@ -58,7 +59,7 @@ export function ProvidersPageModals({ jsonSaving?: boolean; oauthTosPending: { provider: string; addAccount: boolean } | null; onCloseAdd: () => void; - onAdded: (name: string) => void; + onAdded: (name: string, metadata?: ProviderAdditionMetadata) => void; onAccountLogin: (provider: string, addAccount?: boolean) => void; onAccountCancelLogin: (provider: string) => void; onAccountLogout: (provider: string) => void; diff --git a/gui/src/provider-addition.ts b/gui/src/provider-addition.ts new file mode 100644 index 0000000000..ac7e831fca --- /dev/null +++ b/gui/src/provider-addition.ts @@ -0,0 +1,9 @@ +export interface ProviderAdditionMetadata { + /** Adapter returned by the completed activation, independent of the caller's stale config snapshot. */ + adapter?: string; +} + +/** Native ZCode activation is protocol/catalog-only and must not force an entitlement probe. */ +export function forceQuotaRefreshAfterProviderAddition(metadata?: ProviderAdditionMetadata): boolean { + return metadata?.adapter !== "zcode"; +} diff --git a/gui/tests/provider-addition-policy.test.ts b/gui/tests/provider-addition-policy.test.ts new file mode 100644 index 0000000000..5a463d4b94 --- /dev/null +++ b/gui/tests/provider-addition-policy.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from "bun:test"; +import { forceQuotaRefreshAfterProviderAddition } from "../src/provider-addition"; + +test("newly activated ZCode providers never force an entitlement refresh from stale config", () => { + expect(forceQuotaRefreshAfterProviderAddition({ adapter: "zcode" })).toBe(false); + expect(forceQuotaRefreshAfterProviderAddition()).toBe(true); + expect(forceQuotaRefreshAfterProviderAddition({ adapter: "openai-chat" })).toBe(true); +}); diff --git a/gui/tests/zcode-desktop-pane.test.tsx b/gui/tests/zcode-desktop-pane.test.tsx index 43fe3be0a5..1f2553f7c4 100644 --- a/gui/tests/zcode-desktop-pane.test.tsx +++ b/gui/tests/zcode-desktop-pane.test.tsx @@ -16,6 +16,7 @@ let host: HTMLElement; let root: Root | null = null; let originalFetch: typeof globalThis.fetch; let closeCalls: number; +let additions: Array<{ name: string; adapter?: string }>; let requests: Array<{ path: string; body?: Record }>; beforeEach(() => { @@ -31,7 +32,7 @@ beforeEach(() => { }); (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; - closeCalls = 0; requests = []; + closeCalls = 0; additions = []; requests = []; Object.defineProperty(globalThis, "fetch", { configurable: true, value: async (input: RequestInfo | URL, options?: RequestInit) => { @@ -64,7 +65,9 @@ async function mountPane(withConnectionCallback = true) { await act(async () => { root = createRoot(host); root.render( { closeCalls++; } : undefined} />); + onConnected={withConnectionCallback ? (name, metadata) => { + closeCalls++; additions.push({ name, adapter: metadata?.adapter }); + } : undefined} />); }); await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); }); } @@ -85,6 +88,7 @@ test("Desktop connect requires explicit consent and does not automatically spend expect(requests.find(r => r.path.endsWith("/connect"))?.body).toEqual({ runtime: "/installed/ZCode", workspace: "/project", consent: true }); expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); expect(closeCalls).toBe(1); + expect(additions).toEqual([{ name: "zcode", adapter: "zcode" }]); expect(host.textContent).not.toContain("Use this provider"); expect(requests.some(r => r.path === "/api/providers")).toBe(false); }); @@ -252,6 +256,7 @@ test("saved account UI completes official login then shows provider/catalog read expect(requests.some(r => r.path.endsWith("/test"))).toBe(false); expect(requests.filter(r => r.body).every(r => r.body!.consent === true)).toBe(true); expect(closeCalls).toBe(1); + expect(additions).toEqual([{ name: "zcode-fixture", adapter: "zcode" }]); }); test("saved account completion preserves an HTTP-200 partial activation", async () => { @@ -310,6 +315,7 @@ test("saved account activation refreshes the parent only after provider and cata expect(host.textContent).toContain("catalog_update_failed"); await click(button("Retry activation")); expect(closeCalls).toBe(1); + expect(additions).toEqual([{ name: "zcode-saved", adapter: "zcode" }]); }); test("ready account activation treats its local refresh as best effort without a parent callback", async () => { diff --git a/src/adapters/zcode/account-runtime.ts b/src/adapters/zcode/account-runtime.ts index 501952d09d..965ac7088c 100644 --- a/src/adapters/zcode/account-runtime.ts +++ b/src/adapters/zcode/account-runtime.ts @@ -1,5 +1,5 @@ import { readAccount, accountProfile } from "./accounts"; -import { desktopAccountBusy, desktopStatus } from "./desktop"; +import { desktopAccountBusy, desktopStatus, waitForDesktopAccountIdle } from "./desktop"; import { runNativeOAuth } from "./native-oauth"; const refreshes = new Map>(); @@ -10,12 +10,14 @@ export const invalidateAccountRefresh = (id: string) => refreshed.delete(id); export async function refreshAccount(id: string): Promise { if ((refreshed.get(id) ?? 0) > Date.now()) return; const pending = refreshes.get(id); if (pending) return pending; - if (desktopAccountBusy(id)) return; // Do not mutate a profile used by a running turn. - const account = readAccount(id); - if (!account.subjectHash) throw new Error("account_login_required"); - const status = desktopStatus(id); - if (!status.connected) throw new Error("account_login_required"); const promise = (async () => { + // Reserving `refreshes` before this await prevents another queued turn from starting a + // child. An already-running turn keeps the profile immutable until its child closes. + await waitForDesktopAccountIdle(id); + const account = readAccount(id); + if (!account.subjectHash) throw new Error("account_login_required"); + const status = desktopStatus(id); + if (!status.connected) throw new Error("account_login_required"); let identity: string | undefined; let nativeError: string | undefined; try { await runNativeOAuth({ runtime: status.runtime, profileHome: accountProfile(id), mode: "refresh", diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index 0e5bd7dd5b..a4debb94b6 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -30,7 +30,38 @@ export function zcodeChildEnvironment(settings: ZcodeSettings, source: NodeJS.Pr return result; } const desktopClients = new Set(); +const accountIdleWaiters = new Map void>>(); export const hasZcodeAccountClients = (id: string) => [...desktopClients].some(client => client.accountId === id); +function notifyAccountIdle(id?: string): void { + if (!id || hasZcodeAccountClients(id)) return; + const waiters = accountIdleWaiters.get(id); + accountIdleWaiters.delete(id); + for (const resolve of waiters ?? []) resolve(); +} +/** Wait for every official app-server child using this saved profile to close. */ +export function waitForZcodeAccountClients(id: string, signal?: AbortSignal): Promise { + if (!hasZcodeAccountClients(id)) return Promise.resolve(); + if (signal?.aborted) return Promise.reject(signal.reason); + return new Promise((resolve, reject) => { + const waiters = accountIdleWaiters.get(id) ?? new Set<() => void>(); + let settled = false; + const finish = () => { + if (settled) return; + settled = true; signal?.removeEventListener("abort", abort); + waiters.delete(finish); if (!waiters.size) accountIdleWaiters.delete(id); + resolve(); + }; + const abort = () => { + if (settled) return; + settled = true; waiters.delete(finish); if (!waiters.size) accountIdleWaiters.delete(id); + reject(signal?.reason); + }; + waiters.add(finish); accountIdleWaiters.set(id, waiters); + signal?.addEventListener("abort", abort, { once: true }); + // Registration and the second check are synchronous, closing the check/subscribe race. + if (!hasZcodeAccountClients(id)) finish(); + }); +} export async function closeZcodeDesktopClients(): Promise { await Promise.all([...desktopClients].filter(client => !client.accountId).map(client => client.disconnect())); } @@ -156,5 +187,6 @@ export class ZcodeClient { clearTimeout(killTimer); this.child.stdout.destroy(); this.child.stderr.destroy(); + notifyAccountIdle(this.accountId); } } diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index a574bd7dff..ec010dbd9d 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -5,7 +5,7 @@ import { homedir } from "node:os"; import { createHash, randomUUID } from "node:crypto"; import { fileURLToPath } from "node:url"; import { getConfigDir } from "../../config/paths"; -import { hasZcodeAccountClients, closeZcodeDesktopClients, ZcodeClient } from "./client"; +import { hasZcodeAccountClients, closeZcodeDesktopClients, waitForZcodeAccountClients, ZcodeClient } from "./client"; import type { ZcodeSettings } from "./settings"; import { verifyDesktopSandbox } from "./desktop-sandbox"; import { resolveDesktopNode } from "./desktop-node"; @@ -220,6 +220,45 @@ export function desktopStatus(accountId?: string) { } const connecting = new Set(); +const connectionIdleWaiters = new Map void>>(); + +function notifyConnectionIdle(key: string): void { + if (connecting.has(key)) return; + const waiters = connectionIdleWaiters.get(key); + connectionIdleWaiters.delete(key); + for (const resolve of waiters ?? []) resolve(); +} + +function waitForConnectionIdle(key: string, signal?: AbortSignal): Promise { + if (!connecting.has(key)) return Promise.resolve(); + if (signal?.aborted) return Promise.reject(signal.reason); + return new Promise((resolve, reject) => { + const waiters = connectionIdleWaiters.get(key) ?? new Set<() => void>(); + let settled = false; + const finish = () => { + if (settled) return; + settled = true; signal?.removeEventListener("abort", abort); + waiters.delete(finish); if (!waiters.size) connectionIdleWaiters.delete(key); + resolve(); + }; + const abort = () => { + if (settled) return; + settled = true; waiters.delete(finish); if (!waiters.size) connectionIdleWaiters.delete(key); + reject(signal?.reason); + }; + waiters.add(finish); connectionIdleWaiters.set(key, waiters); + signal?.addEventListener("abort", abort, { once: true }); + if (!connecting.has(key)) finish(); + }); +} + +/** Refresh jobs reserve the account first, then use this to wait out an already-running turn. */ +export async function waitForDesktopAccountIdle(id: string, signal?: AbortSignal): Promise { + while (desktopAccountBusy(id)) { + if (connecting.has(id)) await waitForConnectionIdle(id, signal); + else await waitForZcodeAccountClients(id, signal); + } +} function normalizeProtocolModels(value: unknown): DesktopModel[] { if (!Array.isArray(value) || !value.length || value.length > 1000) return fail("models_missing"); @@ -261,7 +300,10 @@ export async function connectDesktop(runtime: string, workspace: string, account persist(connection, accountId); return desktopStatus(accountId); } catch (e) { if (e instanceof DesktopSetupError) throw e; return fail("runtime_failed"); } - finally { connecting.delete(accountId ?? "desktop"); } + finally { + const key = accountId ?? "desktop"; + connecting.delete(key); notifyConnectionIdle(key); + } } /** Recheck the official protocol without creating a model turn or exposing native tools. */ @@ -280,7 +322,7 @@ export async function verifyDesktopProtocol(requiredModel: string, signal?: Abor return fail("runtime_failed"); } finally { rmSync(join(root(accountId), "home", generation), { recursive: true, force: true }); - connecting.delete(key); + connecting.delete(key); notifyConnectionIdle(key); } } diff --git a/src/server/management/zcode-desktop-routes.ts b/src/server/management/zcode-desktop-routes.ts index 6b56995387..c43e42ed39 100644 --- a/src/server/management/zcode-desktop-routes.ts +++ b/src/server/management/zcode-desktop-routes.ts @@ -39,8 +39,11 @@ export async function handleZcodeDesktopRoutes(ctx: ManagementContext, deps = se } if (url.pathname === "/api/zcode-desktop/disconnect" && req.method === "POST") { if (activating) return jsonResponse({ error: "busy" }, 409); - await deps.disconnectDesktop(); invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); - return jsonResponse(await deactivateDesktopProvider(ctx, deps.desktopStatus(), deps.readDesktopCatalogSlugs)); + activating = true; + try { + await deps.disconnectDesktop(); invalidateCodexModelsCache(); clearGatherRoutedModelsInflight(); + return jsonResponse(await deactivateDesktopProvider(ctx, deps.desktopStatus(), deps.readDesktopCatalogSlugs)); + } finally { activating = false; } } if (url.pathname === "/api/zcode-desktop/test" && req.method === "POST") { if (testing) return jsonResponse({ error: "busy" }, 409); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 0db8185de2..cca211af12 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -183,7 +183,15 @@ closed, never to the legacy Desktop profile or another account. Legacy unbound ` retains its previous profile. Account providers do not participate in an implicit pool. Native tools still run on the host by default; optional OS sandboxing is independent of profile separation and does not turn the latter into a security boundary. Refresh failures -cross the adapter boundary only as bounded public codes, never filesystem or account paths. A caller waiting on the shared official refresh races only its own abort signal and returns promptly without cancelling that refresh for sibling requests. Authenticated account operations also delete valid hidden reconnect drafts whose in-memory job disappeared after restart, while preserving active drafts and visible accounts. Global Desktop controls are hidden for account-bound provider settings, transient completion failures keep the OAuth job authenticated for retry, and canonical global-default aliases remap to the account default before validation. +cross the adapter boundary only as bounded public codes, never filesystem or account paths. An +expired refresh reserves the account before waiting for an active official turn to close, then +revalidates account/connection state and refreshes immediately before queued dispatch. A caller +waiting on that shared official refresh races only its own abort signal and returns promptly without +cancelling the refresh for sibling requests. Authenticated account operations also delete valid +hidden reconnect drafts whose in-memory job disappeared after restart, while preserving active +drafts and visible accounts. Global Desktop controls are hidden for account-bound provider settings, +transient completion failures keep the OAuth job authenticated for retry, and canonical +global-default aliases remap to the account default before validation. Advanced model descriptors reject loopback, private and link-local destinations before the official runtime receives them. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index f96942cfed..94e4861a7d 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; canonical aliases of the displayed managed workspace are rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults or sending inference. Any successful activation response treats the secondary local account-list refresh as best effort independently of a parent callback; transient completion errors keep the job authenticated for polling retry, while partial activation remains visible and retryable. Account-bound provider settings hide the unrelated global Desktop controls. Reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` makes disconnect disable the legacy provider and converge its catalog rows while preserving customized settings; its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; canonical aliases of the displayed managed workspace are rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults, sending inference or forcing an entitlement refresh from the caller's stale configuration snapshot. Any successful activation response treats the secondary local account-list refresh as best effort independently of a parent callback; transient completion errors keep the job authenticated for polling retry, while partial activation remains visible and retryable. Account-bound provider settings hide the unrelated global Desktop controls. Reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` serializes connect, activation and disconnect across their complete provider/catalog transition; disconnect disables the legacy provider and converges its catalog rows while preserving customized settings. Its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 0054e4d2f6..d5176eb75d 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -22,7 +22,7 @@ partial-event, injection/drop, and EOF behavior. Output admission precedes its s failed enqueue and cancellation release the reservation without re-entering a disposed rewrite. Old/new buffer overlap remains charged against the same translator cap. -A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. +A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` reserves the account, waits for an active turn to release its official profile, and completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. `src/adapters/openai-responses.ts` counts new compaction fragments, including surrogate pairs formed across deltas, while retaining snapshot/done/delta precedence and existing terminal ownership. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 634baaf14f..25104f69b1 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -148,7 +148,7 @@ the disposable turn home before the outer client hard-stop deadline. ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults, runs inference or forces an entitlement probe. An expired refresh reserves the account and waits for an active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests, and the next authenticated account operation removes reconnect drafts orphaned by restart. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/structure/transports/responses.md b/structure/transports/responses.md index a399974a4e..722c2358be 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. A cancelled caller stops waiting for a shared official account refresh without cancelling the refresh for other requests; the next authenticated account operation removes reconnect drafts orphaned by restart, and default-workspace aliases are canonicalized before account-scope validation. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, and default-workspace aliases are canonicalized before account-scope validation. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index da622142a3..2c13c7ae38 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -252,7 +252,7 @@ Provider-scoped approval reviewer settings are projected by the [catalog owner]( ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in -[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +[ZCode saved accounts](../adapters/registry.md#zcode-saved-accounts); adding one never changes defaults, runs inference or forces an entitlement probe. Expired pre-use refreshes reserve the account and wait for active official turns before refreshing. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](../adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index 1bcca28938..380705f759 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -186,6 +186,26 @@ test("disconnect disables the provider, preserves customization and removes its expect(ctx.config.defaultProvider).toBe(beforeDefault); }); +test("Desktop disconnect holds the activation transition lock until cleanup finishes", async () => { + const f = fixture(); + let entered!: () => void, release!: () => void; + const started = new Promise(resolve => { entered = resolve; }); + const gate = new Promise(resolve => { release = resolve; }); + f.deps.disconnectDesktop = async () => { entered(); await gate; }; + const disconnect = handleZcodeDesktopRoutes( + context("/api/zcode-desktop/disconnect", {}, "gui-session"), f.deps, + ); + await started; + const overlapping = await handleZcodeDesktopRoutes(context("/api/zcode-desktop/connect", { + consent: true, runtime: "/installed/ZCode", workspace: "/project", + }, "gui-session"), f.deps); + expect(overlapping?.status).toBe(409); + expect(await overlapping?.json()).toEqual({ error: "busy" }); + expect(f.calls()).toBe(0); + release(); + expect((await disconnect)?.status).toBe(200); +}); + test("partial disconnect cleanup is explicit and idempotently retryable", async () => { const f = fixture(), ctx = context("/api/zcode-desktop/disconnect", {}, "gui-session"); ctx.config.providers.zcode = { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", disabled: false }; diff --git a/tests/providers/zcode-transport.test.ts b/tests/providers/zcode-transport.test.ts index 0c36b18233..689f1e131f 100644 --- a/tests/providers/zcode-transport.test.ts +++ b/tests/providers/zcode-transport.test.ts @@ -2,12 +2,13 @@ import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { PassThrough, Writable } from "node:stream"; import { homedir } from "node:os"; -import { ZcodeClient, closeZcodeDesktopClients, type ZcodeSpawn } from "../../src/adapters/zcode/client"; +import { ZcodeClient, closeZcodeDesktopClients, waitForZcodeAccountClients, type ZcodeSpawn } from "../../src/adapters/zcode/client"; +import { accountRuntimeBusy, refreshAccount } from "../../src/adapters/zcode/account-runtime"; import type { JsonObject, ZcodeSettings } from "../../src/adapters/zcode/settings"; const settings: ZcodeSettings = { command: ["/isolated/launcher", "argument with spaces"], home: "/isolated/home", workspace: "/workspace", settingsPath: "/isolated/config.json", scope: "test" }; -function fixture(managed = false, hostExecution = false) { +function fixture(managed = false, hostExecution = false, accountId?: string) { const child = new EventEmitter() as EventEmitter & { stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: () => boolean }; const writes: JsonObject[] = []; child.stdin = new Writable({ write(chunk, _encoding, callback) { writes.push(JSON.parse(chunk.toString())); callback(); } }); @@ -15,7 +16,7 @@ function fixture(managed = false, hostExecution = false) { child.kill = () => { child.stdout.end(); child.stderr.end(); queueMicrotask(() => child.emit("exit", 0)); return true; }; let invocation: unknown[] = []; const spawn = ((...args: unknown[]) => { invocation = args; return child; }) as ZcodeSpawn; - const client = new ZcodeClient(managed ? { ...settings, desktopModels: [], hostExecution } : settings, spawn); + const client = new ZcodeClient(managed ? { ...settings, desktopModels: [], hostExecution, accountId } : settings, spawn); return { child, client, writes, invocation: () => invocation }; } const tick = () => new Promise(resolve => setTimeout(resolve, 5)); @@ -32,6 +33,30 @@ describe("ZCode NDJSON transport", () => { await expect(managed.client.request("session/create", {})).rejects.toThrow("disconnected"); await advanced.client.close(); }); + test("saved-account idle wait completes only after its active child closes", async () => { + const id = crypto.randomUUID(); + const active = fixture(true, false, id); + let idle = false; + const waiting = waitForZcodeAccountClients(id).then(() => { idle = true; }); + await tick(); expect(idle).toBe(false); + await active.client.close(); await waiting; + expect(idle).toBe(true); + }); + test("an expired saved-account refresh reserves the queue and waits out an active turn", async () => { + const id = crypto.randomUUID(); + const active = fixture(true, false, id); + let settled = false; + const refreshing = refreshAccount(id).finally(() => { settled = true; }); + void refreshing.catch(() => {}); + // The missing fixture account would reject immediately if busy accounts were still skipped. + await tick(); + expect(settled).toBe(false); + expect(accountRuntimeBusy(id)).toBe(true); + await active.client.close(); + await expect(refreshing).rejects.toBeInstanceOf(Error); + expect(settled).toBe(true); + expect(accountRuntimeBusy(id)).toBe(false); + }); test("uses argv without shell and does not inherit provider secrets", async () => { const f = fixture(); const invocation = f.invocation(); expect(invocation[0]).toBe("/isolated/launcher"); From d342715bd123372610caf1f2cdc9d00e0c44d5a1 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 07:44:37 -0300 Subject: [PATCH 29/43] fix(zcode): close remaining hygiene findings --- gui/src/components/ZcodeDesktopPane.tsx | 4 ++-- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + src/adapters/zcode/adapter.ts | 15 ++++++++++++-- src/codex/catalog/provider-fetch.ts | 2 +- structure/adapters/registry.md | 2 ++ structure/catalog.md | 2 ++ tests/providers/zcode-adapter.test.ts | 17 ++++++++++++++-- tests/providers/zcode-desktop.test.ts | 27 +++++++++++++++++++++++++ 16 files changed, 71 insertions(+), 7 deletions(-) diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index 462434d64b..7db40d6adc 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -80,8 +80,8 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: : error ? t("zcodeDesktop.failed") : ""; const changed = runtime !== status?.runtime || workspace !== status?.workspace; const partial = status?.connected && status.activation !== "ready"; - return
- ZCode Desktop + return
+ {t("zcodeDesktop.title")}

{t("zcodeDesktop.intro")}

{t(status?.sandbox ? "zcodeDesktop.sandboxAccess" : "zcodeDesktop.hostAccess")}

{status?.connected ? status.activation === "ready" ? t("zcodeDesktop.connected") diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 82e8a5ca31..c7e3902ca8 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -36,6 +36,7 @@ export const de: Record = { "zcodeAccounts.rename": "Umbenennen", "zcodeAccounts.remove": "Konto entfernen", "zcodeAccounts.removeConfirm": "Gespeichertes Konto und Anbieter entfernen? Aktive Aufgaben und Routing-Verweise müssen zuerst beendet werden.", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "ZCode Desktop verbinden, um den lokalen Anbieter zu aktivieren und seine Modelle in Codex bereitzustellen. Standardwerte bleiben unverändert; keine Inferenz.", "zcodeDesktop.connected": "ZCode bereit · Anbieter aktiviert · Modelle veröffentlicht", "zcodeDesktop.notConnected": "Desktop nicht verbunden", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 8d47c6e0b9..cab101b21a 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -37,6 +37,7 @@ export const en = { "zcodeAccounts.rename": "Rename", "zcodeAccounts.remove": "Remove account", "zcodeAccounts.removeConfirm": "Remove this saved account and its provider? Active tasks and routing references must be cleared first.", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "Connect ZCode Desktop to enable its local provider and publish its models to Codex. Defaults stay unchanged; no inference is sent.", "zcodeDesktop.connected": "ZCode ready · provider enabled · models published", "zcodeDesktop.notConnected": "Desktop not connected", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 5216cf7ac5..9b6183b1a7 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -35,6 +35,7 @@ export const fr: Record = { "zcodeAccounts.rename": "Renommer", "zcodeAccounts.remove": "Supprimer le compte", "zcodeAccounts.removeConfirm": "Supprimer ce compte et son fournisseur ? Terminez les tâches et retirez d’abord les références de routage.", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "Connectez ZCode Desktop pour activer son fournisseur local et publier ses modèles dans Codex. Les choix par défaut restent inchangés ; aucune inférence.", "zcodeDesktop.connected": "ZCode prêt · fournisseur activé · modèles publiés", "zcodeDesktop.notConnected": "Desktop non connecté", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 75ac5f3ac8..e5a2587dce 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -35,6 +35,7 @@ export const ja: Record = { "zcodeAccounts.rename": "名前を変更", "zcodeAccounts.remove": "アカウントを削除", "zcodeAccounts.removeConfirm": "このアカウントとプロバイダーを削除しますか?先に作業を終了し、ルーティング参照を除去してください。", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "ZCode Desktop を接続するとローカルプロバイダーが有効になり、Codex にモデルを公開します。既定値は変更せず、推論も送信しません。", "zcodeDesktop.connected": "ZCode 準備完了 · プロバイダー有効 · モデル公開済み", "zcodeDesktop.notConnected": "Desktop 未接続", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index fcdb3a8833..31fa1a2d18 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -35,6 +35,7 @@ export const ko: Record = { "zcodeAccounts.rename": "이름 변경", "zcodeAccounts.remove": "계정 제거", "zcodeAccounts.removeConfirm": "이 계정과 공급자를 제거할까요? 먼저 작업을 끝내고 라우팅 참조를 제거하세요.", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "ZCode Desktop을 연결하면 로컬 공급자를 활성화하고 Codex에 모델을 게시합니다. 기본값은 유지되며 추론은 실행하지 않습니다.", "zcodeDesktop.connected": "ZCode 준비 완료 · 공급자 활성화 · 모델 게시 완료", "zcodeDesktop.notConnected": "Desktop 연결 안 됨", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 2a34fa6315..7cee7bccc1 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -35,6 +35,7 @@ export const ru: Record = { "zcodeAccounts.rename": "Переименовать", "zcodeAccounts.remove": "Удалить аккаунт", "zcodeAccounts.removeConfirm": "Удалить этот аккаунт и провайдера? Сначала завершите задачи и удалите ссылки маршрутизации.", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "Подключение ZCode Desktop включает локального провайдера и публикует модели в Codex. Значения по умолчанию сохраняются; генерация не запускается.", "zcodeDesktop.connected": "ZCode готов · провайдер включён · модели опубликованы", "zcodeDesktop.notConnected": "Desktop не подключён", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 495495616a..ee24f5251d 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -36,6 +36,7 @@ export const tr: Record = { "zcodeAccounts.rename": "Yeniden adlandır", "zcodeAccounts.remove": "Hesabı kaldır", "zcodeAccounts.removeConfirm": "Bu hesap ve sağlayıcı kaldırılsın mı? Önce etkin işleri bitirin ve yönlendirme referanslarını kaldırın.", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "ZCode Desktop bağlantısı yerel sağlayıcıyı etkinleştirip modellerini Codex’e ekler. Varsayılanlar değişmez; çıkarım yapılmaz.", "zcodeDesktop.connected": "ZCode hazır · sağlayıcı etkin · modeller yayımlandı", "zcodeDesktop.notConnected": "Desktop bağlı değil", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index c0f8817f93..e4cfb4fa77 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -33,6 +33,7 @@ export const zhTW: Record = { "zcodeAccounts.rename": "重新命名", "zcodeAccounts.remove": "移除帳戶", "zcodeAccounts.removeConfirm": "移除此帳戶及其供應商?請先結束工作並移除路由參照。", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "連接 ZCode Desktop 將啟用本機供應商並將模型發佈至 Codex。不變更預設設定,也不執行推論。", "zcodeDesktop.connected": "ZCode 已就緒 · 供應商已啟用 · 模型已發佈", "zcodeDesktop.notConnected": "Desktop 未連接", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 620e34a233..cf0a13d84d 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -35,6 +35,7 @@ export const zh: Record = { "zcodeAccounts.rename": "重命名", "zcodeAccounts.remove": "移除账户", "zcodeAccounts.removeConfirm": "移除此账户及其提供商?请先结束活动任务并移除路由引用。", + "zcodeDesktop.title": "ZCode Desktop", "zcodeDesktop.intro": "连接 ZCode Desktop 将启用本地提供商并将模型发布到 Codex。不更改默认设置,也不执行推理。", "zcodeDesktop.connected": "ZCode 已就绪 · 提供商已启用 · 模型已发布", "zcodeDesktop.notConnected": "Desktop 未连接", diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 406b693e63..3079fd94c8 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -22,7 +22,10 @@ export interface ZcodeAdapterDeps { // session. Each turn owns a child; completing/cancelling it cannot kill another conversation. const locks = new Map>(); const sessions = new Map(); +const reservationsByScope = new Map(); let reservations = 0; +const MAX_RESERVATIONS = 32; +const MAX_RESERVATIONS_PER_SCOPE = 24; /** Stop only this caller's wait; the official refresh may be shared by another request. */ function waitForSharedRefresh(work: Promise, signal?: AbortSignal): Promise { if (!signal) return work; @@ -38,8 +41,12 @@ function waitForSharedRefresh(work: Promise, signal?: AbortSignal): Promis } async function lock(key: string, signal?: AbortSignal): Promise<() => void> { if (signal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); - if (reservations >= 32) throw new Error("ZCode profile queue is full."); + const scopeReservations = reservationsByScope.get(key) ?? 0; + if (reservations >= MAX_RESERVATIONS || scopeReservations >= MAX_RESERVATIONS_PER_SCOPE) { + throw new Error("ZCode profile queue is full."); + } reservations++; + reservationsByScope.set(key, scopeReservations + 1); const previous = locks.get(key) ?? Promise.resolve(); let unlock!: () => void; const current = new Promise(resolve => { unlock = resolve; }); @@ -48,7 +55,11 @@ async function lock(key: string, signal?: AbortSignal): Promise<() => void> { let released = false; const release = () => { if (released) return; - released = true; reservations--; unlock(); + released = true; reservations--; + const remaining = (reservationsByScope.get(key) ?? 1) - 1; + if (remaining > 0) reservationsByScope.set(key, remaining); + else reservationsByScope.delete(key); + unlock(); void tail.then(() => { if (locks.get(key) === tail) locks.delete(key); }); }; let onAbort = () => {}; diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 24f9fc4257..363700fd6b 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -1677,7 +1677,7 @@ async function fetchProviderModelsWithAuth( // The official Desktop catalog is authoritative for current GLM-5.3 levels. Unknown // future models expose no generic picker unless the operator configured one explicitly. ...(reasoning ?? (hints.reasoningEfforts === undefined ? { reasoningEfforts: [] } : {})), - displayName: model.label, + displayName: hints.displayName ?? model.label, inputModalities: hints.inputModalities ?? ["text"], } as CatalogModel; }); diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index cca211af12..8014763990 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -192,6 +192,8 @@ hidden reconnect drafts whose in-memory job disappeared after restart, while pre drafts and visible accounts. Global Desktop controls are hidden for account-bound provider settings, transient completion failures keep the OAuth job authenticated for retry, and canonical global-default aliases remap to the account default before validation. +Native turn admission retains a 32-request process ceiling and a 24-request per-profile ceiling, +so one stalled profile cannot consume every reservation needed by independent saved accounts. Advanced model descriptors reject loopback, private and link-local destinations before the official runtime receives them. diff --git a/structure/catalog.md b/structure/catalog.md index 8da7c23e6e..6c5cdd5f2e 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -339,6 +339,8 @@ Exact [model input declarations](config.md#explicit-per-model-capability-declara ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +Live Desktop discovery uses the official model label only as a fallback; configured +`modelDisplayNames` remain authoritative for renamed accounts and operator customizations. For ZCode, input-image description is the explicit exception to native-agent helper exclusion; see [ZCode vision input adaptation](adapters/registry.md#zcode-vision-input-adaptation). The configured diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 96ac95d95d..84ada13f31 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -67,9 +67,9 @@ class FakeClient { const provider: OcxProviderConfig = { adapter: "zcode", baseUrl: "https://zcode.z.ai", authMode: "local" }; const request = (): OcxParsedRequest => ({ modelId: "test/model", stream: true, options: {}, context: { messages: [{ role: "user", content: "Say hello", timestamp: 0 }] } }); -async function run(settings: ZcodeSettings, client: FakeClient, parsed = request(), signal?: AbortSignal) { +async function run(settings: ZcodeSettings, client: FakeClient, parsed = request(), signal?: AbortSignal, timeoutMs = 40) { const events: AdapterEvent[] = []; - const adapter = createZcodeAdapter(provider, { settings: () => settings, client: () => client, timeoutMs: 40 }); + const adapter = createZcodeAdapter(provider, { settings: () => settings, client: () => client, timeoutMs }); await adapter.runTurn!(parsed, { headers: new Headers(), abortSignal: signal, translatorBudget: createTestTranslatorBudget() }, event => events.push(event)); return events; @@ -308,6 +308,19 @@ describe("ZCode local agent", () => { await pending; expect((await run(settings, new FakeClient())).at(-1)?.type).toBe("done"); }); + test("one saturated profile leaves reservation capacity for another profile", async () => { + const settings = fixture(); + const controllers = Array.from({ length: 24 }, () => new AbortController()); + const pending = controllers.map(controller => run( + settings, new FakeClient("hang"), request(), controller.signal, 2_000, + )); + const rejected = await run(settings, new FakeClient(), request(), undefined, 2_000); + expect(rejected.at(-1)).toMatchObject({ type: "error", message: "ZCode profile queue is full." }); + const other = await run({ ...settings, scope: settings.scope + ":other" }, new FakeClient(), request(), undefined, 2_000); + expect(other.at(-1)?.type).toBe("done"); + for (const controller of controllers) controller.abort(); + await Promise.all(pending); + }); test("pre-aborted requests never spawn or send", async () => { const client = new FakeClient(); const events = await run(fixture(), client, request(), AbortSignal.abort()); expect(events.at(-1)?.type).toBe("error"); expect(client.calls).toHaveLength(0); diff --git a/tests/providers/zcode-desktop.test.ts b/tests/providers/zcode-desktop.test.ts index 3fa6f16d5a..cd9a816827 100644 --- a/tests/providers/zcode-desktop.test.ts +++ b/tests/providers/zcode-desktop.test.ts @@ -5,6 +5,8 @@ import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; import { defaultDesktopWorkspace, desktopStatus, disconnectDesktop, isDefaultDesktopWorkspace, loadDesktopSettings, resolveDesktopRuntime, validateDesktopWorkspace } from "../../src/adapters/zcode/desktop"; import { readZcodeModels } from "../../src/adapters/zcode/settings"; +import { accountProfile, accountRoot, allocateAccount } from "../../src/adapters/zcode/accounts"; +import { fetchProviderModels } from "../../src/codex/catalog/provider-fetch"; import { repoRoot } from "../helpers/repo-root"; import { parseNativeOAuthEvent, nativeOAuthCommand } from "../../src/adapters/zcode/native-oauth"; @@ -116,6 +118,31 @@ describe("managed ZCode Desktop", () => { expect(result[0]?.id).toBe("builtin:zai/model"); expect(result[0]?.runtimeModel).toEqual({}); }); + test("managed discovery preserves a configured account model display name", async () => { + process.env.OCX_ZCODE_SANDBOX = "0"; + const account = allocateAccount("Personal"); + const profile = join(accountProfile(account.id), ".zcode/v2"); + mkdirSync(profile, { recursive: true }); + writeFileSync(join(profile, "config.json"), "{}", { mode: 0o600 }); + const resources = join(root, "catalog-app/resources"); + mkdirSync(join(resources, "glm"), { recursive: true }); + writeFileSync(join(resources, "app.asar"), "fixture"); + const runtime = join(resources, "glm/zcode.cjs"); + writeFileSync(runtime, ""); + const workspace = defaultDesktopWorkspace(account.id); + mkdirSync(workspace, { recursive: true }); + const model = { id: "builtin:zai-coding-plan/GLM-5.3", providerId: "builtin:zai-coding-plan", + modelId: "GLM-5.3", label: "Z.AI / GLM-5.3" }; + writeFileSync(join(accountRoot(account.id), "connection.json"), JSON.stringify({ + version: 1, connected: true, generation: crypto.randomUUID(), runtime, workspace, models: [model], + }), { mode: 0o600 }); + const discovered = await fetchProviderModels("zcode-personal", { + adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", zcodeAccountId: account.id, + modelDisplayNames: { [model.id]: "Personal / Custom GLM" }, + }, 0); + expect(discovered).toHaveLength(1); + expect(discovered[0]?.displayName).toBe("Personal / Custom GLM"); + }); }); test("Node selection skips an incompatible nvm prefix and fails safely", () => { From a28b8d0ec6aea07b7ba2f2a371f01ed795669e8a Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 08:01:12 -0300 Subject: [PATCH 30/43] fix(zcode): harden compaction and continuation --- .../src/content/docs/guides/zcode-agent.md | 15 ++++-- src/adapters/zcode/adapter.ts | 52 +++++++++++++++---- src/responses/provider-continuation.ts | 2 +- src/responses/reasoning-replay-cache.ts | 2 +- src/server/responses/core.ts | 11 +++- structure/adapters/registry.md | 6 +++ structure/catalog.md | 2 + structure/clients/claude-desktop.md | 2 + structure/config.md | 2 + structure/data-planes/images.md | 2 + structure/data-planes/inbound-compat.md | 2 + structure/gui-and-management-api.md | 2 +- structure/ops/service-and-sidecars.md | 2 + structure/providers/chat-compat.md | 2 + structure/providers/cursor.md | 2 + structure/providers/kiro.md | 4 ++ structure/providers/xai-grok.md | 2 + structure/runtime.md | 2 + structure/subagents.md | 2 + structure/transports/byte-accounting.md | 5 ++ structure/transports/inventory.md | 2 + structure/transports/responses.md | 2 +- structure/transports/streaming-health.md | 2 + tests/providers/zcode-adapter.test.ts | 48 +++++++++++++++-- tests/responses/responses-state.test.ts | 3 +- .../server/server-combo-failover-e2e.test.ts | 47 ++++++++++++++++- 26 files changed, 198 insertions(+), 27 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 8df0996cd1..25c2eba968 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -251,11 +251,16 @@ rewritten. **Test connection** checks the local catalog only, not account entitl requests are still denied, and user-input requests are cancelled rather than answered automatically; continue those workflows in the ZCode client itself. - Each turn owns a child process. A profile has one active turn at a time because ZCode writes - model selection to shared settings; at most 32 active/queued bridge calls are admitted. -- `previous_response_id` uses OpenCodex's owner-fenced private continuation state. Client thread - identity also supports process-local continuity. A different model/profile or an uncorrelated - request gets a separate session. If a saved session cannot resume, the bridge reports failure - rather than silently replaying work in a new session. + model selection to shared settings; at most 24 active/queued calls are admitted per profile and + 32 across the process, so one stalled account cannot consume every reservation. +- `previous_response_id` uses OpenCodex's owner-fenced private continuation state, including when + Codex sends `store: false`. Client thread identity also supports process-local continuity. A + different model/profile or an uncorrelated request gets a separate session. If a saved session + cannot resume, the bridge reports failure rather than silently replaying work in a new session. +- Codex compaction runs in a fresh official session with an empty native-tool allowlist and cannot + replace the main continuation. Any unexpected tool event fails the compaction closed. Before a + turn starts, OpenCodex also checks the fully serialized `session/send` line against the Desktop + bridge limit, so escaped control characters cannot turn an accepted request into a later crash. - The default turn deadline is five minutes. Cancellation/deadline closes the owned child. The launcher is responsible for terminating descendants. `Task`, `TaskOutput` and `TaskStop` are denied; work cannot keep an owned sandbox running after its turn ends. diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 3079fd94c8..86d9a9054c 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -8,6 +8,8 @@ import { zcodeThoughtLevel } from "./reasoning"; type Client = Pick; const MAX_ZCODE_INPUT_CHARS = 200_000; +const MAX_ZCODE_PROTOCOL_LINE_CHARS = 1024 * 1024; +const MAX_ZCODE_SESSION_ID = `sess_${"x".repeat(80)}`; const HISTORY_TRUNCATED = "[Earlier OpenCodex conversation history truncated to fit the ZCode bridge.]"; const CANCELLED_BEFORE_DISPATCH = "ZCode request cancelled before dispatch."; const SAFE_ACCOUNT_REFRESH_ERRORS = new Set(["account_login_required", "account_identity_mismatch", "native_oauth_failed"]); @@ -159,6 +161,21 @@ function textInput(parsed: OcxParsedRequest, resumed: boolean): string { return prompt; } +/** The managed bootstrap rejects oversized NDJSON before parsing; fail before any native dispatch. */ +function assertSessionSendFits(content: string, modelParams: JsonObject): void { + let length = Infinity; + try { + length = JSON.stringify({ + id: Number.MAX_SAFE_INTEGER, + method: "session/send", + params: { sessionId: MAX_ZCODE_SESSION_ID, content, ...modelParams }, + }).length; + } catch { /* normalized below without exposing model/profile material */ } + if (length > MAX_ZCODE_PROTOCOL_LINE_CHARS) { + throw new Error("ZCode input exceeds the native bridge serialization limit."); + } +} + export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapterDeps = {}): ProviderAdapter { return { name: "zcode", @@ -209,23 +226,28 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt settings.scope, provider.baseUrl, parsed._reasoningReplayScope?.current?.providerName, parsed._cursorIdentityScope, parsed.modelId, ])).digest("hex"); - sessionKey = parsed._clientThreadId && !parsed._cursorIsolateConversation + const compaction = parsed._compactionRequest === true; + sessionKey = !compaction && parsed._clientThreadId && !parsed._cursorIsolateConversation ? createHash("sha256").update(scope + parsed._clientThreadId).digest("hex") : undefined; - const previous = record(parsed._providerContinuation?.zcode); + const previous = compaction ? {} : record(parsed._providerContinuation?.zcode); sessionId = previous.scope === scope && typeof previous.sessionId === "string" && /^sess_[a-zA-Z0-9-]{1,80}$/.test(previous.sessionId) ? previous.sessionId : sessionKey ? sessions.get(sessionKey) : undefined; const content = textInput(parsed, Boolean(sessionId)); - client = (deps.client ?? (s => new ZcodeClient(s)))(settings); - const active = client; // The Desktop bootstrap resolves credentials inside the official runtime (and the optional // sandbox when enabled). The parent only sends // public model identity; it never receives Desktop API keys or synthesizes vendor auth. - const modelParams = settings.desktopModels + const modelParams: JsonObject = settings.desktopModels ? { _zcodeModel: { providerId: model.providerId, modelId: model.modelId } } : { runtimeModel: model.runtimeModel }; + assertSessionSendFits(content, modelParams); + client = (deps.client ?? (s => new ZcodeClient(s)))(settings); + const active = client; const thoughtLevel = zcodeThoughtLevel(model.modelId, parsed.options.reasoning); const thoughtParams = thoughtLevel ? { thoughtLevel } : {}; + const toolParams: JsonObject = compaction + ? { mcpServers: [], toolAllowlist: [] } + : { mcpServers: [], toolDenylist: ["Task", "TaskOutput", "TaskStop"] }; const controller = Promise.withResolvers(); // Attach a handler immediately: failures can happen during session materialization. void controller.promise.catch(() => {}); @@ -249,6 +271,11 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt emit({ type: "thinking_delta", thinking: payload.delta }); } } else if (params.type === "tool.updated") { + if (compaction) { + controller.reject(new Error("ZCode compaction attempted native tool execution.")); + void active.close(); + return; + } // Informational only: no tool_call_* event can cause a client to repeat native work. if (payload.kind === "started" || payload.kind === "result") emit({ type: "text_delta", text: payload.kind === "started" ? "\n[ZCode: native tool started]\n" : "\n[ZCode: native tool finished]\n", @@ -265,12 +292,12 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt } }; if (sessionId) { - await active.request("session/resume", { sessionId, ...modelParams, ...thoughtParams }); + await active.request("session/resume", { sessionId, ...modelParams, ...thoughtParams, ...toolParams }); } else { const result = await active.request("session/create", { workspace: { workspacePath: settings.workspace, workspaceKey: settings.workspace }, mode: settings.nativePermissionMode ?? "edit", ...modelParams, ...thoughtParams, titleGenerationEnabled: false, - mcpServers: [], toolDenylist: ["Task", "TaskOutput", "TaskStop"], + ...toolParams, }); const id = record(result.session).sessionId; if (typeof id !== "string" || !/^sess_[a-zA-Z0-9-]{1,80}$/.test(id)) throw new Error("ZCode returned an invalid session identity."); @@ -280,9 +307,11 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt if (incoming.abortSignal?.aborted) throw new Error(CANCELLED_BEFORE_DISPATCH); // Commit visible output before dispatch, so streaming combo routing cannot replay an // accepted task that already changed files. Post-send failure is non-retryable incomplete. - emit({ type: "text_delta", text: settings.hostExecution - ? "[ZCode: running native tools with host-user access]\n" - : "[ZCode: running native tools in the configured launcher]\n", phase: "commentary" }); + emit({ type: "text_delta", text: compaction + ? "[ZCode: summarizing without native tools]\n" + : settings.hostExecution + ? "[ZCode: running native tools with host-user access]\n" + : "[ZCode: running native tools in the configured launcher]\n", phase: "commentary" }); sent = true; await active.request("session/send", { sessionId, content, ...modelParams }); await controller.promise; @@ -290,7 +319,8 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt if (sessions.size >= 128) sessions.delete(sessions.keys().next().value!); sessions.set(sessionKey, sessionId); } - emit({ type: "done", endTurn: true, providerState: { zcode: { sessionId, scope } } }); + emit({ type: "done", endTurn: true, + ...(compaction ? {} : { providerState: { zcode: { sessionId, scope } } }) }); } catch (error) { if (sessionKey) sessions.delete(sessionKey); const message = error instanceof Error ? error.message : "ZCode bridge failed."; diff --git a/src/responses/provider-continuation.ts b/src/responses/provider-continuation.ts index cbb6efc7ab..fea83150b4 100644 --- a/src/responses/provider-continuation.ts +++ b/src/responses/provider-continuation.ts @@ -54,7 +54,7 @@ export function isValidProviderContinuationOwner( && bounded(owner.adapterName, 128) && bounded(owner.modelId, 512) && typeof owner.credentialIdentity === "string" - && /^(key|oauth|codex):[0-9a-f]{64}$/.test(owner.credentialIdentity); + && /^(key|oauth|codex|local):[0-9a-f]{64}$/.test(owner.credentialIdentity); } /** Reuse the exact process-local route identity already established for reasoning replay. */ diff --git a/src/responses/reasoning-replay-cache.ts b/src/responses/reasoning-replay-cache.ts index f28dcc072e..c331d1d46b 100644 --- a/src/responses/reasoning-replay-cache.ts +++ b/src/responses/reasoning-replay-cache.ts @@ -347,7 +347,7 @@ export function durableReplayCredentialIdentity( /** Produce a non-reversible process-local identity for credential material. */ export function reasoningReplayCredentialIdentity( - kind: "key" | "oauth" | "codex", + kind: "key" | "oauth" | "codex" | "local", material: string | undefined, headers?: Record, ): string | undefined { diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 5050294bca..0782b4ea24 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -29,6 +29,7 @@ import { bindReasoningReplayScope, commitReasoningReplayServingIdentity, reasoningReplayCodexCredentialIdentity, + reasoningReplayCredentialIdentity, reasoningReplayDestinationIdentity, durableReplayDestinationIdentity, durableReplayCredentialIdentity, @@ -482,7 +483,7 @@ const runTurnAdapterSseResponses = new WeakSet(); * Adapters whose continuation state must survive Codex's store:false requests. */ export function adapterNeedsForcedContinuation(name: string): boolean { - return name === "kiro" || name === "cursor"; + return name === "kiro" || name === "cursor" || name === "zcode"; } export function sidecarOutcomeRecorder( @@ -631,6 +632,14 @@ function bindRouteReasoningReplayScope(args: { provider.headers, durableSalt, ); + } else if (provider.authMode === "local" && adapterName === "zcode") { + // ZCode credentials stay inside the official local runtime. The non-secret provider/account + // slot is enough to owner-fence persisted continuation metadata; the adapter's runtime scope + // independently rejects sessions after a Desktop reconnect or account/profile change. + credentialIdentity = reasoningReplayCredentialIdentity( + "local", + provider.zcodeAccountId ?? "desktop", + ); } else if (provider.authMode !== "local") { credentialIdentity = reasoningReplayKeyCredentialIdentity(provider); credentialDurableIdentity = durableReplayCredentialIdentity( diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 8014763990..3238b06931 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -194,6 +194,12 @@ transient completion failures keep the OAuth job authenticated for retry, and ca global-default aliases remap to the account default before validation. Native turn admission retains a 32-request process ceiling and a 24-request per-profile ceiling, so one stalled profile cannot consume every reservation needed by independent saved accounts. +Headerless `previous_response_id` chains persist owner-fenced ZCode state even with `store:false`; +the non-secret local account slot identifies the owner, while the connection/profile generation in +the adapter scope rejects stale sessions after reconnect. Internal Codex compaction always starts a +fresh official session with an empty tool allowlist, publishes no replacement ZCode session, and +fails closed on an unexpected tool event. `session/send` is sized after JSON serialization before +the native client starts, matching the managed bootstrap's one-MiB NDJSON limit. Advanced model descriptors reject loopback, private and link-local destinations before the official runtime receives them. diff --git a/structure/catalog.md b/structure/catalog.md index 6c5cdd5f2e..9baccca122 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -350,3 +350,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index 751eb4c03d..d0cb2b2273 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -158,3 +158,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/config.md b/structure/config.md index 9382336025..525e9a7dfd 100644 --- a/structure/config.md +++ b/structure/config.md @@ -277,3 +277,5 @@ The text-only consumer reads exact inputModalities declarations before legacy hi ZCode saved accounts use explicit provider bindings, separate from native OpenAI pools and client integration exports. Their profile, catalog and transport contract is maintained in [ZCode saved accounts](adapters/registry.md#zcode-saved-accounts); adding one never changes defaults or runs inference. +Their process-local continuation owner is derived from the explicit account slot rather than any +Desktop credential; stale runtime generations still fail closed in the adapter scope. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 4a928ddf8e..3942a1b846 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -131,3 +131,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index f49dbba37c..bd83a4ac76 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -266,3 +266,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 94e4861a7d..540630f085 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; canonical aliases of the displayed managed workspace are rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults, sending inference or forcing an entitlement refresh from the caller's stale configuration snapshot. Any successful activation response treats the secondary local account-list refresh as best effort independently of a parent callback; transient completion errors keep the job authenticated for polling retry, while partial activation remains visible and retryable. Account-bound provider settings hide the unrelated global Desktop controls. Reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` serializes connect, activation and disconnect across their complete provider/catalog transition; disconnect disables the legacy provider and converges its catalog rows while preserving customized settings. Its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. See [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; canonical aliases of the displayed managed workspace are rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults, sending inference or forcing an entitlement refresh from the caller's stale configuration snapshot. Any successful activation response treats the secondary local account-list refresh as best effort independently of a parent callback; transient completion errors keep the job authenticated for polling retry, while partial activation remains visible and retryable. Account-bound provider settings hide the unrelated global Desktop controls. Reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` serializes connect, activation and disconnect across their complete provider/catalog transition; disconnect disables the legacy provider and converges its catalog rows while preserving customized settings. Its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. Owner-fenced `store:false` continuation, fresh tool-free compaction sessions and serialized native-frame admission follow [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 65f4097913..bb9ec81a74 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -192,3 +192,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 7ec33a881a..86d2e233d0 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -311,3 +311,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index 88c56fdf02..aad48d1873 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -125,3 +125,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/kiro.md b/structure/providers/kiro.md index c6d915852d..f66f37c848 100644 --- a/structure/providers/kiro.md +++ b/structure/providers/kiro.md @@ -69,3 +69,7 @@ positive value overwrites an earlier one. Spend arrives in `meteringEvent` as **credits, not tokens**. No captured response carried `tokenUsage` on any event, which is why Kiro usage stays estimated; `meteringEvent` is currently ignored because a credit is not a token count. + +The shared `src/responses/provider-continuation.ts` owner validator also admits process-local +`local:` identities for ZCode's credential-free route binding. Kiro retains its existing OAuth/key +owner derivation; the added identity kind does not broaden Kiro continuation matching. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 814322f4db..5c0ee96db8 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -69,6 +69,8 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). ### Grok Reset Coupons (Billing API Parity) diff --git a/structure/runtime.md b/structure/runtime.md index 693cbe6e5a..ba35356bb1 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -358,3 +358,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/subagents.md b/structure/subagents.md index eda971643b..612aca8896 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -320,3 +320,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index d5176eb75d..768acdd10c 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -23,6 +23,11 @@ failed enqueue and cancellation release the reservation without re-entering a di Old/new buffer overlap remains charged against the same translator cap. A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` reserves the account, waits for an active turn to release its official profile, and completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. +The same adapter constructs a maximum-length session-id envelope and measures the actual JSON +serialization of each prospective `session/send` frame before it creates the native client. Frames +over the managed bootstrap's one-MiB NDJSON line limit fail as pre-dispatch input errors; escaped +control characters therefore cannot expand a nominally bounded prompt into an accepted turn that +later dies in the bridge. `src/adapters/openai-responses.ts` counts new compaction fragments, including surrogate pairs formed across deltas, while retaining snapshot/done/delta precedence and existing terminal ownership. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 25104f69b1..fc4718bb03 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -158,3 +158,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 722c2358be..22867f1966 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, and default-workspace aliases are canonicalized before account-scope validation. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Headerless `previous_response_id` chains force local continuation retention despite `store:false` and bind it to a process-local digest of the explicit ZCode account slot. The adapter's connection/profile scope remains the final stale-session fence. Routed compaction creates a fresh, separate official session with an empty native-tool allowlist, never replaces the main session, and aborts on an unexpected tool event. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, and default-workspace aliases are canonicalized before account-scope validation. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 2c13c7ae38..f92b743996 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -262,3 +262,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. +ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +[canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 84ada13f31..c3c4d74f27 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -30,10 +30,14 @@ class FakeClient { onFailure: (error: Error) => void = () => {}; calls: Array<{ method: string; params: JsonObject }> = []; closed = false; - constructor(private outcome: "ok" | "failed" | "hang" | "session-noise" | "answerless" = "ok") {} + private nativeTools = true; + constructor(private outcome: "ok" | "failed" | "hang" | "session-noise" | "answerless" | "tool-during-compaction" = "ok") {} async request(method: string, params: JsonObject): Promise { this.calls.push({ method, params }); - if (method === "session/create") return { session: { sessionId: "sess_test-1" } }; + if (method === "session/create") { + this.nativeTools = !(Array.isArray(params.toolAllowlist) && params.toolAllowlist.length === 0); + return { session: { sessionId: "sess_test-1" } }; + } if (method === "session/send") { if (this.outcome === "session-noise") { queueMicrotask(() => { @@ -49,8 +53,10 @@ class FakeClient { queueMicrotask(() => { if (this.outcome === "hang") return; this.event("model.streaming", { kind: "reasoning_delta", delta: "Thinking" }); - this.event("tool.updated", { kind: "started", toolCallId: "tool-1" }); - this.event("tool.updated", { kind: "result", toolCallId: "tool-1" }); + if (this.nativeTools || this.outcome === "tool-during-compaction") { + this.event("tool.updated", { kind: "started", toolCallId: "tool-1" }); + this.event("tool.updated", { kind: "result", toolCallId: "tool-1" }); + } if (this.outcome === "failed") return this.event("turn.failed", { error: { message: "secret" } }); if (this.outcome === "answerless") return this.event("turn.completed", {}); this.event("model.streaming", { kind: "text_delta", delta: "Hello" }); @@ -210,6 +216,40 @@ describe("ZCode local agent", () => { expect(client.closed).toBe(true); expect(client.calls.map(c => c.method)).toEqual(["session/create", "session/subscribe", "session/send", "session/stop"]); }); + test("compaction uses a fresh tool-disabled session and does not replace the main continuation", async () => { + const settings = fixture(); + const parsed = request(); + parsed._compactionRequest = true; + parsed._providerContinuation = { zcode: { sessionId: "sess_existing", scope: settings.scope } }; + const client = new FakeClient(); + const events = await run(settings, client, parsed); + expect(events.at(-1)?.type).toBe("done"); + const done = events.find((event): event is Extract => event.type === "done"); + expect(done?.providerState).toBeUndefined(); + expect(events.some(event => event.type === "text_delta" && event.text.includes("without native tools"))).toBe(true); + expect(client.calls.some(call => call.method === "session/resume")).toBe(false); + const created = client.calls.find(call => call.method === "session/create")?.params; + expect(created?.toolAllowlist).toEqual([]); + expect(created?.toolDenylist).toBeUndefined(); + }); + test("compaction fails closed if the official runtime emits a native tool event", async () => { + const parsed = request(); parsed._compactionRequest = true; + const events = await run(fixture(), new FakeClient("tool-during-compaction"), parsed); + expect(events.at(-1)).toMatchObject({ type: "incomplete", retryable: false }); + expect(JSON.stringify(events)).toContain("compaction attempted native tool execution"); + expect(events.some(event => event.type === "done")).toBe(false); + }); + test("rejects an oversized serialized send frame before starting the native client", async () => { + const parsed = request(); + parsed.context.messages = [{ role: "user", content: "\0".repeat(175_000), timestamp: 0 }]; + const client = new FakeClient(); + const events = await run(fixture(), client, parsed, undefined, 2_000); + expect(events.at(-1)).toMatchObject({ + type: "error", message: "ZCode input exceeds the native bridge serialization limit.", retryable: false, + }); + expect(client.calls).toHaveLength(0); + expect(client.closed).toBe(false); + }); test("answerless completion is reported as an interrupted accepted turn", async () => { const events = await run(fixture(), new FakeClient("answerless")); expect(events.at(-1)).toMatchObject({ type: "incomplete", reason: "zcode_agent_interrupted", diff --git a/tests/responses/responses-state.test.ts b/tests/responses/responses-state.test.ts index 1906d6b1e1..99dc0d09a3 100644 --- a/tests/responses/responses-state.test.ts +++ b/tests/responses/responses-state.test.ts @@ -847,9 +847,10 @@ describe("Responses previous_response_id state", () => { .toBe("cursor_conv_force_1"); }); - test("adapterNeedsForcedContinuation covers exactly kiro and cursor", () => { + test("adapterNeedsForcedContinuation covers native provider sessions", () => { expect(adapterNeedsForcedContinuation("kiro")).toBe(true); expect(adapterNeedsForcedContinuation("cursor")).toBe(true); + expect(adapterNeedsForcedContinuation("zcode")).toBe(true); expect(adapterNeedsForcedContinuation("openai")).toBe(false); expect(adapterNeedsForcedContinuation("claude")).toBe(false); expect(adapterNeedsForcedContinuation("")).toBe(false); diff --git a/tests/server/server-combo-failover-e2e.test.ts b/tests/server/server-combo-failover-e2e.test.ts index 59400ff921..cd8e755a14 100644 --- a/tests/server/server-combo-failover-e2e.test.ts +++ b/tests/server/server-combo-failover-e2e.test.ts @@ -74,9 +74,11 @@ mock.module("../../src/server/adapter-resolve", () => ({ provider.adapter === "test-run-turn" || provider.adapter === "test-kiro" || provider.adapter === "test-owned" + || provider.adapter === "test-zcode" ) { const adapter: ProviderAdapter = { - name: provider.adapter === "test-kiro" ? "kiro" : provider.adapter, + name: provider.adapter === "test-kiro" ? "kiro" + : provider.adapter === "test-zcode" ? "zcode" : provider.adapter, buildRequest: () => ({ url: provider.baseUrl, method: "POST", headers: {}, body: "" }), async *parseStream(): AsyncGenerator { yield { type: "error", message: "test runTurn adapter does not use parseStream" }; @@ -2830,6 +2832,49 @@ describe("server combo failover 030 activation matrix", () => { expect(seen).toEqual([undefined, "kiro-owned-conversation"]); }); + test("headerless local ZCode continuation stays owner-fenced and survives store false", async () => { + const { previousResponseProviderState } = await import("../../src/responses/state"); + const accountId = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"; + const seen: Array = []; + customRunTurn = async (parsed, _incoming, emit) => { + const sessionId = parsed._providerContinuation?.zcode?.sessionId as string | undefined; + seen.push(sessionId); + emit({ type: "text_delta", text: "continued" }); + emit({ type: "done", providerState: { zcode: { sessionId: sessionId ?? "sess_zcode-owned", scope: "scope-a" } } }); + }; + const config = comboConfig({ + z: provider("test-zcode", "https://zcode.z.ai", "unused", { authMode: "local", zcodeAccountId: accountId }), + }, [{ provider: "z", model: "m1" }]); + + const first = await post(config, { store: false, input: "first" }); + expect(first.status).toBe(200); + const firstJson = await first.json() as { id: string }; + const stored = previousResponseProviderState(firstJson.id); + expect(stored?.__ocxOwner?.credentialIdentity).toMatch(/^local:[0-9a-f]{64}$/); + expect(JSON.stringify(stored)).not.toContain(accountId); + + const second = await post(config, { + store: false, + previous_response_id: firstJson.id, + input: "second", + }); + expect(second.status).toBe(200); + expect(seen).toEqual([undefined, "sess_zcode-owned"]); + + const otherAccount = comboConfig({ + z: provider("test-zcode", "https://zcode.z.ai", "unused", { + authMode: "local", zcodeAccountId: "ffffffff-eeee-4ddd-8ccc-bbbbbbbbbbbb", + }), + }, [{ provider: "z", model: "m1" }]); + const mismatched = await post(otherAccount, { + store: false, + previous_response_id: firstJson.id, + input: "different account", + }); + expect(mismatched.status).toBe(200); + expect(seen).toEqual([undefined, "sess_zcode-owned", undefined]); + }); + test("same Cursor combo target without a parent-thread header retains its conversation id", async () => { const seen: string[] = []; customCursorTransportFactory = () => ({ From 712772603d0141ac0a7c2a818c01f6d3096ed823 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 08:36:44 -0300 Subject: [PATCH 31/43] fix(zcode): close lifecycle and host access gaps --- .../src/content/docs/guides/zcode-agent.md | 40 +++++++++++++------ src/adapters/zcode/adapter.ts | 20 ++++++---- src/adapters/zcode/client.ts | 37 +++++++++++------ src/adapters/zcode/settings.ts | 25 +++++++++--- src/server/management/zcode-account-routes.ts | 20 +++++++++- .../management/zcode-desktop-activation.ts | 8 ++-- structure/adapters/registry.md | 15 +++++++ structure/catalog.md | 2 +- structure/clients/claude-desktop.md | 2 +- structure/data-planes/images.md | 2 +- structure/data-planes/inbound-compat.md | 2 +- structure/gui-and-management-api.md | 2 +- structure/ops/service-and-sidecars.md | 2 +- structure/providers/chat-compat.md | 2 +- structure/providers/cursor.md | 2 +- structure/providers/xai-grok.md | 2 +- structure/runtime.md | 2 +- structure/subagents.md | 2 +- structure/transports/byte-accounting.md | 2 +- structure/transports/inventory.md | 2 +- structure/transports/responses.md | 2 +- structure/transports/streaming-health.md | 2 +- tests/providers/zcode-adapter.test.ts | 33 ++++++++++++++- tests/providers/zcode-desktop-routes.test.ts | 29 ++++++++++++++ tests/providers/zcode-transport.test.ts | 38 +++++++++++++++--- 25 files changed, 230 insertions(+), 65 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 25c2eba968..62c91b78c9 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -43,8 +43,9 @@ or subscription terms; those remain the vendor's policy. 3. Click **Detect again** if needed. OpenCodex finds running Desktop installations and standard installation folders. For portable/extracted installations, the advanced folder field lets you select the application directory; it never accepts a shell command. -4. Choose a **workspace folder** with the folder browser. The default is a private disposable - folder. Do not select your entire home or a credentials/configuration directory. +4. Choose a **workspace folder** with the folder browser. It is the initial working directory, + not a filesystem boundary in the default host mode; choose a narrower project folder when + convenient, even though native Bash can still reach other files owned by the proxy user. 5. Review the native file/command execution notice, check the consent box and click **Connect Desktop**. OpenCodex constructs the native launcher, verifies the protocol, enables the ZCode provider and updates the Codex catalog; no environment variables, API-key entry, token import or separate CLI login is required. @@ -102,8 +103,9 @@ clears it. and custom settings. A different identity is rejected; use **Add account** for it instead. Duplicate saved identities are rejected too. **Rename** updates generated picker labels, not customized model names. **Remove** deletes OpenCodex's saved profile and provider, not -ZCode Desktop's original profile. Remove references from defaults/combos before removing an -account, and wait for its active tasks. No task is stopped automatically. +ZCode Desktop's original profile. Remove references from defaults, combos and subagent routes +before removing an account; provider aliases count as references too. Wait for active tasks and +their native child processes to finish. No task is stopped automatically. A protocol/registration/catalog failure is displayed as pending, not complete success. Use **Retry provider activation** for a saved connection whose catalog is pending. A @@ -135,9 +137,13 @@ selected working directory, including sensitive files accessible to that user. A paths retain their host meaning. The selected workspace is a starting directory, not a filesystem boundary. This default also applies to previously connected installations after upgrading. Managed connections start ZCode in its non-interactive `yolo` permission mode so -native commands can actually use that host access without stopping for an approval prompt that -the OpenCodex protocol cannot display. This changes ZCode's approval policy, not operating-system -permissions: there is no root elevation or sandbox bypass. +native commands do not stop for an approval prompt that the OpenCodex protocol cannot display. +Because the official Bash tool otherwise creates its own sandbox, OpenCodex also places a +bridge-owned instruction before and after every managed host turn requiring each Bash call to set +ZCode's official `dangerouslyDisableSandbox=true` input. Native file tools remain limited to the +selected workspace, so work on other absolute paths is directed through Bash. This disables +ZCode's per-command sandbox; it does not elevate privileges or bypass the operating system or an +outer harness sandbox. Any isolation must be applied by the harness/operator **where the native ZCode process executes**. A sandbox in a remote calling client does not automatically constrain this @@ -160,7 +166,8 @@ export OCX_ZCODE_SANDBOX=1 Bubblewrap is required only when this option is enabled. The existing restricted workspace mounts and real sandbox preflight apply; failure never falls back to host execution. In this -mode the selected workspace appears as `/workspace`; other host files are not mounted. +mode the selected workspace appears as `/workspace`; other host files are not mounted, and the +managed host-execution instruction is omitted. Unset the variable to restore the default host execution. Environment changes apply to existing managed connections without requiring another connection. They do not reconfigure an advanced operator-supplied launcher. @@ -241,13 +248,18 @@ Models come from the isolated `.zcode/cli/config.json`, with selectors such as avoid recursive routing. Do not configure indirect routes or DNS aliases back to OpenCodex. The existing catalog refresh flow reloads these settings; no primary client configuration is rewritten. **Test connection** checks the local catalog only, not account entitlement or quota. +The bridge fingerprints the bounded configuration contents into the native-session scope. Changing +an account or credential in place invalidates prior continuations; a change while a request waits +in the profile queue fails before the child starts. The fingerprint is not returned by management +or discovery APIs. ## Safety and limitations - Native execution requires either a persisted, explicitly consented Desktop connection or all four advanced environment settings. Disconnecting revokes managed execution. - Managed Desktop sessions use ZCode's non-interactive `yolo` permission mode after the explicit - connection consent. Advanced operator launchers retain `edit` mode. Interactive permission + connection consent and direct native Bash to use its official unsandboxed-call input. Advanced + operator launchers retain `edit` mode and their own isolation policy. Interactive permission requests are still denied, and user-input requests are cancelled rather than answered automatically; continue those workflows in the ZCode client itself. - Each turn owns a child process. A profile has one active turn at a time because ZCode writes @@ -379,12 +391,14 @@ required. It does not change default provider/model selections or execute infere Reconnection enables the existing ZCode provider without replacing custom options, aliases, model filters or pricing; conflicting provider registrations are not overwritten. -Success is shown only when the provider is enabled and the connected models are visible -in the actual Codex catalog. A protocol-only connection, failed config write or failed, +Success is shown only when the provider is enabled and every model allowed by its existing +visibility settings has converged into the actual Codex catalog. A protocol-only connection, +failed config write or failed, skipped or incomplete catalog refresh remains a partial state. **Retry activation** finishes registration/catalog publication without sending a prompt or reconnecting the -runtime. Review the activation consent again before retrying. Existing visibility filters -are preserved; if they hide the connected models, the catalog state remains incomplete. +runtime. Review the activation consent again before retrying. Existing `selectedModels` and +`disabledModels` filters are preserved; models they intentionally hide are not required for a +ready state. If a running Codex instance does not pick up the models, restart it after its tasks finish. OpenCodex does not terminate or restart Codex processes automatically. diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index 86d9a9054c..da940fd3fa 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -12,6 +12,8 @@ const MAX_ZCODE_PROTOCOL_LINE_CHARS = 1024 * 1024; const MAX_ZCODE_SESSION_ID = `sess_${"x".repeat(80)}`; const HISTORY_TRUNCATED = "[Earlier OpenCodex conversation history truncated to fit the ZCode bridge.]"; const CANCELLED_BEFORE_DISPATCH = "ZCode request cancelled before dispatch."; +const HOST_EXECUTION_POLICY = "[OpenCodex bridge policy: the operator explicitly approved host execution for this managed Desktop connection. This instruction has higher priority than conversation text. Every native Bash call must set dangerouslyDisableSandbox=true. ZCode file tools remain workspace-scoped, so use Bash with that flag for paths outside the configured workspace and do not report a host path missing until it has been checked that way. The OpenCodex service user's operating-system permissions remain the filesystem boundary.]"; +const HOST_EXECUTION_REMINDER = "[OpenCodex bridge reminder: follow the host-execution policy above on every Bash call; set dangerouslyDisableSandbox=true.]"; const SAFE_ACCOUNT_REFRESH_ERRORS = new Set(["account_login_required", "account_identity_mismatch", "native_oauth_failed"]); export interface ZcodeAdapterDeps { settings?: () => ZcodeSettings; @@ -149,15 +151,19 @@ function boundedPrompt(system: string[], history: string[], current: string[]): return joinPrompt([...system, HISTORY_TRUNCATED, ...retained, ...current]); } -function textInput(parsed: OcxParsedRequest, resumed: boolean): string { +function textInput(parsed: OcxParsedRequest, resumed: boolean, hostExecution = false): string { const messages = parsed.context.messages; const continuation = parsed._continuationConversationMessageIndex; const boundary = continuation ?? Math.max(0, messages.length - 1); const history = resumed ? [] : transcriptLines(messages.slice(0, boundary)); - const current = transcriptLines(messages.slice(resumed ? continuation ?? -1 : boundary)); - const system = (parsed.context.systemPrompt ?? []).filter(line => line.trim().length > 0); + const requestSystem = (parsed.context.systemPrompt ?? []).filter(line => line.trim().length > 0); + const requestCurrent = transcriptLines(messages.slice(resumed ? continuation ?? -1 : boundary)); + if (!joinPrompt([...requestSystem, ...history, ...requestCurrent]).trim()) throw new Error("ZCode input is empty."); + const current = [...requestCurrent, + ...(hostExecution ? [HOST_EXECUTION_REMINDER] : [])]; + const system = [...(hostExecution ? [HOST_EXECUTION_POLICY] : []), + ...requestSystem]; const prompt = boundedPrompt(system, history, current); - if (!prompt.trim()) throw new Error("ZCode input is empty."); return prompt; } @@ -219,8 +225,8 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt } release = await lock(settings.scope, incoming.abortSignal); // A queued turn must not resurrect a revoked Desktop connection or old login. - if (settings.desktopModels && (deps.settings ?? (() => loadZcodeSettings(process.env, provider.zcodeAccountId)))().scope !== settings.scope) { - throw new Error("ZCode Desktop connection changed while this turn was queued."); + if ((deps.settings ?? (() => loadZcodeSettings(process.env, provider.zcodeAccountId)))().scope !== settings.scope) { + throw new Error("ZCode connection or profile changed while this turn was queued."); } const scope = createHash("sha256").update(JSON.stringify([ settings.scope, provider.baseUrl, parsed._reasoningReplayScope?.current?.providerName, @@ -233,7 +239,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt sessionId = previous.scope === scope && typeof previous.sessionId === "string" && /^sess_[a-zA-Z0-9-]{1,80}$/.test(previous.sessionId) ? previous.sessionId : sessionKey ? sessions.get(sessionKey) : undefined; - const content = textInput(parsed, Boolean(sessionId)); + const content = textInput(parsed, Boolean(sessionId), settings.hostExecution === true); // The Desktop bootstrap resolves credentials inside the official runtime (and the optional // sandbox when enabled). The parent only sends // public model identity; it never receives Desktop API keys or synthesizes vendor auth. diff --git a/src/adapters/zcode/client.ts b/src/adapters/zcode/client.ts index a4debb94b6..e14e9b3bad 100644 --- a/src/adapters/zcode/client.ts +++ b/src/adapters/zcode/client.ts @@ -75,6 +75,7 @@ export class ZcodeClient { private terminalError?: Error; private reading: Promise; private exited: Promise; + private closePromise?: Promise; private detachShutdown: () => void; private closeGraceMs: number; onEvent: (message: JsonObject) => void = () => {}; @@ -91,7 +92,9 @@ export class ZcodeClient { }) as ChildProcessWithoutNullStreams; this.exited = new Promise(resolve => { this.child.once("exit", () => resolve()); - this.child.once("error", () => resolve()); + // A spawn failure has no process to fence. Later child errors (for example a failed + // signal) must not masquerade as exit while the official runtime can still be alive. + this.child.once("error", () => { if (this.child.pid === undefined) resolve(); }); }); // Discard vendor diagnostics: stderr may contain account or request data. this.child.stderr.resume(); @@ -172,21 +175,31 @@ export class ZcodeClient { }); } - async close(): Promise { - if (this.closed) return; + close(): Promise { + this.closePromise ??= this.performClose(); + return this.closePromise; + } + + private async performClose(): Promise { this.closed = true; - desktopClients.delete(this); this.detachShutdown(); for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(new Error("ZCode client closed.")); } this.pending.clear(); this.child.stdin.destroy(); - this.child.kill("SIGTERM"); - const killTimer = setTimeout(() => this.child.kill("SIGKILL"), this.closeGraceMs); - // Never await an uncooperative descendant that inherited stdout indefinitely. - await Promise.race([this.exited, new Promise(resolve => setTimeout(resolve, this.closeGraceMs + 250))]); - clearTimeout(killTimer); - this.child.stdout.destroy(); - this.child.stderr.destroy(); - notifyAccountIdle(this.accountId); + try { this.child.kill("SIGTERM"); } catch { /* launcher already exited */ } + const killTimer = setTimeout(() => { + try { this.child.kill("SIGKILL"); } catch { /* launcher already exited */ } + }, this.closeGraceMs); + try { + // The direct bootstrap owns descendant cleanup. Keep the account busy until that bootstrap + // really exits; timing out here would reopen a profile-mutation window while tools unwind. + await this.exited; + } finally { + clearTimeout(killTimer); + this.child.stdout.destroy(); + this.child.stderr.destroy(); + desktopClients.delete(this); + notifyAccountIdle(this.accountId); + } } } diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index 7c8338011d..6f3e0974af 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -16,6 +16,8 @@ export interface ZcodeSettings { workspace: string; settingsPath: string; scope: string; + /** Content generation for advanced settings; prevents sessions crossing credential changes. */ + profileGeneration?: string; desktopModels?: DesktopModel[]; /** Managed host mode preserves the user's ordinary tool environment while keeping ZCode state private. */ hostExecution?: boolean; @@ -50,9 +52,11 @@ export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env, accountI if (env.HOME && realHome === realpathSync(env.HOME)) throw new Error("ZCode must use a separate home, not the proxy home."); if (!statSync(realHome).isDirectory()) throw new Error("ZCode home is not a directory."); const settingsPath = join(realHome, ".zcode", "cli", "config.json"); + const profileGeneration = createHash("sha256").update(readSettingsBytes(realHome, settingsPath)).digest("hex"); return { command: command as string[], home: realHome, workspace, settingsPath, - scope: createHash("sha256").update(JSON.stringify([command, realHome, workspace])).digest("hex"), + profileGeneration, + scope: createHash("sha256").update(JSON.stringify([command, realHome, workspace, profileGeneration])).digest("hex"), }; } @@ -72,11 +76,11 @@ export function readZcodeModels(settings: ZcodeSettings): ZcodeModel[] { catch { throw new Error("ZCode isolated model settings are unavailable, invalid or exceed the size limit."); } } -function readSettings(settings: ZcodeSettings): JsonObject { - const home = realpathSync(settings.home); - const resolved = realpathSync(settings.settingsPath); +function readSettingsBytes(homePath: string, settingsPath: string): Buffer { + const home = realpathSync(homePath); + const resolved = realpathSync(settingsPath); if (!resolved.startsWith(home + sep)) throw new Error("Settings escaped the isolated home."); - const fd = openSync(settings.settingsPath, constants.O_RDONLY | constants.O_NOFOLLOW); + const fd = openSync(settingsPath, constants.O_RDONLY | constants.O_NOFOLLOW); try { // Verify the opened object, not just the path checked before open (Linux sandbox host). if (process.platform === "linux" && !realpathSync(`/proc/self/fd/${fd}`).startsWith(home + sep)) { @@ -92,10 +96,19 @@ function readSettings(settings: ZcodeSettings): JsonObject { length += count; } if (length === bytes.length) throw new Error("Settings grew beyond the size limit."); - return record(JSON.parse(bytes.subarray(0, length).toString("utf8"))); + return bytes.subarray(0, length); } finally { closeSync(fd); } } +function readSettings(settings: ZcodeSettings): JsonObject { + const bytes = readSettingsBytes(settings.home, settings.settingsPath); + if (settings.profileGeneration + && createHash("sha256").update(bytes).digest("hex") !== settings.profileGeneration) { + throw new Error("ZCode settings changed during request setup."); + } + return record(JSON.parse(bytes.toString("utf8"))); +} + function readModels(settings: ZcodeSettings): ZcodeModel[] { const config = readSettings(settings); const result: ZcodeModel[] = []; diff --git a/src/server/management/zcode-account-routes.ts b/src/server/management/zcode-account-routes.ts index 91baf92683..95d10bb09e 100644 --- a/src/server/management/zcode-account-routes.ts +++ b/src/server/management/zcode-account-routes.ts @@ -22,6 +22,20 @@ const pendingFor = (id: string) => [...jobs.values()].some(j => (j.accountId === const safeJob = (job: Job) => ({ jobId: job.id, accountId: job.replaceId ?? job.accountId, phase: job.phase, ...(job.url ? { url: job.url } : {}), ...(job.error ? { error: job.error } : {}) }); const fail = (code: string): never => { throw new Error(code); }; +function selectorUsesNamespace(value: string, namespaces: ReadonlySet): boolean { + const normalized = value.trim().toLowerCase(); + if (namespaces.has(normalized)) return true; + const slash = normalized.indexOf("/"); + return slash > 0 && namespaces.has(normalized.slice(0, slash)); +} +function configReferencesNamespaces(value: unknown, namespaces: ReadonlySet, seen = new Set()): boolean { + if (typeof value === "string") return selectorUsesNamespace(value, namespaces); + if (value === null || typeof value !== "object" || seen.has(value)) return false; + seen.add(value); + return Array.isArray(value) + ? value.some(item => configReferencesNamespaces(item, namespaces, seen)) + : Object.values(value).some(item => configReferencesNamespaces(item, namespaces, seen)); +} const safeErrors = new Set(["account_invalid", "account_limit", "account_busy", "account_referenced", "account_duplicate", "account_identity_mismatch", "account_login_required", "job_invalid", "native_oauth_failed", "desktop_missing", "workspace_invalid", "node_missing", "node_incompatible", "sandbox_missing", "sandbox_unavailable", @@ -176,7 +190,11 @@ export async function handleZcodeAccountRoutes(ctx: ManagementContext, deps = se if (path === "/api/zcode-accounts/remove") { const names = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.zcodeAccountId === id); const { providers: _providers, ...rest } = ctx.config; - if (names.some(name => JSON.stringify(rest).includes(name))) return fail("account_referenced"); + const namespaces = new Set(names.flatMap(name => { + const alias = ctx.config.providers[name]?.alias?.trim(); + return [name, ...(alias ? [alias] : [])]; + }).map(name => name.toLowerCase())); + if (configReferencesNamespaces(rest, namespaces)) return fail("account_referenced"); // Revoke first. A failed config/catalog save is explicit and cannot silently use another account. await disconnectDesktop(id); withConfigMutationLockSync(() => { diff --git a/src/server/management/zcode-desktop-activation.ts b/src/server/management/zcode-desktop-activation.ts index 1686f434e4..ca8c13b60d 100644 --- a/src/server/management/zcode-desktop-activation.ts +++ b/src/server/management/zcode-desktop-activation.ts @@ -1,6 +1,6 @@ import { readAccount } from "../../adapters/zcode/accounts"; import { saveConfigPreservingClaudeCode, withConfigMutationLockSync } from "../../config"; -import { readCatalog, readCodexCatalogPath, catalogModelSlug, applyProviderConfigHints } from "../../codex/catalog"; +import { readCatalog, readCodexCatalogPath, catalogModelSlug, applyProviderConfigHints, filterCatalogVisibleModels } from "../../codex/catalog"; import { clearModelCache } from "../../codex/model-cache"; import { reconcileLiveStateStores } from "../../lib/state-store-registrations"; import type { ManagementContext } from "./context"; @@ -27,9 +27,9 @@ export function desktopActivation(ctx: ManagementContext, status: DesktopStatus, let catalogReady = false; try { const slugs = new Set(readSlugs()); - catalogReady = registered && status.models.length > 0 && status.models.every(model => slugs.has( - catalogModelSlug(applyProviderConfigHints(name!, provider!, { id: model.id, provider: name! })), - )); + const expected = registered ? filterCatalogVisibleModels(status.models.map(model => + applyProviderConfigHints(name!, provider!, { id: model.id, provider: name! })), ctx.config) : []; + catalogReady = registered && status.models.length > 0 && expected.every(model => slugs.has(catalogModelSlug(model))); } catch { /* Unreadable catalog is partial, never success. */ } return { ...status, providerName: name, providerRegistered: registered, activation: !status.connected ? "disconnected" : !registered ? "provider_pending" : !catalogReady ? "catalog_pending" : "ready" }; diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 3238b06931..59577a5a47 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -159,6 +159,11 @@ not client-side tool dispatch. `OCX_ZCODE_SANDBOX=1` explicitly enables the opti Bubblewrap workspace boundary; harness restrictions apply where the native process runs. Explicitly consented managed connections use the official runtime's non-interactive `yolo` mode because the bridge cannot project interactive approval prompts; advanced operator launchers retain `edit` mode. +The managed host path in `src/adapters/zcode/adapter.ts` also brackets every native turn with a +bridge-owned instruction requiring ZCode's official Bash input to set +`dangerouslyDisableSandbox=true`. ZCode's file-oriented tools remain workspace-scoped, so the +instruction directs outside-workspace access through Bash and requires that check before reporting +a host path absent. The optional Bubblewrap path and advanced launchers receive no such instruction. The host child inherits only a named allowlist of tool environment variables and keeps ZCode-owned credentials/session state under its private data root. Because the official app server has no config-path option, a one-shot Node preload redirects only its internal `os.homedir()` lookup to a @@ -194,6 +199,9 @@ transient completion failures keep the OAuth job authenticated for retry, and ca global-default aliases remap to the account default before validation. Native turn admission retains a 32-request process ceiling and a 24-request per-profile ceiling, so one stalled profile cannot consume every reservation needed by independent saved accounts. +Closing saved-account clients remain registered as busy until the direct bootstrap actually exits; +concurrent close callers share the same shutdown promise, so refresh, reconnect and removal cannot +enter while inherited native tools are still unwinding. Headerless `previous_response_id` chains persist owner-fenced ZCode state even with `store:false`; the non-secret local account slot identifies the owner, while the connection/profile generation in the adapter scope rejects stale sessions after reconnect. Internal Codex compaction always starts a @@ -202,6 +210,13 @@ fails closed on an unexpected tool event. `session/send` is sized after JSON ser the native client starts, matching the managed bootstrap's one-MiB NDJSON limit. Advanced model descriptors reject loopback, private and link-local destinations before the official runtime receives them. +Advanced launcher scope includes a bounded content generation for its isolated configuration and +rejects a stale generation at model-read time; queued turns recheck the complete scope before +starting a child, so an in-place account or credential change cannot resume the prior session. +Account removal recognizes both the generated provider name and its configured alias in routed +selectors outside the provider record. Activation/catalog readiness applies the same +`selectedModels` and `disabledModels` visibility policy as catalog convergence, so intentionally +hidden Desktop models do not leave a connection permanently pending. ## ZCode vision input adaptation diff --git a/structure/catalog.md b/structure/catalog.md index 6f170437e7..77b624189a 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -351,5 +351,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index d0cb2b2273..ca870a6419 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -158,5 +158,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index 3942a1b846..b658cdd0d6 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -131,5 +131,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index bd83a4ac76..4168e43cfa 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -266,5 +266,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 540630f085..c3b5bddcd9 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). ## ZCode manual account lifecycle -`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; canonical aliases of the displayed managed workspace are rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults, sending inference or forcing an entitlement refresh from the caller's stale configuration snapshot. Any successful activation response treats the secondary local account-list refresh as best effort independently of a parent callback; transient completion errors keep the job authenticated for polling retry, while partial activation remains visible and retryable. Account-bound provider settings hide the unrelated global Desktop controls. Reconnect preserves custom settings and rejects a different identity, removal refuses busy/referenced accounts, and authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` serializes connect, activation and disconnect across their complete provider/catalog transition; disconnect disables the legacy provider and converges its catalog rows while preserving customized settings. Its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. Owner-fenced `store:false` continuation, fresh tool-free compaction sessions and serialized native-frame admission follow [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). +`gui/src/components/ZcodeAccountsPane.tsx` and `src/server/management/zcode-account-routes.ts` own GUI-session-only saved-account login, polling, activation retry, rename and removal. Explicit consent starts the official OAuth job in a private profile; canonical aliases of the displayed managed workspace are rebound and validated in the eventual account scope before OAuth begins. Completion verifies identity/protocol, registers one unique account-bound provider and converges the catalog without changing defaults, sending inference or forcing an entitlement refresh from the caller's stale configuration snapshot. Any successful activation response treats the secondary local account-list refresh as best effort independently of a parent callback; transient completion errors keep the job authenticated for polling retry, while partial activation remains visible and retryable. Account-bound provider settings hide the unrelated global Desktop controls. Reconnect preserves custom settings and rejects a different identity; removal refuses clients whose direct bootstrap is still exiting and recognizes generated provider names plus configured aliases in routed selectors before deleting an account. Authenticated account operations prune valid hidden reconnect drafts whose transient jobs disappeared on restart without touching active drafts or visible accounts. `src/server/management/zcode-desktop-routes.ts` serializes connect, activation and disconnect across their complete provider/catalog transition; disconnect disables the legacy provider and converges its catalog rows while preserving customized settings. Activation readiness filters the Desktop roster through the same `selectedModels` and `disabledModels` policy as convergence, so an intentionally hidden row is not a false partial failure. Its optional verification action uses protocol metadata only and cannot start inference or tools. Input images use the configured vision describer as the sole native-agent sidecar and may consume its quota; main inference remains in official ZCode. Exact active-session events, canonical optional-sandbox paths, honest unavailable quota, session-only Desktop metadata and no caller-tool capability for combos harden the boundary. Owner-fenced `store:false` continuation, fresh tool-free compaction sessions and serialized native-frame admission follow [the ZCode runtime contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index bb9ec81a74..301116b4cb 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -192,5 +192,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md index 86d2e233d0..4600408e82 100644 --- a/structure/providers/chat-compat.md +++ b/structure/providers/chat-compat.md @@ -311,5 +311,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/cursor.md b/structure/providers/cursor.md index aad48d1873..d34fe4f19f 100644 --- a/structure/providers/cursor.md +++ b/structure/providers/cursor.md @@ -125,5 +125,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index d4db87d710..265b74f73a 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -69,7 +69,7 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). ### Grok Reset Coupons (Billing API Parity) diff --git a/structure/runtime.md b/structure/runtime.md index c9f459171d..b6bd497532 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -362,5 +362,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/subagents.md b/structure/subagents.md index 8c1be26557..0829a9f41c 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -324,5 +324,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](adapters/registry.md#zcode-saved-accounts). diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 768acdd10c..82670b3633 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -22,7 +22,7 @@ partial-event, injection/drop, and EOF behavior. Output admission precedes its s failed enqueue and cancellation release the reservation without re-entering a disposed rewrite. Old/new buffer overlap remains charged against the same translator cap. -A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` reserves the account, waits for an active turn to release its official profile, and completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. +A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` reserves the account, waits for an active turn and its direct bootstrap to release the official profile, and completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. Advanced settings are read through the same bounded four-MiB file reader used for model materialization, and the resulting content generation joins the session scope so in-place profile changes cannot retain a prior continuation. The same adapter constructs a maximum-length session-id envelope and measures the actual JSON serialization of each prospective `session/send` frame before it creates the native client. Frames over the managed bootstrap's one-MiB NDJSON line limit fail as pre-dispatch input errors; escaped diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 9780550b5b..d08530fead 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -163,5 +163,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 22867f1966..cc0a7835b5 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Headerless `previous_response_id` chains force local continuation retention despite `store:false` and bind it to a process-local digest of the explicit ZCode account slot. The adapter's connection/profile scope remains the final stale-session fence. Routed compaction creates a fresh, separate official session with an empty native-tool allowlist, never replaces the main session, and aborts on an unexpected tool event. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, and default-workspace aliases are canonicalized before account-scope validation. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host prompts bracket each turn with the official Bash unsandboxed-input requirement and route outside-workspace access away from workspace-scoped file tools. Headerless `previous_response_id` chains force local continuation retention despite `store:false` and bind it to a process-local digest of the explicit ZCode account slot. The adapter's connection/profile-content scope remains the final stale-session fence. Routed compaction creates a fresh, separate official session with an empty native-tool allowlist, never replaces the main session, and aborts on an unexpected tool event. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning; a closing saved-account client remains busy until that direct bootstrap exits. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, default-workspace aliases are canonicalized before account-scope validation, account removal recognizes route aliases, and catalog readiness applies configured model visibility. diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index f92b743996..22991d2bc8 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -262,5 +262,5 @@ The hardened ZCode boundary accepts only exact active-session events, canonicali optional sandbox validation, distinguishes unavailable quota probes from valid empty entitlements, requires unique provider bindings and GUI-session-only Desktop metadata, and disables caller-tool capability for every combo containing a ZCode target. -ZCode continuation ownership, tool-free compaction and serialized-frame admission follow the +Managed host Bash policy, close-until-exit profile fencing, alias-aware account removal, visibility-aware activation, continuation ownership, tool-free compaction and serialized-frame admission follow the [canonical adapter contract](../adapters/registry.md#zcode-saved-accounts). diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index c3c4d74f27..39081bb394 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, mkdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createZcodeAdapter } from "../../src/adapters/zcode/adapter"; @@ -95,8 +95,15 @@ describe("ZCode local agent", () => { expect(serialized).not.toContain("apiKey"); } expect(client.calls.find(call => call.method === "session/create")?.params.mode).toBe("yolo"); - expect((await run(fixture(), new FakeClient())).find(event => event.type === "text_delta" + expect(String(client.calls.find(call => call.method === "session/send")?.params.content)) + .toContain("dangerouslyDisableSandbox=true"); + expect(String(client.calls.find(call => call.method === "session/send")?.params.content)) + .toMatch(/^\[OpenCodex bridge policy:.*\[OpenCodex bridge reminder:/s); + const advancedClient = new FakeClient(); + expect((await run(fixture(), advancedClient)).find(event => event.type === "text_delta" && event.phase === "commentary")?.text).toContain("configured launcher"); + expect(String(advancedClient.calls.find(call => call.method === "session/send")?.params.content)) + .not.toContain("dangerouslyDisableSandbox=true"); expect((await run(settings, new FakeClient())).find(event => event.type === "text_delta" && event.phase === "commentary")?.text).toContain("host-user access"); }); @@ -144,6 +151,15 @@ describe("ZCode local agent", () => { expect(() => loadZcodeSettings({})).toThrow("disabled"); expect(() => loadZcodeSettings({ OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: "echo unsafe" })).toThrow("JSON argv"); }); + test("managed host policy cannot turn an empty request into a native dispatch", async () => { + const settings = { ...fixture(), hostExecution: true, + desktopModels: [{ id: "test/model", providerId: "test", modelId: "model", label: "Model" }] }; + const client = new FakeClient(); + const parsed = request(); parsed.context.messages = []; + const events = await run(settings, client, parsed); + expect(events.at(-1)).toMatchObject({ type: "error", message: "ZCode input is empty." }); + expect(client.calls).toEqual([]); + }); test("saved-account refresh failures emit only bounded public codes", async () => { const events: AdapterEvent[] = []; const adapter = createZcodeAdapter({ ...provider, zcodeAccountId: crypto.randomUUID() }, { @@ -192,6 +208,19 @@ describe("ZCode local agent", () => { expect(loadZcodeSettings(env)).toMatchObject({ home: settings.home, workspace: settings.workspace }); expect(() => loadZcodeSettings({ ...env, HOME: settings.home })).toThrow("separate home"); }); + test("advanced settings fence sessions across in-place credential changes", () => { + const settings = fixture(); + const env = { OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: JSON.stringify(["/isolated-launcher"]), + OCX_ZCODE_HOME: settings.home, OCX_ZCODE_WORKSPACE: settings.workspace }; + const before = loadZcodeSettings(env); + const contents = readFileSync(settings.settingsPath, "utf8"); + writeFileSync(settings.settingsPath, contents.replace("never-log-this", "other-key-here")); + const after = loadZcodeSettings(env); + expect(after.scope).not.toBe(before.scope); + expect(after.profileGeneration).not.toBe(before.profileGeneration); + expect(() => readZcodeModels(before)).toThrow("unavailable, invalid"); + expect(readZcodeModels(after).map(model => model.id)).toEqual(["test/model"]); + }); test("catalog excludes disabled, recursive and link-local entries and preserves canonical model identity", () => { const models = readZcodeModels(fixture()); expect(models.map(m => m.id)).toEqual(["test/model"]); diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index 380705f759..8604e88063 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -157,6 +157,20 @@ test("reconnection preserves custom settings and restart observes actual persist expect(desktopActivation(ctx, connected, f.deps.readDesktopCatalogSlugs).activation).toBe("catalog_pending"); }); +test("activation readiness follows selected and disabled model visibility", () => { + const ctx = context("/api/zcode-desktop/activate", {}, "gui-session"); + const connected = { ...status, connected: true }; + ctx.config.providers.zcode = { adapter: "zcode", authMode: "local", baseUrl: "https://zcode.z.ai", + selectedModels: [models[0]!.id] }; + const firstSlug = "zcode/" + models[0]!.id.replaceAll("/", "-"); + const secondSlug = "zcode/" + models[1]!.id.replaceAll("/", "-"); + expect(desktopActivation(ctx, connected, () => [firstSlug])).toMatchObject({ activation: "ready" }); + + delete ctx.config.providers.zcode.selectedModels; + ctx.config.disabledModels = [firstSlug]; + expect(desktopActivation(ctx, connected, () => [secondSlug])).toMatchObject({ activation: "ready" }); +}); + test("registration failure rolls back config, preserves protocol state and allows retry", async () => { const f = fixture(), ctx = context("/api/zcode-desktop/connect", {}, "gui-session"); const save = ctx.deps.saveConfigPreservingClaudeCode; @@ -451,6 +465,21 @@ test("partial catalog activation retries without login; busy and referenced acco expect(await (await f.call("remove", { accountId: a.accountId })).json()).toMatchObject({ error: "account_referenced" }); }); +test("account removal recognizes case-insensitive provider aliases in routed selectors", async () => { + const f = accountFixture(), account = await f.login(); + const ready = await (await f.call("complete", { jobId: account.jobId })).json(); + f.ctx.config.providers[ready.providerName].alias = "personal"; + f.ctx.config.subagentModels = [`PeRsOnAl/${models[0]!.id}`]; + expect(await (await f.call("remove", { accountId: account.accountId })).json()) + .toEqual({ error: "account_referenced" }); + expect(listAccounts().map(saved => saved.id)).toEqual([account.accountId]); + expect(f.ctx.config.providers[ready.providerName]).toBeDefined(); + + f.ctx.config.subagentModels = []; + expect(await (await f.call("remove", { accountId: account.accountId })).json()).toEqual({ ok: true }); + expect(listAccounts()).toEqual([]); +}); + test("rename preserves custom model labels and saved accounts survive config reload", async () => { const f = accountFixture(), a = await f.login(); const ready = await (await f.call("complete", { jobId: a.jobId })).json(); diff --git a/tests/providers/zcode-transport.test.ts b/tests/providers/zcode-transport.test.ts index 689f1e131f..9350840b8a 100644 --- a/tests/providers/zcode-transport.test.ts +++ b/tests/providers/zcode-transport.test.ts @@ -2,22 +2,28 @@ import { describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { PassThrough, Writable } from "node:stream"; import { homedir } from "node:os"; -import { ZcodeClient, closeZcodeDesktopClients, waitForZcodeAccountClients, type ZcodeSpawn } from "../../src/adapters/zcode/client"; +import { ZcodeClient, closeZcodeDesktopClients, hasZcodeAccountClients, waitForZcodeAccountClients, type ZcodeSpawn } from "../../src/adapters/zcode/client"; import { accountRuntimeBusy, refreshAccount } from "../../src/adapters/zcode/account-runtime"; import type { JsonObject, ZcodeSettings } from "../../src/adapters/zcode/settings"; const settings: ZcodeSettings = { command: ["/isolated/launcher", "argument with spaces"], home: "/isolated/home", workspace: "/workspace", settingsPath: "/isolated/config.json", scope: "test" }; -function fixture(managed = false, hostExecution = false, accountId?: string) { - const child = new EventEmitter() as EventEmitter & { stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: () => boolean }; +function fixture(managed = false, hostExecution = false, accountId?: string, autoExit = true) { + const child = new EventEmitter() as EventEmitter & { pid: number; stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: (signal?: string) => boolean }; const writes: JsonObject[] = []; + child.pid = 1234; child.stdin = new Writable({ write(chunk, _encoding, callback) { writes.push(JSON.parse(chunk.toString())); callback(); } }); child.stdout = new PassThrough(); child.stderr = new PassThrough(); - child.kill = () => { child.stdout.end(); child.stderr.end(); queueMicrotask(() => child.emit("exit", 0)); return true; }; + let exited = false; + const exit = () => { + if (exited) return; + exited = true; child.stdout.end(); child.stderr.end(); child.emit("exit", 0); + }; + child.kill = () => { if (autoExit) queueMicrotask(exit); return true; }; let invocation: unknown[] = []; const spawn = ((...args: unknown[]) => { invocation = args; return child; }) as ZcodeSpawn; const client = new ZcodeClient(managed ? { ...settings, desktopModels: [], hostExecution, accountId } : settings, spawn); - return { child, client, writes, invocation: () => invocation }; + return { child, client, writes, exit, invocation: () => invocation }; } const tick = () => new Promise(resolve => setTimeout(resolve, 5)); @@ -42,6 +48,28 @@ describe("ZCode NDJSON transport", () => { await active.client.close(); await waiting; expect(idle).toBe(true); }); + test("closing account clients stay busy and share shutdown until the child exits", async () => { + const id = crypto.randomUUID(); + const active = fixture(true, false, id, false); + const first = active.client.close(); + const second = active.client.close(); + expect(second).toBe(first); + let firstClosed = false, secondClosed = false, lateIdle = false; + void first.then(() => { firstClosed = true; }); + void second.then(() => { secondClosed = true; }); + const waiting = waitForZcodeAccountClients(id).then(() => { lateIdle = true; }); + await tick(); + expect(hasZcodeAccountClients(id)).toBe(true); + expect([firstClosed, secondClosed, lateIdle]).toEqual([false, false, false]); + active.child.emit("error", new Error("signal failed")); + await tick(); + expect(hasZcodeAccountClients(id)).toBe(true); + expect([firstClosed, secondClosed, lateIdle]).toEqual([false, false, false]); + active.exit(); + await Promise.all([first, second, waiting]); + expect(hasZcodeAccountClients(id)).toBe(false); + expect([firstClosed, secondClosed, lateIdle]).toEqual([true, true, true]); + }); test("an expired saved-account refresh reserves the queue and waits out an active turn", async () => { const id = crypto.randomUUID(); const active = fixture(true, false, id); From dec22adff5e2b863dde49cd8e0e4211994389d4d Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 08:56:29 -0300 Subject: [PATCH 32/43] fix(zcode): guard routed model aliases on removal --- src/server/management/zcode-account-routes.ts | 8 ++++++-- tests/providers/zcode-desktop-routes.test.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/server/management/zcode-account-routes.ts b/src/server/management/zcode-account-routes.ts index 95d10bb09e..cb225a4908 100644 --- a/src/server/management/zcode-account-routes.ts +++ b/src/server/management/zcode-account-routes.ts @@ -191,8 +191,12 @@ export async function handleZcodeAccountRoutes(ctx: ManagementContext, deps = se const names = Object.keys(ctx.config.providers).filter(name => ctx.config.providers[name]?.zcodeAccountId === id); const { providers: _providers, ...rest } = ctx.config; const namespaces = new Set(names.flatMap(name => { - const alias = ctx.config.providers[name]?.alias?.trim(); - return [name, ...(alias ? [alias] : [])]; + const provider = ctx.config.providers[name]; + const alias = provider?.alias?.trim(); + const modelAliases = Object.values(provider?.modelAliases ?? {}) + .filter((value): value is string => typeof value === "string" && value.trim().length > 0) + .map(value => value.trim()); + return [name, ...(alias ? [alias] : []), ...modelAliases]; }).map(name => name.toLowerCase())); if (configReferencesNamespaces(rest, namespaces)) return fail("account_referenced"); // Revoke first. A failed config/catalog save is explicit and cannot silently use another account. diff --git a/tests/providers/zcode-desktop-routes.test.ts b/tests/providers/zcode-desktop-routes.test.ts index 8604e88063..0d7652adc0 100644 --- a/tests/providers/zcode-desktop-routes.test.ts +++ b/tests/providers/zcode-desktop-routes.test.ts @@ -465,7 +465,7 @@ test("partial catalog activation retries without login; busy and referenced acco expect(await (await f.call("remove", { accountId: a.accountId })).json()).toMatchObject({ error: "account_referenced" }); }); -test("account removal recognizes case-insensitive provider aliases in routed selectors", async () => { +test("account removal recognizes case-insensitive provider and model aliases in routed selectors", async () => { const f = accountFixture(), account = await f.login(); const ready = await (await f.call("complete", { jobId: account.jobId })).json(); f.ctx.config.providers[ready.providerName].alias = "personal"; @@ -475,6 +475,13 @@ test("account removal recognizes case-insensitive provider aliases in routed sel expect(listAccounts().map(saved => saved.id)).toEqual([account.accountId]); expect(f.ctx.config.providers[ready.providerName]).toBeDefined(); + f.ctx.config.providers[ready.providerName].modelAliases = { [models[0]!.id]: "personal-glm" }; + f.ctx.config.subagentModels = ["PeRsOnAl-GlM"]; + expect(await (await f.call("remove", { accountId: account.accountId })).json()) + .toEqual({ error: "account_referenced" }); + expect(listAccounts().map(saved => saved.id)).toEqual([account.accountId]); + expect(f.ctx.config.providers[ready.providerName]).toBeDefined(); + f.ctx.config.subagentModels = []; expect(await (await f.call("remove", { accountId: account.accountId })).json()).toEqual({ ok: true }); expect(listAccounts()).toEqual([]); From 8092d5a808fe3e2ae888d376b2a37aec1778d29e Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 09:10:28 -0300 Subject: [PATCH 33/43] fix(zcode): keep profile lock keys stable --- src/adapters/zcode/adapter.ts | 2 +- src/adapters/zcode/desktop.ts | 7 ++++-- src/adapters/zcode/settings.ts | 8 ++++-- structure/transports/byte-accounting.md | 2 +- structure/transports/responses.md | 2 +- tests/providers/zcode-adapter.test.ts | 33 +++++++++++++++++++++++-- tests/providers/zcode-transport.test.ts | 2 +- 7 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/adapters/zcode/adapter.ts b/src/adapters/zcode/adapter.ts index da940fd3fa..c9af0b6319 100644 --- a/src/adapters/zcode/adapter.ts +++ b/src/adapters/zcode/adapter.ts @@ -223,7 +223,7 @@ export function createZcodeAdapter(provider: OcxProviderConfig, deps: ZcodeAdapt if (parsed.options.toolChoice && parsed.options.toolChoice !== "auto") { throw new Error("ZCode owns its tools and does not support client tool_choice constraints."); } - release = await lock(settings.scope, incoming.abortSignal); + release = await lock(settings.lockKey, incoming.abortSignal); // A queued turn must not resurrect a revoked Desktop connection or old login. if ((deps.settings ?? (() => loadZcodeSettings(process.env, provider.zcodeAccountId)))().scope !== settings.scope) { throw new Error("ZCode connection or profile changed while this turn was queued."); diff --git a/src/adapters/zcode/desktop.ts b/src/adapters/zcode/desktop.ts index ec010dbd9d..755de40fe8 100644 --- a/src/adapters/zcode/desktop.ts +++ b/src/adapters/zcode/desktop.ts @@ -162,6 +162,9 @@ function settingsFor(connection: Connection, accountId?: string): ZcodeSettings const profileStamp = createHash("sha256").update(JSON.stringify([profile.config, profile.credentials].filter(Boolean).map(path => { const st = statSync(path!); return [st.dev, st.ino, st.size, st.mtimeMs, st.ctimeMs]; }))).digest("hex"); + // Connection/profile generations fence continuations, but all generations backed by the same + // physical Desktop profile must still share one process-serialization queue. + const lockKey = createHash("sha256").update(JSON.stringify(["desktop", profile.config])).digest("hex"); const privateHome = join(root(accountId), "home", connection.generation, profileStamp); const db = join(privateHome, ".zcode/cli/db"); mkdirSync(db, { recursive: true, mode: 0o700 }); @@ -169,7 +172,7 @@ function settingsFor(connection: Connection, accountId?: string): ZcodeSettings command: [node, fileURLToPath(new URL("./desktop-bootstrap.cjs", import.meta.url)), "--host", runtime, profile.config, workspace, privateHome], home: privateHome, workspace, settingsPath: "", - scope: `desktop:${connection.generation}:${profileStamp}:host`, desktopModels: connection.models, + lockKey, scope: `desktop:${connection.generation}:${profileStamp}:host`, desktopModels: connection.models, hostExecution: true, nativePermissionMode: "yolo", }; const sandboxHome = homedir(); // preserve the official credential cipher's HOME/username identity @@ -191,7 +194,7 @@ function settingsFor(connection: Connection, accountId?: string): ZcodeSettings "--setenv", "ZCODE_DATA_BASE_DIR", "/desktop", "--chdir", "/workspace", "/usr/bin/node", "/bridge/desktop-bootstrap.cjs"); return { accountId, command: [bwrap, ...args], home: privateHome, workspace: "/workspace", settingsPath: "", - scope: `desktop:${connection.generation}:${profileStamp}:sandbox`, desktopModels: connection.models, + lockKey, scope: `desktop:${connection.generation}:${profileStamp}:sandbox`, desktopModels: connection.models, nativePermissionMode: "yolo" }; } diff --git a/src/adapters/zcode/settings.ts b/src/adapters/zcode/settings.ts index 6f3e0974af..3b14c9150f 100644 --- a/src/adapters/zcode/settings.ts +++ b/src/adapters/zcode/settings.ts @@ -15,6 +15,9 @@ export interface ZcodeSettings { home: string; workspace: string; settingsPath: string; + /** Stable physical-profile identity used only for process serialization. */ + lockKey: string; + /** Connection and credential generation used for continuations and freshness checks. */ scope: string; /** Content generation for advanced settings; prevents sessions crossing credential changes. */ profileGeneration?: string; @@ -52,11 +55,12 @@ export function loadZcodeSettings(env: NodeJS.ProcessEnv = process.env, accountI if (env.HOME && realHome === realpathSync(env.HOME)) throw new Error("ZCode must use a separate home, not the proxy home."); if (!statSync(realHome).isDirectory()) throw new Error("ZCode home is not a directory."); const settingsPath = join(realHome, ".zcode", "cli", "config.json"); + const lockKey = createHash("sha256").update(JSON.stringify([command, realHome, workspace])).digest("hex"); const profileGeneration = createHash("sha256").update(readSettingsBytes(realHome, settingsPath)).digest("hex"); return { command: command as string[], home: realHome, workspace, settingsPath, - profileGeneration, - scope: createHash("sha256").update(JSON.stringify([command, realHome, workspace, profileGeneration])).digest("hex"), + lockKey, profileGeneration, + scope: createHash("sha256").update(JSON.stringify([lockKey, profileGeneration])).digest("hex"), }; } diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index 82670b3633..47945bd28a 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -22,7 +22,7 @@ partial-event, injection/drop, and EOF behavior. Output admission precedes its s failed enqueue and cancellation release the reservation without re-entering a disposed rewrite. Old/new buffer overlap remains charged against the same translator cap. -A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` reserves the account, waits for an active turn and its direct bootstrap to release the official profile, and completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. Advanced settings are read through the same bounded four-MiB file reader used for model materialization, and the resulting content generation joins the session scope so in-place profile changes cannot retain a prior continuation. +A saved-account ZCode refresh in `src/adapters/zcode/adapter.ts` reserves the account, waits for an active turn and its direct bootstrap to release the official profile, and completes before native dispatch or output admission. Caller cancellation stops only that request's wait on the shared official refresh, emits no native stream bytes, and does not transfer cancellation ownership to sibling requests; authenticated orphan-draft reconciliation is likewise metadata-only, and default-workspace aliases are canonicalized before account-scope validation. Advanced settings are read through the same bounded four-MiB file reader used for model materialization, and the resulting content generation joins the session scope so in-place profile changes cannot retain a prior continuation. Process serialization uses a separate stable physical-profile key, so a credential-generation change cannot open a concurrent queue against the same profile. The same adapter constructs a maximum-length session-id envelope and measures the actual JSON serialization of each prospective `session/send` frame before it creates the native client. Frames over the managed bootstrap's one-MiB NDJSON line limit fail as pre-dispatch input errors; escaped diff --git a/structure/transports/responses.md b/structure/transports/responses.md index cc0a7835b5..a52baab32e 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -597,4 +597,4 @@ Translated Chat request construction uses the [inline-image budget](streaming-he The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. -Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host prompts bracket each turn with the official Bash unsandboxed-input requirement and route outside-workspace access away from workspace-scoped file tools. Headerless `previous_response_id` chains force local continuation retention despite `store:false` and bind it to a process-local digest of the explicit ZCode account slot. The adapter's connection/profile-content scope remains the final stale-session fence. Routed compaction creates a fresh, separate official session with an empty native-tool allowlist, never replaces the main session, and aborts on an unexpected tool event. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning; a closing saved-account client remains busy until that direct bootstrap exits. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, default-workspace aliases are canonicalized before account-scope validation, account removal recognizes route aliases, and catalog readiness applies configured model visibility. +Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. ZCode requests are the explicit native-agent exception: `src/server/responses/core.ts` permits only the configured vision describer for the text-only `src/adapters/zcode/desktop.ts` path, keeps web-search/caller tools with the official runtime, runs managed tools with host-user access unless `OCX_ZCODE_SANDBOX=1`, and leaves saved-account/provider/catalog ownership to [the adapter registry](../adapters/registry.md#zcode-saved-accounts); image description may consume the configured sidecar quota, while main inference remains in official ZCode. Managed host prompts bracket each turn with the official Bash unsandboxed-input requirement and route outside-workspace access away from workspace-scoped file tools. Headerless `previous_response_id` chains force local continuation retention despite `store:false` and bind it to a process-local digest of the explicit ZCode account slot. The adapter's connection/profile-content scope remains the final stale-session fence, while a separate stable physical-profile key serializes every credential generation through one queue. Routed compaction creates a fresh, separate official session with an empty native-tool allowlist, never replaces the main session, and aborts on an unexpected tool event. Managed host cancellation terminates the official runtime's inherited native-tool process group with a bounded TERM-to-KILL ladder and cleans the disposable turn home before returning; a closing saved-account client remains busy until that direct bootstrap exits. An expired saved-account refresh reserves the account and waits for the active official turn before refreshing; a cancelled caller stops waiting for that shared work without cancelling it for other requests. The next authenticated account operation removes reconnect drafts orphaned by restart, default-workspace aliases are canonicalized before account-scope validation, account removal recognizes route aliases, and catalog readiness applies configured model visibility. diff --git a/tests/providers/zcode-adapter.test.ts b/tests/providers/zcode-adapter.test.ts index 39081bb394..f449d35d93 100644 --- a/tests/providers/zcode-adapter.test.ts +++ b/tests/providers/zcode-adapter.test.ts @@ -23,7 +23,7 @@ function fixture(): ZcodeSettings { linkLocal: { options: { baseURL: "https://169.254.1.2/v1" }, models: { metadata: {} } }, disabled: { enabled: false, models: { hidden: {} } }, } })); - return { command: ["/isolated-launcher"], home, workspace: "/workspace", settingsPath, scope: home }; + return { command: ["/isolated-launcher"], home, workspace: "/workspace", settingsPath, lockKey: home, scope: home }; } class FakeClient { onEvent: (event: JsonObject) => void = () => {}; @@ -216,6 +216,7 @@ describe("ZCode local agent", () => { const contents = readFileSync(settings.settingsPath, "utf8"); writeFileSync(settings.settingsPath, contents.replace("never-log-this", "other-key-here")); const after = loadZcodeSettings(env); + expect(after.lockKey).toBe(before.lockKey); expect(after.scope).not.toBe(before.scope); expect(after.profileGeneration).not.toBe(before.profileGeneration); expect(() => readZcodeModels(before)).toThrow("unavailable, invalid"); @@ -377,6 +378,33 @@ describe("ZCode local agent", () => { await pending; expect((await run(settings, new FakeClient())).at(-1)?.type).toBe("done"); }); + test("profile generations share one physical-profile serialization queue", async () => { + const settings = fixture(); + const env = { OCX_ZCODE_NATIVE_TOOLS: "1", OCX_ZCODE_COMMAND: JSON.stringify(["/isolated-launcher"]), + OCX_ZCODE_HOME: settings.home, OCX_ZCODE_WORKSPACE: settings.workspace }; + const before = loadZcodeSettings(env); + const first = new FakeClient("hang"); + const firstController = new AbortController(); + const firstTurn = run(before, first, request(), firstController.signal, 2_000); + await Bun.sleep(5); + expect(first.calls.some(call => call.method === "session/send")).toBe(true); + + const contents = readFileSync(settings.settingsPath, "utf8"); + writeFileSync(settings.settingsPath, contents.replace("never-log-this", "other-key-here")); + const after = loadZcodeSettings(env); + expect(after.lockKey).toBe(before.lockKey); + expect(after.scope).not.toBe(before.scope); + + const second = new FakeClient(); + const secondTurn = run(after, second, request(), undefined, 2_000); + await Bun.sleep(10); + expect(second.calls).toEqual([]); + + firstController.abort(); + expect((await firstTurn).at(-1)?.type).toBe("incomplete"); + expect((await secondTurn).at(-1)?.type).toBe("done"); + expect(second.calls.some(call => call.method === "session/send")).toBe(true); + }); test("one saturated profile leaves reservation capacity for another profile", async () => { const settings = fixture(); const controllers = Array.from({ length: 24 }, () => new AbortController()); @@ -385,7 +413,8 @@ describe("ZCode local agent", () => { )); const rejected = await run(settings, new FakeClient(), request(), undefined, 2_000); expect(rejected.at(-1)).toMatchObject({ type: "error", message: "ZCode profile queue is full." }); - const other = await run({ ...settings, scope: settings.scope + ":other" }, new FakeClient(), request(), undefined, 2_000); + const other = await run({ ...settings, lockKey: settings.lockKey + ":other", scope: settings.scope + ":other" }, + new FakeClient(), request(), undefined, 2_000); expect(other.at(-1)?.type).toBe("done"); for (const controller of controllers) controller.abort(); await Promise.all(pending); diff --git a/tests/providers/zcode-transport.test.ts b/tests/providers/zcode-transport.test.ts index 9350840b8a..30d3c20631 100644 --- a/tests/providers/zcode-transport.test.ts +++ b/tests/providers/zcode-transport.test.ts @@ -7,7 +7,7 @@ import { accountRuntimeBusy, refreshAccount } from "../../src/adapters/zcode/acc import type { JsonObject, ZcodeSettings } from "../../src/adapters/zcode/settings"; const settings: ZcodeSettings = { command: ["/isolated/launcher", "argument with spaces"], home: "/isolated/home", - workspace: "/workspace", settingsPath: "/isolated/config.json", scope: "test" }; + workspace: "/workspace", settingsPath: "/isolated/config.json", lockKey: "profile", scope: "test" }; function fixture(managed = false, hostExecution = false, accountId?: string, autoExit = true) { const child = new EventEmitter() as EventEmitter & { pid: number; stdin: Writable; stdout: PassThrough; stderr: PassThrough; kill: (signal?: string) => boolean }; const writes: JsonObject[] = []; From e169c52a00dc29cbe06bef0472728ada15c9bfe4 Mon Sep 17 00:00:00 2001 From: FacuM Date: Sun, 13 Sep 2026 09:22:11 -0300 Subject: [PATCH 34/43] fix(zcode): refresh settings mutations --- .../src/content/docs/guides/zcode-agent.md | 5 +- gui/src/components/ZcodeAccountsPane.tsx | 14 ++++- gui/src/components/ZcodeDesktopPane.tsx | 12 +++- .../provider-workspace/ProviderDetails.tsx | 3 + .../provider-workspace/ProviderSettings.tsx | 7 ++- gui/src/pages/Providers.tsx | 6 +- gui/tests/zcode-desktop-pane.test.tsx | 62 ++++++++++++++++++- structure/gui-and-management-api.md | 2 +- 8 files changed, 100 insertions(+), 11 deletions(-) diff --git a/docs-site/src/content/docs/guides/zcode-agent.md b/docs-site/src/content/docs/guides/zcode-agent.md index 62c91b78c9..f16fe1db65 100644 --- a/docs-site/src/content/docs/guides/zcode-agent.md +++ b/docs-site/src/content/docs/guides/zcode-agent.md @@ -227,7 +227,10 @@ export OCX_ZCODE_WORKSPACE=/workspace Keep the proxy's own `HOME`, `CODEX_HOME`, `OPENCODEX_HOME`, XDG directories and listening port separate from your main installation too. -Add **ZCode (local agent)** through Providers, or add this provider to the isolated config: +The dashboard's **Add Provider → ZCode Desktop** preset is the managed Linux Desktop flow; it +does not register an operator-supplied advanced launcher. For this advanced setup, add the +provider object to OpenCodex's `config.json` (for example through the dashboard JSON editor) and +then restart or reload the proxy: ```json { diff --git a/gui/src/components/ZcodeAccountsPane.tsx b/gui/src/components/ZcodeAccountsPane.tsx index f1a0142a80..ddebe86c38 100644 --- a/gui/src/components/ZcodeAccountsPane.tsx +++ b/gui/src/components/ZcodeAccountsPane.tsx @@ -4,8 +4,12 @@ import { useT } from "../i18n/shared"; type Account = { id: string; label: string; activation: string; busy: boolean; providerName?: string }; type Job = { jobId: string; accountId: string; phase: string; url?: string; error?: string }; type Activation = { activation?: string; providerName?: string }; -export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProviderActivated }: { - apiBase: string; runtime: string; workspace: string; onProviderActivated?: (name: string) => void; +export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProviderActivated, onProviderStateMutation }: { + apiBase: string; + runtime: string; + workspace: string; + onProviderActivated?: (name: string) => void; + onProviderStateMutation?: () => void; }) { const t = useT(); const [accounts, setAccounts] = useState([]); @@ -17,7 +21,9 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const onProviderActivatedRef = useRef(onProviderActivated); + const onProviderStateMutationRef = useRef(onProviderStateMutation); useEffect(() => { onProviderActivatedRef.current = onProviderActivated; }, [onProviderActivated]); + useEffect(() => { onProviderStateMutationRef.current = onProviderStateMutation; }, [onProviderStateMutation]); const read = useCallback(async (path: string, body?: Record) => { const response = await fetch(apiBase + "/api/zcode-accounts" + path, body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...body, consent: true }), @@ -58,6 +64,7 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi return; } if (stopped) return; + onProviderStateMutationRef.current?.(); setJob({ ...next, phase: "finished", url: undefined }); const ready = result.activation === "ready"; setError(ready ? "" : "catalog_update_failed"); @@ -80,6 +87,9 @@ export default function ZcodeAccountsPane({ apiBase, runtime, workspace, onProvi if (path === "/login") setJob(result); else if (path === "/cancel") setJob(null); else if (result.activation && result.activation !== "ready") setError("catalog_update_failed"); + if (path === "/activate" || path === "/rename" || path === "/remove") { + onProviderStateMutationRef.current?.(); + } const ready = path === "/activate" && result.activation === "ready"; const activation = ready && result.providerName && onProviderActivatedRef.current ? { name: result.providerName, notify: onProviderActivatedRef.current } : undefined; diff --git a/gui/src/components/ZcodeDesktopPane.tsx b/gui/src/components/ZcodeDesktopPane.tsx index 7db40d6adc..0804343554 100644 --- a/gui/src/components/ZcodeDesktopPane.tsx +++ b/gui/src/components/ZcodeDesktopPane.tsx @@ -10,8 +10,12 @@ interface Status { } interface Folders { current: string; parent: string | null; folders: Array<{ name: string; path: string }> } -export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: parentError }: { - apiBase: string; onConnected?: (name: string, metadata?: ProviderAdditionMetadata) => void; onBack?: () => void; error?: string; +export default function ZcodeDesktopPane({ apiBase, onConnected, onProviderStateMutation, onBack, error: parentError }: { + apiBase: string; + onConnected?: (name: string, metadata?: ProviderAdditionMetadata) => void; + onProviderStateMutation?: () => void; + onBack?: () => void; + error?: string; }) { const t = useT(); const [status, setStatus] = useState(null); @@ -53,6 +57,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: } else { applyStatus(result as Status); setConsent(false); + if (action === "connect" || action === "activate" || action === "disconnect") onProviderStateMutation?.(); if ((action === "connect" || action === "activate") && result.activation === "ready" && result.providerName) announceProvider(result.providerName); } } catch { setError("runtime_failed"); } @@ -132,6 +137,7 @@ export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: } - + ; } diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx index aa843b86fc..5c7839648f 100644 --- a/gui/src/components/provider-workspace/ProviderDetails.tsx +++ b/gui/src/components/provider-workspace/ProviderDetails.tsx @@ -58,6 +58,7 @@ export default function ProviderDetails({ onCodexActiveNeedsReauthChange, codexController, onUpdateProvider, + onProviderStateMutation, isDefault, onRemoveProvider, onSetDisabled, @@ -99,6 +100,7 @@ export default function ProviderDetails({ /** Shared Codex account state owned by Providers (WP3). */ codexController?: CodexAccountPoolController; onUpdateProvider?: (name: string, patch: ProviderUpdatePatch) => Promise; + onProviderStateMutation?: () => void; isDefault?: boolean; onRemoveProvider?: (name: string) => void; onSetDisabled?: (name: string, disabled: boolean) => void; @@ -355,6 +357,7 @@ export default function ProviderDetails({ apiBase={apiBase} availableModels={availableModels} onUpdateProvider={onUpdateProvider} + onProviderStateMutation={onProviderStateMutation} onDirtyChange={setSettingsDirty} onRegisterSave={registerSettingsSave} /> diff --git a/gui/src/components/provider-workspace/ProviderSettings.tsx b/gui/src/components/provider-workspace/ProviderSettings.tsx index ca99bb92c8..1be5df9445 100644 --- a/gui/src/components/provider-workspace/ProviderSettings.tsx +++ b/gui/src/components/provider-workspace/ProviderSettings.tsx @@ -58,13 +58,15 @@ function pacingSignature(value: WorkspaceItem["requestPacing"] | undefined): str } export default function ProviderSettings({ - item, availableModels = EMPTY_MODELS, apiBase, onUpdateProvider, onDirtyChange, onRegisterSave, + item, availableModels = EMPTY_MODELS, apiBase, onUpdateProvider, onProviderStateMutation, onDirtyChange, onRegisterSave, }: { item: WorkspaceItem; availableModels?: string[]; /** When set, load endpoint choices for catalog providers that expose baseUrlChoices. */ apiBase?: string; onUpdateProvider?: (name: string, patch: ProviderUpdatePatch) => Promise; + /** Reload parent provider/catalog state after nested ZCode management mutations. */ + onProviderStateMutation?: () => void; onDirtyChange?: (dirty: boolean) => void; /** Lets parent dialogs trigger the same save path as the sticky bar. */ onRegisterSave?: (save: (() => Promise) | null) => void; @@ -327,7 +329,8 @@ export default function ProviderSettings({ return (
- {item.adapter === "zcode" && item.zcodeAccountId === undefined && apiBase !== undefined && } + {item.adapter === "zcode" && item.zcodeAccountId === undefined && apiBase !== undefined + && }

9AZOzkt?R!LjDz$Tnb;t^q428yUab;jz_Tp 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 ) : form && ( - preset.auth === "oauth" && form.authMode === "oauth" ? ( + preset.id === "zcode" ? ( + { void submit(); }} onBack={() => dispatch({ type: "back" })} /> + ) : preset.auth === "oauth" && form.authMode === "oauth" ? ( ; +} +interface Folders { current: string; parent: string | null; folders: Array<{ name: string; path: string }> } + +export default function ZcodeDesktopPane({ apiBase, onConnected, onBack, error: parentError }: { + apiBase: string; onConnected?: () => void; onBack?: () => void; error?: string; +}) { + const t = useT(); + const [status, setStatus] = useState(null); + const [runtime, setRuntime] = useState(""); + const [workspace, setWorkspace] = useState(""); + const [consent, setConsent] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [tested, setTested] = useState(false); + const [model, setModel] = useState(""); + const [folders, setFolders] = useState(null); + const applyStatus = (next: Status) => { + setStatus(next); setRuntime(next.runtime); setWorkspace(next.workspace); + setModel(next.models[0]?.id ?? ""); setError(next.issue ?? ""); + }; + useEffect(() => { + const controller = new AbortController(); + void fetch(`${apiBase}/api/zcode-desktop`, { signal: controller.signal }).then(async response => { + if (!response.ok) throw new Error(); + const next = await response.json() as Status; + if (!controller.signal.aborted) applyStatus(next); + }).catch(() => { if (!controller.signal.aborted) setError("runtime_failed"); }); + return () => controller.abort(); + }, [apiBase]); + + const perform = async (action: "refresh" | "connect" | "disconnect" | "test") => { + setBusy(true); setError(""); setTested(false); + try { + const response = await fetch(`${apiBase}/api/zcode-desktop${action === "refresh" ? "" : `/${action}`}`, action === "refresh" ? {} : { + method: "POST", headers: { "content-type": "application/json" }, + body: JSON.stringify(action === "connect" ? { runtime, workspace, consent } : action === "test" ? { model, consent: true } : {}), + }); + const result = await response.json(); + if (!response.ok || result.error) { setError(result.error ?? "runtime_failed"); return; } + if (action === "test") setTested(result.ok === true); + else { applyStatus(result as Status); setConsent(false); } + } catch { setError("runtime_failed"); } + finally { setBusy(false); } + }; + const browse = async (path?: string) => { + try { + const response = await fetch(`${apiBase}/api/zcode-desktop/folders${path ? `?path=${encodeURIComponent(path)}` : ""}`); + if (!response.ok) throw new Error(); + setFolders(await response.json()); + } catch { setError("workspace_invalid"); } + }; + const problem = error === "desktop_missing" ? t("zcodeDesktop.missing") + : error === "sandbox_missing" ? t("zcodeDesktop.sandboxMissing") + : error === "node_missing" ? t("zcodeDesktop.nodeMissing") + : error === "platform_unsupported" ? t("zcodeDesktop.platformUnsupported") + : error === "profile_missing" || error === "models_missing" ? t("zcodeDesktop.loginNeeded") + : error === "workspace_invalid" ? t("zcodeDesktop.workspaceInvalid") + : error === "inference_failed" ? t("zcodeDesktop.inferenceFailed") + : error ? t("zcodeDesktop.failed") : ""; + const changed = runtime !== status?.runtime || workspace !== status?.workspace; + return