@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream connection failures share one message shape across the three catch sites in
|
|
3
|
+
* core.ts. A TLS certificate/hostname mismatch deserves its own wording: the generic
|
|
4
|
+
* "Provider unreachable" reads as if opencodex built a wrong endpoint, which sent issue
|
|
5
|
+
* #553 looking for an adapter URL bug that does not exist. Name the likely cause and the
|
|
6
|
+
* command that settles it.
|
|
7
|
+
*/
|
|
8
|
+
export function describeUpstreamConnectFailure(err: unknown, connectMs: number): string {
|
|
9
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
10
|
+
return `Provider connect timeout after ${connectMs}ms`;
|
|
11
|
+
}
|
|
12
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
13
|
+
const code = err instanceof Error ? (err as { code?: unknown }).code : undefined;
|
|
14
|
+
// `code` is the reliable signal. The message fallback is anchored to the head because Bun
|
|
15
|
+
// renders this rejection as `ERR_TLS_CERT_ALTNAME_INVALID fetching "<url>"`; matching the
|
|
16
|
+
// bare substring anywhere would also fire on text that merely quotes the code back at us.
|
|
17
|
+
// Only transport failures reach these call sites, so that is defensive rather than load-bearing.
|
|
18
|
+
if (code === "ERR_TLS_CERT_ALTNAME_INVALID" || detail.startsWith("ERR_TLS_CERT_ALTNAME_INVALID")) {
|
|
19
|
+
const host = extractHostname(detail);
|
|
20
|
+
const target = host ?? "the provider host";
|
|
21
|
+
const probe = host ?? "<host>";
|
|
22
|
+
return `Provider TLS certificate does not match ${target}: ${redactUrlUserinfo(detail)}. `
|
|
23
|
+
+ "opencodex did not rewrite this hostname — a certificate that does not cover it normally "
|
|
24
|
+
+ "means TLS interception (corporate proxy, VPN, or local MITM tooling) or a poisoned DNS "
|
|
25
|
+
+ `answer. Check with: openssl s_client -connect ${probe}:443 -servername ${probe} `
|
|
26
|
+
+ "</dev/null | openssl x509 -noout -subject -ext subjectAltName";
|
|
27
|
+
}
|
|
28
|
+
return `Provider unreachable: ${redactUrlUserinfo(detail)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A provider base URL may carry credentials as userinfo (`https://user:token@host/`), and the
|
|
33
|
+
* runtime error echoes the URL it was fetching. Strip it before the message reaches a client
|
|
34
|
+
* or a log line.
|
|
35
|
+
*/
|
|
36
|
+
function redactUrlUserinfo(detail: string): string {
|
|
37
|
+
return detail.replace(/(https?:\/\/)[^/\s"'@]*@/g, "$1<redacted>@");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractHostname(detail: string): string | null {
|
|
41
|
+
const match = detail.match(/https?:\/\/([^/\s"']+)/);
|
|
42
|
+
if (!match?.[1]) return null;
|
|
43
|
+
try {
|
|
44
|
+
return new URL(`https://${match[1]}`).hostname || null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
2
|
+
import { relaySseWithPayloadRewrite, type SsePayloadRewrite } from "./sse-payload-rewrite";
|
|
3
|
+
|
|
4
|
+
interface NamespacedTool {
|
|
5
|
+
namespace: string;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const IMAGE_GEN_NAMESPACE = "image_gen";
|
|
10
|
+
const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
|
|
11
|
+
|
|
12
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Restrict the generic bridge map to Codex's standalone image namespace and accept the dotted
|
|
18
|
+
* alias emitted by earlier compatibility attempts. Legacy flat dotted declarations are recovered
|
|
19
|
+
* from the raw request because the generic parser does not assign them a namespace. Exact declared
|
|
20
|
+
* names avoid splitting arbitrary double-underscore function names that belong to unrelated tools.
|
|
21
|
+
*/
|
|
22
|
+
export function imageGenToolCallAliases(
|
|
23
|
+
toolNsMap: ReadonlyMap<string, NamespacedTool>,
|
|
24
|
+
requestBody?: unknown,
|
|
25
|
+
): Map<string, NamespacedTool> {
|
|
26
|
+
const aliases = new Map<string, NamespacedTool>();
|
|
27
|
+
for (const [wireName, tool] of toolNsMap) {
|
|
28
|
+
if (tool.namespace !== IMAGE_GEN_NAMESPACE) continue;
|
|
29
|
+
aliases.set(wireName, tool);
|
|
30
|
+
aliases.set(`${tool.namespace}.${tool.name}`, tool);
|
|
31
|
+
}
|
|
32
|
+
for (const group of collectResponsesToolGroups(requestBody)) {
|
|
33
|
+
for (const tool of group) {
|
|
34
|
+
if (
|
|
35
|
+
!isPlainObject(tool)
|
|
36
|
+
|| tool.type !== "function"
|
|
37
|
+
|| typeof tool.name !== "string"
|
|
38
|
+
|| !tool.name.startsWith(IMAGE_GEN_DOTTED_PREFIX)
|
|
39
|
+
|| tool.name.length === IMAGE_GEN_DOTTED_PREFIX.length
|
|
40
|
+
) continue;
|
|
41
|
+
const localName = tool.name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
|
|
42
|
+
const target = { namespace: IMAGE_GEN_NAMESPACE, name: localName };
|
|
43
|
+
aliases.set(`${IMAGE_GEN_NAMESPACE}__${localName}`, target);
|
|
44
|
+
aliases.set(tool.name, target);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return aliases;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Recursively restore only exact image-gen function-call aliases in a parsed Responses payload. */
|
|
51
|
+
function restoreImageGenCalls(
|
|
52
|
+
value: unknown,
|
|
53
|
+
aliases: ReadonlyMap<string, NamespacedTool>,
|
|
54
|
+
): { value: unknown; changed: boolean } {
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
let changed = false;
|
|
57
|
+
const restored = value.map(entry => {
|
|
58
|
+
const result = restoreImageGenCalls(entry, aliases);
|
|
59
|
+
changed ||= result.changed;
|
|
60
|
+
return result.value;
|
|
61
|
+
});
|
|
62
|
+
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainObject(value)) return { value, changed: false };
|
|
65
|
+
|
|
66
|
+
let changed = false;
|
|
67
|
+
const restored: Record<string, unknown> = {};
|
|
68
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
69
|
+
const result = restoreImageGenCalls(entry, aliases);
|
|
70
|
+
restored[key] = result.value;
|
|
71
|
+
changed ||= result.changed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const name = typeof value.name === "string" ? value.name : undefined;
|
|
75
|
+
const target = value.type === "function_call" && name ? aliases.get(name) : undefined;
|
|
76
|
+
if (target) {
|
|
77
|
+
restored.name = target.name;
|
|
78
|
+
restored.namespace = target.namespace;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Restore image-gen function calls in a non-streaming Responses JSON document. */
|
|
85
|
+
export function restoreImageGenCallsInJson(
|
|
86
|
+
text: string,
|
|
87
|
+
aliases: ReadonlyMap<string, NamespacedTool>,
|
|
88
|
+
): string {
|
|
89
|
+
if (aliases.size === 0) return text;
|
|
90
|
+
let payload: unknown;
|
|
91
|
+
try {
|
|
92
|
+
payload = JSON.parse(text);
|
|
93
|
+
} catch {
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
const restored = restoreImageGenCalls(payload, aliases);
|
|
97
|
+
return restored.changed ? JSON.stringify(restored.value) : text;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Payload rewrite for composition with other client-facing SSE transforms. */
|
|
101
|
+
export function createImageGenCallRestoreRewrite(
|
|
102
|
+
aliases: ReadonlyMap<string, NamespacedTool>,
|
|
103
|
+
): SsePayloadRewrite | undefined {
|
|
104
|
+
if (aliases.size === 0) return undefined;
|
|
105
|
+
return (payload) => restoreImageGenCallsInJson(payload, aliases);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Restore upstream-safe image-gen aliases only on the client-facing SSE branch. The inspection and
|
|
110
|
+
* continuation-cache branch must retain raw upstream names so a replay can be sent back unchanged.
|
|
111
|
+
*/
|
|
112
|
+
export function relaySseWithImageGenCallRestore(
|
|
113
|
+
body: ReadableStream<Uint8Array>,
|
|
114
|
+
aliases: ReadonlyMap<string, NamespacedTool>,
|
|
115
|
+
): ReadableStream<Uint8Array> {
|
|
116
|
+
const rewrite = createImageGenCallRestoreRewrite(aliases);
|
|
117
|
+
return rewrite ? relaySseWithPayloadRewrite(body, rewrite) : body;
|
|
118
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { ResponsesItemIdRepairConfig } from "../types";
|
|
3
|
+
import { relaySseWithPayloadRewrite, type SsePayloadRewrite } from "./sse-payload-rewrite";
|
|
3
4
|
|
|
4
5
|
type RepairableItemType = "message" | "reasoning";
|
|
5
6
|
|
|
@@ -35,44 +36,6 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
35
36
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
|
|
39
|
-
const match = buffer.match(/\r?\n\r?\n/);
|
|
40
|
-
if (!match || match.index === undefined) return null;
|
|
41
|
-
return {
|
|
42
|
-
block: buffer.slice(0, match.index),
|
|
43
|
-
delimiter: match[0],
|
|
44
|
-
rest: buffer.slice(match.index + match[0].length),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function sseDataPayload(block: string): string | null {
|
|
49
|
-
const data: string[] = [];
|
|
50
|
-
for (const line of block.split(/\r?\n/)) {
|
|
51
|
-
if (!line.startsWith("data:")) continue;
|
|
52
|
-
const value = line.slice(5);
|
|
53
|
-
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
54
|
-
}
|
|
55
|
-
return data.length > 0 ? data.join("\n") : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function replaceSseDataPayload(block: string, payload: string): string {
|
|
59
|
-
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
60
|
-
const lines = block.split(/\r?\n/);
|
|
61
|
-
const rewritten: string[] = [];
|
|
62
|
-
let replaced = false;
|
|
63
|
-
for (const line of lines) {
|
|
64
|
-
if (!line.startsWith("data:")) {
|
|
65
|
-
rewritten.push(line);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (!replaced) {
|
|
69
|
-
rewritten.push(`data: ${payload}`);
|
|
70
|
-
replaced = true;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return replaced ? rewritten.join(newline) : block;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
39
|
function asOutputIndex(value: unknown): number | null {
|
|
77
40
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
78
41
|
}
|
|
@@ -221,57 +184,19 @@ function repairEventPayload(
|
|
|
221
184
|
* type에 재사용해도 function_call id/call_id는 보존된다. opt-in 게이트웨이는 sequential streams에서도
|
|
222
185
|
* 고유한 canonical id를 얻지만, 보정이 필요한 경우에만 JS stream 재작성 비용을 지불한다.
|
|
223
186
|
*/
|
|
187
|
+
/** Stateful payload rewrite for composition with other client-facing SSE transforms. */
|
|
188
|
+
export function createResponsesItemIdPayloadRewrite(
|
|
189
|
+
config: ResponsesItemIdRepairConfig,
|
|
190
|
+
): SsePayloadRewrite {
|
|
191
|
+
const state = createRepairState(config);
|
|
192
|
+
return (payload) => repairEventPayload(payload, state);
|
|
193
|
+
}
|
|
194
|
+
|
|
224
195
|
export function relaySseWithResponsesItemIdRepair(
|
|
225
196
|
body: ReadableStream<Uint8Array>,
|
|
226
197
|
config: ResponsesItemIdRepairConfig,
|
|
227
198
|
): ReadableStream<Uint8Array> {
|
|
228
|
-
|
|
229
|
-
const decoder = new TextDecoder();
|
|
230
|
-
const encoder = new TextEncoder();
|
|
231
|
-
const state = createRepairState(config);
|
|
232
|
-
let buffer = "";
|
|
233
|
-
|
|
234
|
-
const emitProcessedBlocks = (
|
|
235
|
-
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
236
|
-
flushFinal = false,
|
|
237
|
-
): void => {
|
|
238
|
-
let next: { block: string; delimiter: string; rest: string } | null;
|
|
239
|
-
while ((next = nextSseBlock(buffer))) {
|
|
240
|
-
buffer = next.rest;
|
|
241
|
-
const payload = sseDataPayload(next.block);
|
|
242
|
-
const repairedPayload = payload ? repairEventPayload(payload, state) : undefined;
|
|
243
|
-
const block = payload && repairedPayload !== undefined && repairedPayload !== payload
|
|
244
|
-
? replaceSseDataPayload(next.block, repairedPayload)
|
|
245
|
-
: next.block;
|
|
246
|
-
controller.enqueue(encoder.encode(block + next.delimiter));
|
|
247
|
-
}
|
|
248
|
-
if (flushFinal && buffer.length > 0) {
|
|
249
|
-
const payload = sseDataPayload(buffer);
|
|
250
|
-
const repairedPayload = payload ? repairEventPayload(payload, state) : undefined;
|
|
251
|
-
const block = payload && repairedPayload !== undefined && repairedPayload !== payload
|
|
252
|
-
? replaceSseDataPayload(buffer, repairedPayload)
|
|
253
|
-
: buffer;
|
|
254
|
-
controller.enqueue(encoder.encode(block));
|
|
255
|
-
buffer = "";
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
return new ReadableStream<Uint8Array>({
|
|
260
|
-
async pull(controller) {
|
|
261
|
-
const { done, value } = await reader.read();
|
|
262
|
-
if (done) {
|
|
263
|
-
buffer += decoder.decode();
|
|
264
|
-
emitProcessedBlocks(controller, true);
|
|
265
|
-
controller.close();
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
buffer += decoder.decode(value, { stream: true });
|
|
269
|
-
emitProcessedBlocks(controller);
|
|
270
|
-
},
|
|
271
|
-
cancel(reason) {
|
|
272
|
-
reader.cancel(reason).catch(() => {});
|
|
273
|
-
},
|
|
274
|
-
});
|
|
199
|
+
return relaySseWithPayloadRewrite(body, createResponsesItemIdPayloadRewrite(config));
|
|
275
200
|
}
|
|
276
201
|
|
|
277
202
|
export function hasResponsesItemIdRepair(config: ResponsesItemIdRepairConfig | undefined): boolean {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client-facing SSE payload rewrite shell.
|
|
3
|
+
*
|
|
4
|
+
* Multiple opt-in transforms (image-gen namespace restore, item-id repair, …) compose into one
|
|
5
|
+
* parse/stringify pass so a tee'd stream is not re-framed twice per event.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type SsePayloadRewrite = (payload: string) => string;
|
|
9
|
+
|
|
10
|
+
/** Split one complete SSE event block while retaining its original blank-line delimiter. */
|
|
11
|
+
export function nextSseBlock(buffer: string): { block: string; delimiter: string; rest: string } | null {
|
|
12
|
+
const match = buffer.match(/\r?\n\r?\n/);
|
|
13
|
+
if (!match || match.index === undefined) return null;
|
|
14
|
+
return {
|
|
15
|
+
block: buffer.slice(0, match.index),
|
|
16
|
+
delimiter: match[0],
|
|
17
|
+
rest: buffer.slice(match.index + match[0].length),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Join all data lines from one SSE event according to the event-stream field rules. */
|
|
22
|
+
export function sseDataPayload(block: string): string | null {
|
|
23
|
+
const data: string[] = [];
|
|
24
|
+
for (const line of block.split(/\r?\n/)) {
|
|
25
|
+
if (!line.startsWith("data:")) continue;
|
|
26
|
+
const value = line.slice(5);
|
|
27
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
28
|
+
}
|
|
29
|
+
return data.length > 0 ? data.join("\n") : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Replace an SSE event's data field while preserving non-data fields and newline style. */
|
|
33
|
+
export function replaceSseDataPayload(block: string, payload: string): string {
|
|
34
|
+
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
35
|
+
const lines = block.split(/\r?\n/);
|
|
36
|
+
const rewritten: string[] = [];
|
|
37
|
+
let replaced = false;
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (!line.startsWith("data:")) {
|
|
40
|
+
rewritten.push(line);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!replaced) {
|
|
44
|
+
rewritten.push(`data: ${payload}`);
|
|
45
|
+
replaced = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return replaced ? rewritten.join(newline) : block;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Apply rewrites left-to-right; empty list is identity. */
|
|
52
|
+
export function composeSsePayloadRewrites(...rewrites: SsePayloadRewrite[]): SsePayloadRewrite {
|
|
53
|
+
if (rewrites.length === 0) return (payload) => payload;
|
|
54
|
+
if (rewrites.length === 1) return rewrites[0]!;
|
|
55
|
+
return (payload) => {
|
|
56
|
+
let next = payload;
|
|
57
|
+
for (const rewrite of rewrites) next = rewrite(next);
|
|
58
|
+
return next;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Relay an SSE body through a single JS pull wrapper, rewriting each event's data payload in place.
|
|
64
|
+
* Non-data fields and framing are preserved; invalid JSON payloads are left to the rewrite callback.
|
|
65
|
+
*/
|
|
66
|
+
export function relaySseWithPayloadRewrite(
|
|
67
|
+
body: ReadableStream<Uint8Array>,
|
|
68
|
+
rewrite: SsePayloadRewrite,
|
|
69
|
+
): ReadableStream<Uint8Array> {
|
|
70
|
+
const reader = body.getReader();
|
|
71
|
+
const decoder = new TextDecoder();
|
|
72
|
+
const encoder = new TextEncoder();
|
|
73
|
+
let buffer = "";
|
|
74
|
+
|
|
75
|
+
const emitProcessedBlocks = (
|
|
76
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
77
|
+
flushFinal = false,
|
|
78
|
+
): void => {
|
|
79
|
+
let next: { block: string; delimiter: string; rest: string } | null;
|
|
80
|
+
while ((next = nextSseBlock(buffer))) {
|
|
81
|
+
buffer = next.rest;
|
|
82
|
+
const payload = sseDataPayload(next.block);
|
|
83
|
+
const rewrittenPayload = payload ? rewrite(payload) : undefined;
|
|
84
|
+
const block = payload && rewrittenPayload !== undefined && rewrittenPayload !== payload
|
|
85
|
+
? replaceSseDataPayload(next.block, rewrittenPayload)
|
|
86
|
+
: next.block;
|
|
87
|
+
controller.enqueue(encoder.encode(block + next.delimiter));
|
|
88
|
+
}
|
|
89
|
+
if (flushFinal && buffer.length > 0) {
|
|
90
|
+
const payload = sseDataPayload(buffer);
|
|
91
|
+
const rewrittenPayload = payload ? rewrite(payload) : undefined;
|
|
92
|
+
const block = payload && rewrittenPayload !== undefined && rewrittenPayload !== payload
|
|
93
|
+
? replaceSseDataPayload(buffer, rewrittenPayload)
|
|
94
|
+
: buffer;
|
|
95
|
+
controller.enqueue(encoder.encode(block));
|
|
96
|
+
buffer = "";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return new ReadableStream<Uint8Array>({
|
|
101
|
+
async pull(controller) {
|
|
102
|
+
const { done, value } = await reader.read();
|
|
103
|
+
if (done) {
|
|
104
|
+
buffer += decoder.decode();
|
|
105
|
+
emitProcessedBlocks(controller, true);
|
|
106
|
+
controller.close();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
buffer += decoder.decode(value, { stream: true });
|
|
110
|
+
emitProcessedBlocks(controller);
|
|
111
|
+
},
|
|
112
|
+
cancel(reason) {
|
|
113
|
+
reader.cancel(reason).catch(() => {});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -106,10 +106,14 @@ export function resetStartupInstallStateForTests(): void {
|
|
|
106
106
|
installState = { status: "idle" };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export function startupInstallArgv(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
export function startupInstallArgv(
|
|
110
|
+
action: StartupInstallAction,
|
|
111
|
+
options?: { repair?: boolean },
|
|
112
|
+
): string[] {
|
|
113
|
+
if (action === "install-service") {
|
|
114
|
+
return options?.repair ? ["service", "repair"] : ["service", "install"];
|
|
115
|
+
}
|
|
116
|
+
return ["codex-shim", "install"];
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
export interface CliInstallFailure {
|
|
@@ -152,10 +156,13 @@ export function installFailureDetail(stdout: string, stderr: string, error: Erro
|
|
|
152
156
|
return classifyCliInstallFailure(stdout, stderr, error).detail;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
function runCliInstall(
|
|
159
|
+
function runCliInstall(
|
|
160
|
+
action: StartupInstallAction,
|
|
161
|
+
options?: { repair?: boolean },
|
|
162
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
156
163
|
const bun = durableBunPath();
|
|
157
164
|
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
158
|
-
const argv = [cli, ...startupInstallArgv(action)];
|
|
165
|
+
const argv = [cli, ...startupInstallArgv(action, options)];
|
|
159
166
|
return new Promise((resolve, reject) => {
|
|
160
167
|
execFile(bun, argv, {
|
|
161
168
|
encoding: "utf8",
|
|
@@ -219,29 +226,37 @@ function applyReconciliationOutcome(
|
|
|
219
226
|
/**
|
|
220
227
|
* Execute the existing fixed CLI installer outside the proxy event loop.
|
|
221
228
|
*
|
|
229
|
+
* Repair mode (`options.repair`) runs `ocx service repair` — asset rewrite + restart
|
|
230
|
+
* without Task Scheduler re-registration, so it must not enter the UAC elevation path.
|
|
231
|
+
*
|
|
222
232
|
* After an elevation request timeout the lock becomes `indeterminate` until the
|
|
223
233
|
* original elevated transaction completes and is reconciled. A process restart
|
|
224
234
|
* clears this in-memory lock — callers must then inspect Task Scheduler reality
|
|
225
235
|
* (see evaluateSchedulerInstallRestartReconciliation) before installing again.
|
|
226
236
|
*/
|
|
227
|
-
export function runStartupInstallAction(
|
|
237
|
+
export function runStartupInstallAction(
|
|
238
|
+
action: StartupInstallAction,
|
|
239
|
+
options?: { repair?: boolean },
|
|
240
|
+
): Promise<{ message: string }> {
|
|
228
241
|
const busy = rejectIfBusy(action);
|
|
229
242
|
if (busy) return Promise.reject(busy);
|
|
230
243
|
|
|
244
|
+
const repair = options?.repair === true;
|
|
231
245
|
const attemptId = randomUUID();
|
|
232
246
|
const startedAt = Date.now();
|
|
233
247
|
installState = { status: "running", action, attemptId, startedAt };
|
|
234
248
|
|
|
235
249
|
const operation = (async () => {
|
|
236
250
|
try {
|
|
237
|
-
await runCliInstall(action);
|
|
251
|
+
await runCliInstall(action, { repair });
|
|
238
252
|
} catch (error) {
|
|
239
253
|
const code = installFailureCode(error);
|
|
240
254
|
const detail = error instanceof Error ? error.message : String(error);
|
|
241
|
-
// Elevate only for
|
|
242
|
-
// WinSW removal, asset writes, or generic permission errors.
|
|
255
|
+
// Elevate only for fresh install + structured Task Scheduler /create access denial —
|
|
256
|
+
// never for repair, WinSW removal, asset writes, or generic permission errors.
|
|
243
257
|
if (
|
|
244
|
-
|
|
258
|
+
!repair
|
|
259
|
+
&& action === "install-service"
|
|
245
260
|
&& process.platform === "win32"
|
|
246
261
|
&& (code === WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER
|
|
247
262
|
|| isWindowsSchtasksCreateAccessDenied(detail))
|
|
@@ -276,10 +291,11 @@ export function runStartupInstallAction(action: StartupInstallAction): Promise<{
|
|
|
276
291
|
throw error;
|
|
277
292
|
}
|
|
278
293
|
}
|
|
294
|
+
if (action === "install-service") {
|
|
295
|
+
return { message: repair ? "Background service repaired." : "Background service installed." };
|
|
296
|
+
}
|
|
279
297
|
return {
|
|
280
|
-
message:
|
|
281
|
-
? "Background service installed."
|
|
282
|
-
: "Codex launcher shim installed.",
|
|
298
|
+
message: repair ? "Codex launcher shim repaired." : "Codex launcher shim installed.",
|
|
283
299
|
};
|
|
284
300
|
})();
|
|
285
301
|
|
package/src/server/system-env.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
@@ -6,6 +6,7 @@ import { resolveAutoContext, type AutoContextMode } from "../claude/context-wind
|
|
|
6
6
|
import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens } from "../claude/auth-detect";
|
|
7
7
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
8
8
|
import type { OcxConfig } from "../types";
|
|
9
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Does the opencodex dummy marker belong in the system environment?
|
|
@@ -72,8 +73,10 @@ function writeShellEnvFile(port: number, config: OcxConfig, modelEnv: Record<str
|
|
|
72
73
|
if (config.claudeCode?.alwaysEnableEffort === true) {
|
|
73
74
|
lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
|
|
74
75
|
}
|
|
76
|
+
const shellEnvPath = getShellEnvFilePath();
|
|
77
|
+
recordOwnedConfigPath(getConfigDir(), shellEnvPath);
|
|
75
78
|
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
76
|
-
writeFileSync(
|
|
79
|
+
writeFileSync(shellEnvPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
function removeShellEnvFile(): void {
|
|
@@ -128,6 +131,19 @@ const SYSTEM_ENV_NAMES = [
|
|
|
128
131
|
"ANTHROPIC_AUTH_TOKEN",
|
|
129
132
|
] as const;
|
|
130
133
|
|
|
134
|
+
const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
|
|
135
|
+
...SYSTEM_ENV_NAMES,
|
|
136
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
137
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
138
|
+
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
139
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
140
|
+
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
141
|
+
"CLAUDE_CODE_MAX_CONTEXT_TOKENS",
|
|
142
|
+
"DISABLE_COMPACT",
|
|
143
|
+
"CLAUDE_CODE_AUTO_COMPACT_WINDOW",
|
|
144
|
+
"CLAUDE_CODE_ALWAYS_ENABLE_EFFORT",
|
|
145
|
+
]);
|
|
146
|
+
|
|
131
147
|
interface SystemEnvTracking {
|
|
132
148
|
pid: number;
|
|
133
149
|
port: number;
|
|
@@ -147,7 +163,7 @@ export function getSystemEnvTrackingPath(): string {
|
|
|
147
163
|
export function launchctlGetenv(name: string): string | undefined {
|
|
148
164
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) return undefined;
|
|
149
165
|
try {
|
|
150
|
-
const value =
|
|
166
|
+
const value = execFileSync("/bin/launchctl", ["getenv", name], { encoding: "utf8" }).trim();
|
|
151
167
|
return value || undefined;
|
|
152
168
|
} catch {
|
|
153
169
|
return undefined;
|
|
@@ -160,22 +176,23 @@ function readTracking(): SystemEnvTracking | undefined {
|
|
|
160
176
|
if (!Number.isInteger(tracking.port) || typeof tracking.pid !== "number" || typeof tracking.injectedAt !== "string") {
|
|
161
177
|
return undefined;
|
|
162
178
|
}
|
|
163
|
-
|
|
179
|
+
const injectedKeys = Array.isArray(tracking.injectedKeys)
|
|
180
|
+
? [...new Set(tracking.injectedKeys.filter(
|
|
181
|
+
(name): name is string => typeof name === "string" && MANAGED_SYSTEM_ENV_NAMES.has(name),
|
|
182
|
+
))]
|
|
183
|
+
: undefined;
|
|
184
|
+
return { ...tracking, injectedKeys } as SystemEnvTracking;
|
|
164
185
|
} catch {
|
|
165
186
|
return undefined;
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
189
|
|
|
169
|
-
function shellArg(value: string): string {
|
|
170
|
-
return /^[A-Za-z0-9_./:-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
190
|
function setLaunchctlEnv(name: string, value: string): void {
|
|
174
|
-
|
|
191
|
+
execFileSync("/bin/launchctl", ["setenv", name, value]);
|
|
175
192
|
}
|
|
176
193
|
|
|
177
194
|
function unsetLaunchctlEnv(name: string): void {
|
|
178
|
-
|
|
195
|
+
execFileSync("/bin/launchctl", ["unsetenv", name]);
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
function ownedBaseUrl(port: number): string {
|
|
@@ -183,6 +200,7 @@ function ownedBaseUrl(port: number): string {
|
|
|
183
200
|
}
|
|
184
201
|
|
|
185
202
|
function writeTracking(port: number, injectedKeys: string[]): void {
|
|
203
|
+
recordOwnedConfigPath(getConfigDir(), getSystemEnvTrackingPath());
|
|
186
204
|
mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
187
205
|
writeFileSync(getSystemEnvTrackingPath(), JSON.stringify({
|
|
188
206
|
pid: process.pid,
|