@bitkyc08/opencodex 2.75.0 → 2.76.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 +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { join, posix } from "node:path";
|
|
4
|
+
import { getConfigDir } from "../config";
|
|
5
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
6
|
+
import {
|
|
7
|
+
chargeImageBudget, createImageBudget, decodeValidatedImageBase64,
|
|
8
|
+
artifactHttpUrl, getArtifactsDir, MAX_ENCODED_BYTES_PER_IMAGE, pruneArtifacts, sniffImageExtension,
|
|
9
|
+
} from "../images/artifacts";
|
|
10
|
+
import { sseDataPayload, replaceSseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
11
|
+
|
|
12
|
+
type Row = Record<string, any>;
|
|
13
|
+
const object = (v: unknown): v is Row => !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
const originators = new Set(["codex_cli_rs", "Codex Desktop", "codex_app", "codex_work_desktop"]);
|
|
15
|
+
const generatedName = /^img-codex-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\.(png|jpg|webp|gif)$/;
|
|
16
|
+
|
|
17
|
+
/** One spelling per local path: file URL or plain, either separator, dot segments, folded case. */
|
|
18
|
+
function comparablePath(value: string, foldCase: boolean): string | undefined {
|
|
19
|
+
let path = value;
|
|
20
|
+
if (/^file:\/\//i.test(path)) {
|
|
21
|
+
try { path = decodeURIComponent(path.replace(/^file:\/\/(?:localhost)?/i, "")); } catch { return undefined; }
|
|
22
|
+
if (/^\/[A-Za-z]:/.test(path)) path = path.slice(1);
|
|
23
|
+
}
|
|
24
|
+
path = posix.normalize(path.replace(/\\/g, "/"));
|
|
25
|
+
return foldCase ? path.toLowerCase() : path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Full client replay can include display-only messages even without their generated ids.
|
|
30
|
+
* Shared by Responses request preparation and the remote compaction handler.
|
|
31
|
+
*/
|
|
32
|
+
export function redactHostedImageDisplayPaths(body: unknown, platform: NodeJS.Platform = process.platform): void {
|
|
33
|
+
if (!object(body) || !Array.isArray(body.input)) return;
|
|
34
|
+
// macOS and Windows filesystems fold case by default, so either spelling names the artifact.
|
|
35
|
+
const foldCase = platform === "darwin" || platform === "win32";
|
|
36
|
+
const dir = comparablePath(getArtifactsDir(), foldCase);
|
|
37
|
+
const generatedLink = /!\[Generated image\]\(<([^>\r\n]+)>\)/g;
|
|
38
|
+
const redact = (text: string) => text.replace(generatedLink, (link, path: string) => {
|
|
39
|
+
const candidate = comparablePath(path, foldCase);
|
|
40
|
+
const slash = candidate?.lastIndexOf("/") ?? -1;
|
|
41
|
+
if (!candidate || !dir || slash < 0 || candidate.slice(0, slash) !== dir) return link;
|
|
42
|
+
const name = candidate.slice(slash + 1);
|
|
43
|
+
if (!generatedName.test(name)) return link;
|
|
44
|
+
// No file read or existence check: history may outlive artifact retention.
|
|
45
|
+
return " + ">)";
|
|
46
|
+
});
|
|
47
|
+
for (const item of body.input) {
|
|
48
|
+
if (!object(item) || item.role !== "assistant"
|
|
49
|
+
|| (item.type !== undefined && item.type !== "message")) continue;
|
|
50
|
+
if (typeof item.content === "string") item.content = redact(item.content);
|
|
51
|
+
else if (Array.isArray(item.content)) {
|
|
52
|
+
for (const part of item.content) {
|
|
53
|
+
if (object(part) && ["output_text", "input_text", "text"].includes(part.type)
|
|
54
|
+
&& typeof part.text === "string") part.text = redact(part.text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Local filesystem links are only appropriate for the loopback Codex client. */
|
|
61
|
+
export function isLocalCodexImageClient(headers: Headers, admissionKind?: string, inboundWire?: string): boolean {
|
|
62
|
+
return admissionKind === "loopback"
|
|
63
|
+
&& (inboundWire === undefined || inboundWire === "responses")
|
|
64
|
+
&& (originators.has(headers.get("originator") ?? "")
|
|
65
|
+
|| /^codex(?:[_ /-]|$)/i.test(headers.get("user-agent") ?? ""));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Client branch only: raw hosted items remain intact in the upstream replay cache. */
|
|
69
|
+
export function createHostedImageDisplayRewrite(): SseBlockRewrite & { json(text: string): string } {
|
|
70
|
+
const budget = createImageBudget();
|
|
71
|
+
const states = new Map<string, { message: Row; added: boolean; done: boolean }>();
|
|
72
|
+
let extraSequence = 0;
|
|
73
|
+
let wroteArtifact = false;
|
|
74
|
+
|
|
75
|
+
function stateFor(item: Row, index: number) {
|
|
76
|
+
const key = typeof item.id === "string" ? item.id : "index:" + index;
|
|
77
|
+
let state = states.get(key);
|
|
78
|
+
if (state) return state;
|
|
79
|
+
// Bound metadata independently of the existing 100 MiB decoded-image budget.
|
|
80
|
+
if (states.size >= 128) throw new RangeError("hosted image result count exceeds local display limit");
|
|
81
|
+
const id = "msg_ocx_img_" + createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
82
|
+
state = { added: false, done: false, message: {
|
|
83
|
+
// Persist image results outside the client's collapsible progress messages.
|
|
84
|
+
id, type: "message", role: "assistant", phase: "final_answer", status: "in_progress", content: [],
|
|
85
|
+
...(item.internal_chat_message_metadata_passthrough === undefined ? {} : {
|
|
86
|
+
internal_chat_message_metadata_passthrough: item.internal_chat_message_metadata_passthrough,
|
|
87
|
+
}),
|
|
88
|
+
} };
|
|
89
|
+
states.set(key, state);
|
|
90
|
+
return state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function complete(item: Row, index: number) {
|
|
94
|
+
const state = stateFor(item, index);
|
|
95
|
+
if (state.message.status !== "in_progress") return state;
|
|
96
|
+
let text = item.status === "completed"
|
|
97
|
+
? "The completed image result has no supported image data for local display."
|
|
98
|
+
: "Image generation did not complete; no image is available to display.";
|
|
99
|
+
let status = "incomplete";
|
|
100
|
+
if (item.status === "completed" && typeof item.result === "string") {
|
|
101
|
+
try {
|
|
102
|
+
if (item.result.length > MAX_ENCODED_BYTES_PER_IMAGE) throw new RangeError("image too large");
|
|
103
|
+
const bytes = decodeValidatedImageBase64(item.result);
|
|
104
|
+
chargeImageBudget(budget, bytes.length);
|
|
105
|
+
const dir = getArtifactsDir();
|
|
106
|
+
recordOwnedConfigPath(getConfigDir(), dir);
|
|
107
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
108
|
+
const candidate = join(dir, "img-codex-" + randomUUID() + "." + sniffImageExtension(bytes));
|
|
109
|
+
// Bounded synchronous write: block rewriters cannot await. Never use upstream ids as paths.
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(candidate, bytes, { flag: "wx", mode: 0o600 });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
114
|
+
try { unlinkSync(candidate); } catch { /* best-effort partial-write cleanup */ }
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
wroteArtifact = true;
|
|
119
|
+
text = "";
|
|
120
|
+
status = "completed";
|
|
121
|
+
} catch {
|
|
122
|
+
// Do not expose image data, filesystem errors, or credentials in a generated message.
|
|
123
|
+
text = "The image result could not be saved for local display.";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
state.message = { ...state.message, status, content: [{ type: "output_text", text, annotations: [] }] };
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function doneEvents(item: Row, index: number): Row[] {
|
|
131
|
+
const state = complete(item, index);
|
|
132
|
+
if (state.done) return [];
|
|
133
|
+
const { message } = state;
|
|
134
|
+
const common = { item_id: message.id, output_index: index, content_index: 0 };
|
|
135
|
+
const part = message.content[0];
|
|
136
|
+
const events: Row[] = [];
|
|
137
|
+
if (!state.added) events.push({ type: "response.output_item.added", output_index: index,
|
|
138
|
+
item: { ...message, status: "in_progress", content: [] } });
|
|
139
|
+
events.push(
|
|
140
|
+
{ type: "response.content_part.added", ...common, part: { ...part, text: "" } },
|
|
141
|
+
{ type: "response.output_text.delta", ...common, delta: part.text },
|
|
142
|
+
{ type: "response.output_text.done", ...common, text: part.text },
|
|
143
|
+
{ type: "response.content_part.done", ...common, part },
|
|
144
|
+
{ type: "response.output_item.done", output_index: index, item: message },
|
|
145
|
+
);
|
|
146
|
+
state.added = state.done = true;
|
|
147
|
+
return events;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function snapshot(response: Row, terminal: boolean, events?: Row[]): Row {
|
|
151
|
+
if (!Array.isArray(response.output)) return response;
|
|
152
|
+
// Reject oversized snapshots before saving artifacts that cannot be delivered.
|
|
153
|
+
const keys = new Set(states.keys());
|
|
154
|
+
for (const [index, item] of response.output.entries()) {
|
|
155
|
+
if (!object(item) || item.type !== "image_generation_call") continue;
|
|
156
|
+
keys.add(typeof item.id === "string" ? item.id : "index:" + index);
|
|
157
|
+
if (keys.size > 128) throw new RangeError("hosted image result count exceeds local display limit");
|
|
158
|
+
}
|
|
159
|
+
let changed = false;
|
|
160
|
+
const output = response.output.map((item: unknown, index: number) => {
|
|
161
|
+
if (!object(item) || item.type !== "image_generation_call") return item;
|
|
162
|
+
changed = true;
|
|
163
|
+
if (terminal) {
|
|
164
|
+
if (events) events.push(...doneEvents(item, index));
|
|
165
|
+
return complete(item, index).message;
|
|
166
|
+
}
|
|
167
|
+
return stateFor(item, index).message;
|
|
168
|
+
});
|
|
169
|
+
return changed ? { ...response, output } : response;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const rewrite = ((block: string): readonly string[] => {
|
|
173
|
+
const payload = sseDataPayload(block);
|
|
174
|
+
if (!payload || payload === "[DONE]") return [block];
|
|
175
|
+
let event: Row;
|
|
176
|
+
try { event = JSON.parse(payload); } catch { return [block]; }
|
|
177
|
+
if (!object(event)) return [block];
|
|
178
|
+
let result: Row[] = [event];
|
|
179
|
+
const hasIndex = Number.isSafeInteger(event.output_index) && event.output_index >= 0;
|
|
180
|
+
const index = hasIndex ? event.output_index : 0;
|
|
181
|
+
if (object(event.item) && event.item.type === "image_generation_call"
|
|
182
|
+
&& (hasIndex || typeof event.item.id === "string")) {
|
|
183
|
+
if (event.type === "response.output_item.added") {
|
|
184
|
+
const state = stateFor(event.item, index);
|
|
185
|
+
result = state.added ? [] : [{ ...event, item: state.message }];
|
|
186
|
+
state.added = true;
|
|
187
|
+
} else if (event.type === "response.output_item.done") {
|
|
188
|
+
result = doneEvents(event.item, index);
|
|
189
|
+
}
|
|
190
|
+
} else if (typeof event.type === "string" && event.type.startsWith("response.image_generation_call.")) {
|
|
191
|
+
// Hosted progress/partial-image frames refer to the replaced item; never expose mismatched ids.
|
|
192
|
+
result = [];
|
|
193
|
+
} else if (object(event.response)) {
|
|
194
|
+
const terminal = ["response.completed", "response.incomplete", "response.failed"].includes(event.type);
|
|
195
|
+
const injected: Row[] = [];
|
|
196
|
+
const response = snapshot(event.response, terminal, injected);
|
|
197
|
+
if (response !== event.response) result = [...injected, { ...event, response }];
|
|
198
|
+
}
|
|
199
|
+
const sequence = Number.isSafeInteger(event.sequence_number) ? event.sequence_number + extraSequence : undefined;
|
|
200
|
+
const shift = extraSequence;
|
|
201
|
+
extraSequence += result.length - 1;
|
|
202
|
+
return result.map((value, offset) => {
|
|
203
|
+
if (value === event && shift === 0 && result.length === 1) return block;
|
|
204
|
+
const next = sequence === undefined ? value : { ...value, sequence_number: sequence + offset };
|
|
205
|
+
const data = JSON.stringify(next);
|
|
206
|
+
return value.type === event.type ? replaceSseDataPayload(block, data) : "event: " + value.type + "\ndata: " + data;
|
|
207
|
+
});
|
|
208
|
+
}) as SseBlockRewrite & { json(text: string): string };
|
|
209
|
+
|
|
210
|
+
rewrite.json = (text: string) => {
|
|
211
|
+
let response: unknown;
|
|
212
|
+
try { response = JSON.parse(text); } catch { return text; }
|
|
213
|
+
if (!object(response)) return text;
|
|
214
|
+
const next = snapshot(response, true);
|
|
215
|
+
return next === response ? text : JSON.stringify(next);
|
|
216
|
+
};
|
|
217
|
+
rewrite.dispose = () => {
|
|
218
|
+
states.clear();
|
|
219
|
+
if (wroteArtifact) {
|
|
220
|
+
wroteArtifact = false;
|
|
221
|
+
pruneArtifacts();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
return rewrite;
|
|
225
|
+
}
|
|
@@ -26,6 +26,13 @@ import {
|
|
|
26
26
|
import type { ServiceInstallState } from "./state";
|
|
27
27
|
|
|
28
28
|
const VERSION_PROBE_TIMEOUT_MS = 5000;
|
|
29
|
+
/**
|
|
30
|
+
* A `--version` spawn is the only evidence for a PATH shim; a transient timeout or a
|
|
31
|
+
* busy cold start turns an unchanged manager into `unknown`, which flips both the
|
|
32
|
+
* takeover answer and the compatibility token the next resolve compares. One bounded
|
|
33
|
+
* retry keeps the observation stable while remaining fail-closed on a real failure.
|
|
34
|
+
*/
|
|
35
|
+
const VERSION_PROBE_ATTEMPTS = 2;
|
|
29
36
|
|
|
30
37
|
export interface ManagingCliDeps {
|
|
31
38
|
/** Environment to scan PATH/PATHEXT in. Defaults to `process.env`. */
|
|
@@ -52,12 +59,12 @@ function versionFromOutput(stdout: string): string | null {
|
|
|
52
59
|
return null;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
function
|
|
62
|
+
function probeVersionOnce(
|
|
56
63
|
executable: string,
|
|
57
64
|
args: readonly string[],
|
|
58
65
|
deps: Required<Pick<ManagingCliDeps, "spawn" | "platform" | "env">>,
|
|
66
|
+
identity: string,
|
|
59
67
|
): ManagingCliObservation {
|
|
60
|
-
const identity = [executable, ...args].join(" ");
|
|
61
68
|
let result: SpawnSyncReturns<string>;
|
|
62
69
|
try {
|
|
63
70
|
const windowsShim =
|
|
@@ -65,10 +72,17 @@ function probeVersion(
|
|
|
65
72
|
if (windowsShim && [executable, ...args].some(part => /[&|<>^%!"()]/.test(part))) {
|
|
66
73
|
return { status: "unknown", reason: "the selected Windows command shim invocation cannot be probed safely" };
|
|
67
74
|
}
|
|
75
|
+
// cmd /c re-parses the remainder of its command line: with more than one quoted
|
|
76
|
+
// part it strips the outer quotes and truncates at the first space, so every part
|
|
77
|
+
// must be quoted individually inside one wrapping pair, passed verbatim.
|
|
78
|
+
const shimCommand = `"${[executable, ...args, "--version"].map(part => `"${part}"`).join(" ")}"`;
|
|
68
79
|
result = deps.spawn(
|
|
69
80
|
windowsShim ? (deps.env?.ComSpec ?? "cmd.exe") : executable,
|
|
70
|
-
windowsShim ? ["/c",
|
|
71
|
-
{
|
|
81
|
+
windowsShim ? ["/c", shimCommand] : [...args, "--version"],
|
|
82
|
+
{
|
|
83
|
+
timeout: VERSION_PROBE_TIMEOUT_MS, encoding: "utf8", stdio: "pipe",
|
|
84
|
+
windowsHide: true, ...(windowsShim ? { windowsVerbatimArguments: true } : {}),
|
|
85
|
+
},
|
|
72
86
|
) as SpawnSyncReturns<string>;
|
|
73
87
|
} catch (error) {
|
|
74
88
|
return {
|
|
@@ -91,6 +105,22 @@ function probeVersion(
|
|
|
91
105
|
: { status: "observed", version, identity };
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
function probeVersion(
|
|
109
|
+
executable: string,
|
|
110
|
+
args: readonly string[],
|
|
111
|
+
deps: Required<Pick<ManagingCliDeps, "spawn" | "platform" | "env">>,
|
|
112
|
+
): ManagingCliObservation {
|
|
113
|
+
const identity = [executable, ...args].join(" ");
|
|
114
|
+
let observation = probeVersionOnce(executable, args, deps, identity);
|
|
115
|
+
// One bounded retry, Windows only (the platform the flake family was verified on):
|
|
116
|
+
// a single shim-timeout answer must not flip the fingerprint of an unchanged CLI.
|
|
117
|
+
const attempts = deps.platform === "win32" ? VERSION_PROBE_ATTEMPTS : 1;
|
|
118
|
+
for (let attempt = 1; observation.status !== "observed" && attempt < attempts; attempt += 1) {
|
|
119
|
+
observation = probeVersionOnce(executable, args, deps, identity);
|
|
120
|
+
}
|
|
121
|
+
return observation;
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
/**
|
|
95
125
|
* Where `ocx` resolves on PATH without spawning `where`/`which`.
|
|
96
126
|
*
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
type SystemdHomes = { codexHome: string | null; opencodexHome: string | null };
|
|
2
|
+
type SystemdHomeParse = { kind: "parsed"; homes: SystemdHomes } | { kind: "invalid" };
|
|
3
|
+
|
|
4
|
+
/** Inverse of systemd.ts's generated quoted values, without expanding systemd specifiers. */
|
|
5
|
+
function decodeQuoted(value: string): string | undefined {
|
|
6
|
+
let decoded = "";
|
|
7
|
+
for (let index = 0; index < value.length; index++) {
|
|
8
|
+
const char = value[index]!;
|
|
9
|
+
if (char === "%") {
|
|
10
|
+
if (value[++index] !== "%") return undefined;
|
|
11
|
+
decoded += "%";
|
|
12
|
+
} else if (char === "\\") {
|
|
13
|
+
const escaped = value[++index];
|
|
14
|
+
if (escaped === "n") decoded += "\n";
|
|
15
|
+
else if (escaped === "\\" || escaped === '"') decoded += escaped;
|
|
16
|
+
else return undefined;
|
|
17
|
+
} else if (char === '"' || char === "\0") {
|
|
18
|
+
return undefined;
|
|
19
|
+
} else {
|
|
20
|
+
decoded += char;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return decoded;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Missing homes are null; unsupported or ambiguous assignments invalidate the entire definition.
|
|
28
|
+
*
|
|
29
|
+
* A directive name is everything before the first `=`, stripped — systemd matches that
|
|
30
|
+
* name literally and case-sensitively, so only the exact `Environment` can carry the
|
|
31
|
+
* home assignments this parser decodes. Every other name must be a plain
|
|
32
|
+
* non-environment identifier to be skipped: `EnvironmentFile=`, `PassEnvironment=` and
|
|
33
|
+
* `UnsetEnvironment=` mutate the applied environment the same way, an escaped or
|
|
34
|
+
* malformed name cannot be proven inert, and a line ending in an odd number of
|
|
35
|
+
* backslashes continues into the next under systemd's parser rather than standing on
|
|
36
|
+
* its own — all of them invalidate the definition instead of passing over silently.
|
|
37
|
+
*/
|
|
38
|
+
export function parseSystemdUnitHomes(body: string): SystemdHomeParse {
|
|
39
|
+
const homes: SystemdHomes = { codexHome: null, opencodexHome: null };
|
|
40
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
41
|
+
const line = rawLine.trim();
|
|
42
|
+
if (!line || /^[#;\[]/.test(line)) continue;
|
|
43
|
+
// systemd folds physical continuations before recognizing directive names. The writer never
|
|
44
|
+
// emits them; accepting individual lines could hide an override or invent a home assignment.
|
|
45
|
+
const trailingBackslashes = /\\+$/.exec(rawLine)?.[0].length ?? 0;
|
|
46
|
+
if (trailingBackslashes % 2 === 1) return { kind: "invalid" };
|
|
47
|
+
const separator = line.indexOf("=");
|
|
48
|
+
const lvalue = (separator === -1 ? line : line.slice(0, separator)).trim();
|
|
49
|
+
if (lvalue !== "Environment") {
|
|
50
|
+
if (!/^[A-Za-z][A-Za-z0-9_.\-]*$/.test(lvalue) || lvalue.includes("Environment")) {
|
|
51
|
+
return { kind: "invalid" };
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const directive = /^Environment\s*=\s*(.*)$/.exec(line);
|
|
56
|
+
if (!directive) return { kind: "invalid" };
|
|
57
|
+
const encoded = directive[1]!;
|
|
58
|
+
let assignment: string | undefined;
|
|
59
|
+
if (encoded.startsWith('"') && encoded.endsWith('"')) {
|
|
60
|
+
assignment = decodeQuoted(encoded.slice(1, -1));
|
|
61
|
+
} else if (encoded.length > 0 && !/[\s"'\\%\0]/.test(encoded)) {
|
|
62
|
+
// Older writers emitted one unquoted assignment. Anything needing quoting stays unknown.
|
|
63
|
+
assignment = encoded;
|
|
64
|
+
}
|
|
65
|
+
if (assignment === undefined) return { kind: "invalid" };
|
|
66
|
+
const pair = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(assignment);
|
|
67
|
+
if (!pair) return { kind: "invalid" };
|
|
68
|
+
const name = pair[1]!;
|
|
69
|
+
if (name !== "CODEX_HOME" && name !== "OPENCODEX_HOME") continue;
|
|
70
|
+
const key = name === "CODEX_HOME" ? "codexHome" : "opencodexHome";
|
|
71
|
+
if (homes[key] !== null || pair[2] === "") return { kind: "invalid" };
|
|
72
|
+
homes[key] = pair[2]!;
|
|
73
|
+
}
|
|
74
|
+
return { kind: "parsed", homes };
|
|
75
|
+
}
|
|
@@ -59,6 +59,13 @@ export interface WindowsGuardedManagerDeps {
|
|
|
59
59
|
winProcs?: () => WindowsProcessEntry[] | null;
|
|
60
60
|
winTaskXml?: () => string;
|
|
61
61
|
winTaskState?: () => WindowsTaskState;
|
|
62
|
+
/**
|
|
63
|
+
* Presence re-probe, invoked only on the failure paths that cannot tell a
|
|
64
|
+
* DELETED registration from an unreadable one. Absence is the one answer
|
|
65
|
+
* that changes the verdict — a registration that is gone can respawn
|
|
66
|
+
* nothing; "present" and "unknown" both stay unknown.
|
|
67
|
+
*/
|
|
68
|
+
winTaskProbe?: () => WindowsSchedulerTaskProbe;
|
|
62
69
|
winService?: () => WindowsWinswServiceInfo | null;
|
|
63
70
|
winRegistrationOurs?: (xml: string, expectedUserIds: readonly string[] | null) => boolean;
|
|
64
71
|
winTaskUserIds?: () => readonly string[] | null;
|
|
@@ -121,6 +128,18 @@ export function windowsProcessList(): WindowsProcessEntry[] | null {
|
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The PowerShell behind windowsScheduledTaskState — exported so the folder scope
|
|
133
|
+
* is reviewable. `-TaskName` alone is a folder-AGNOSTIC name filter: a same-named
|
|
134
|
+
* task in any folder answers the query (and several answers leave `.State` an
|
|
135
|
+
* array that reads as "unknown"), while every `/tn` operation the caller runs is
|
|
136
|
+
* root-folder scoped. `-TaskPath '\'` pins the read to the root registration.
|
|
137
|
+
*/
|
|
138
|
+
export function windowsScheduledTaskStateQuery(taskName: string): string {
|
|
139
|
+
return "(Get-ScheduledTask -TaskName '" + taskName.replace(/'/g, "''")
|
|
140
|
+
+ "' -TaskPath '\\' -ErrorAction Stop).State";
|
|
141
|
+
}
|
|
142
|
+
|
|
124
143
|
/**
|
|
125
144
|
* The registered task's RUNNING state, read through Get-ScheduledTask whose State
|
|
126
145
|
* values are enum names and therefore locale-independent — parsing schtasks table
|
|
@@ -129,9 +148,7 @@ export function windowsProcessList(): WindowsProcessEntry[] | null {
|
|
|
129
148
|
export function windowsScheduledTaskState(taskName = TASK): WindowsTaskState {
|
|
130
149
|
if (process.platform !== "win32") return "unknown";
|
|
131
150
|
try {
|
|
132
|
-
const output = runPowerShell(
|
|
133
|
-
"(Get-ScheduledTask -TaskName '" + taskName.replace(/'/g, "''") + "' -ErrorAction Stop).State",
|
|
134
|
-
).trim();
|
|
151
|
+
const output = runPowerShell(windowsScheduledTaskStateQuery(taskName)).trim();
|
|
135
152
|
if (/^running$/i.test(output)) return "running";
|
|
136
153
|
if (/^(ready|disabled)$/i.test(output)) return "not-running";
|
|
137
154
|
return "unknown";
|
|
@@ -229,6 +246,9 @@ function resolveWindowsDeps(deps: WindowsGuardedManagerDeps, live: boolean): Res
|
|
|
229
246
|
throw new Error("Windows task query is unavailable in this context");
|
|
230
247
|
}),
|
|
231
248
|
winTaskState: deps.winTaskState ?? (live ? () => windowsScheduledTaskState() : () => "unknown"),
|
|
249
|
+
winTaskProbe: deps.winTaskProbe ?? (live
|
|
250
|
+
? () => probeWindowsSchedulerTask()
|
|
251
|
+
: () => ({ status: "unknown", detail: "the Task Scheduler probe is unavailable in this context" })),
|
|
232
252
|
winService: deps.winService ?? (live ? () => windowsWinswServiceInfo() : () => null),
|
|
233
253
|
winRegistrationOurs: deps.winRegistrationOurs ?? ((xml, expectedUserIds) =>
|
|
234
254
|
windowsTaskRegistrationIsOurs(xml, expectedUserIds, (deps.winLauncherPath ?? windowsLauncherVbsPath)())),
|
|
@@ -367,12 +387,6 @@ function inspectWindowsSchedulerManager(
|
|
|
367
387
|
approvedPid: number,
|
|
368
388
|
io: ResolvedWindowsDeps,
|
|
369
389
|
): GuardedManagerTarget {
|
|
370
|
-
let xml: string;
|
|
371
|
-
try {
|
|
372
|
-
xml = io.winTaskXml();
|
|
373
|
-
} catch {
|
|
374
|
-
return unknown("the Task Scheduler registration could not be read");
|
|
375
|
-
}
|
|
376
390
|
const processes = io.winProcs();
|
|
377
391
|
if (processes === null) return unknown("the Windows process list could not be read");
|
|
378
392
|
const paths = wrapperPaths(io);
|
|
@@ -381,16 +395,34 @@ function inspectWindowsSchedulerManager(
|
|
|
381
395
|
if (unreadableAncestorWrapperPids(approvedPid, processes).length > 0) {
|
|
382
396
|
return unknown("a possible scheduler wrapper has an unreadable command line");
|
|
383
397
|
}
|
|
398
|
+
// A registration that cannot run anything owes only the stray-wrapper check.
|
|
399
|
+
// That covers BOTH a not-running task and a task proven deleted between the
|
|
400
|
+
// caller's presence probe and this inspection: in either case nothing
|
|
401
|
+
// registered can respawn the proxy, while a surviving wrapper still can —
|
|
402
|
+
// schtasks /end would report success on the inert task and leave it alive.
|
|
403
|
+
const inertVerdict = (): GuardedManagerTarget => strays.length === 0
|
|
404
|
+
? { kind: "absent" }
|
|
405
|
+
: unknown("a surviving scheduler wrapper could not be tied to the registered task");
|
|
384
406
|
const state = io.winTaskState();
|
|
385
|
-
if (state === "unknown")
|
|
386
|
-
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
407
|
+
if (state === "unknown") {
|
|
408
|
+
// Missing vs unreadable: the state query also fails for a task deleted
|
|
409
|
+
// between probes. A re-probe that proves absence is the inert verdict; a
|
|
410
|
+
// registration that persists or stays unanswerable keeps failing closed.
|
|
411
|
+
return io.winTaskProbe().status === "absent"
|
|
412
|
+
? inertVerdict()
|
|
413
|
+
: unknown("the registered task's running state could not be proven");
|
|
414
|
+
}
|
|
415
|
+
if (state === "not-running") return inertVerdict();
|
|
416
|
+
// Only the running path needs the registered definition: an unreadable
|
|
417
|
+
// registration cannot prove the task is ours, but it must not hold an
|
|
418
|
+
// otherwise-provable inert verdict unknown.
|
|
419
|
+
let xml: string;
|
|
420
|
+
try {
|
|
421
|
+
xml = io.winTaskXml();
|
|
422
|
+
} catch {
|
|
423
|
+
return io.winTaskProbe().status === "absent"
|
|
424
|
+
? inertVerdict()
|
|
425
|
+
: unknown("the Task Scheduler registration could not be read");
|
|
394
426
|
}
|
|
395
427
|
if (!io.winRegistrationOurs(xml, io.winTaskUserIds())) {
|
|
396
428
|
return unknown("the registered task is not a recognized OpenCodex definition");
|
|
@@ -520,8 +552,17 @@ export function observeWindowsGuardedManagerStopped(
|
|
|
520
552
|
if (native === "started") return "active";
|
|
521
553
|
if (scheduler.status === "present") {
|
|
522
554
|
const state = (deps.winTaskState ?? (live ? () => windowsScheduledTaskState() : () => "unknown"))();
|
|
523
|
-
if (state === "unknown") return "unknown";
|
|
524
555
|
if (state === "running") return "active";
|
|
556
|
+
if (state === "unknown") {
|
|
557
|
+
// The state read also fails for a task deleted between the presence probe
|
|
558
|
+
// and this observation. A registration that is GONE cannot respawn
|
|
559
|
+
// anything, so re-probe before failing closed — only a persisting or
|
|
560
|
+
// unanswerable task stays unknown. Proven absence still owes the
|
|
561
|
+
// unreadable-former-manager check below.
|
|
562
|
+
if ((deps.scheduler ?? probeWindowsSchedulerTask)().status !== "absent") {
|
|
563
|
+
return "unknown";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
525
566
|
}
|
|
526
567
|
if (deps.formerManagerPid !== undefined && unreadableWrappersOfManager(deps.formerManagerPid, procs).length > 0) {
|
|
527
568
|
return "unknown";
|
|
@@ -47,11 +47,20 @@ export function decodeSchtasksOutput(
|
|
|
47
47
|
return decodeWindowsTextBytes(buffer, options);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Every schtasks operation is sub-second on a healthy system; a wedged Task
|
|
52
|
+
* Scheduler service must not block a guarded stop forever. A killed command
|
|
53
|
+
* throws into the same unreadable/failed classification a nonzero exit would —
|
|
54
|
+
* the guarded verdicts stay fail-closed either way.
|
|
55
|
+
*/
|
|
56
|
+
const SCHTASKS_TIMEOUT_MS = 15_000;
|
|
57
|
+
|
|
50
58
|
function runFile(file: string, args: string[]): string {
|
|
51
59
|
const buffer = execFileSync(file, args, {
|
|
52
60
|
encoding: "buffer",
|
|
53
61
|
stdio: ["ignore", "pipe", "pipe"],
|
|
54
62
|
windowsHide: true,
|
|
63
|
+
timeout: SCHTASKS_TIMEOUT_MS,
|
|
55
64
|
}) as Buffer;
|
|
56
65
|
return decodeSchtasksOutput(buffer);
|
|
57
66
|
}
|
|
@@ -173,17 +182,27 @@ export function schtasksErrorDetail(error: unknown): string {
|
|
|
173
182
|
return error instanceof Error ? error.message : String(error);
|
|
174
183
|
}
|
|
175
184
|
|
|
176
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* True when a schtasks CSV listing line refers to the given task name.
|
|
187
|
+
* "Given task name" means the task of that name in the ROOT folder — the only
|
|
188
|
+
* one every `/tn` operation here can address; a same-named task inside a task
|
|
189
|
+
* folder is a different registration and must not count.
|
|
190
|
+
*/
|
|
177
191
|
export function windowsSchedulerCsvIncludesTask(csv: string, taskName: string): boolean {
|
|
178
192
|
const needle = taskName.toLowerCase();
|
|
179
193
|
for (const line of csv.split(/\r?\n/)) {
|
|
180
194
|
const lower = line.toLowerCase();
|
|
181
195
|
if (!lower.includes(needle)) continue;
|
|
182
|
-
// Prefer exact CSV field matches ("\TaskName" / "TaskName") before a
|
|
196
|
+
// Prefer exact CSV field matches ("\TaskName" / "TaskName") before a looser
|
|
197
|
+
// boundary hit. A bare backslash is NOT a boundary: `folder\name` is a path
|
|
198
|
+
// separator, not the root task — every /tn operation here resolves a bare
|
|
199
|
+
// name in the root folder only, so `,\Tools\opencodex-proxy,` is a different
|
|
200
|
+
// registration this probe can neither read nor stop. The optional `\\?`
|
|
201
|
+
// covers the unquoted root form `,\opencodex-proxy,` only.
|
|
183
202
|
if (
|
|
184
203
|
lower.includes(`"\\${needle}"`)
|
|
185
204
|
|| lower.includes(`"${needle}"`)
|
|
186
|
-
|| new RegExp(`(
|
|
205
|
+
|| new RegExp(`(^|,)\\\\?${needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([,"]|$)`).test(lower)
|
|
187
206
|
) {
|
|
188
207
|
return true;
|
|
189
208
|
}
|
|
@@ -4,6 +4,7 @@ import { TASK, windowsServiceScriptPath, windowsLauncherVbsPath, windowsTaskXmlP
|
|
|
4
4
|
import { windowsWscript } from "./windows-scheduler";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "../lib/bun-runtime";
|
|
7
|
+
import { REAL_BUN_MIN_BYTES } from "../lib/bun-binary-validator.mjs";
|
|
7
8
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
8
9
|
import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
9
10
|
import { cachedCurrentWindowsIdentity, resolveCurrentWindowsPrincipal, WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
|
|
@@ -54,6 +55,10 @@ function taskXmlRunLevelAcceptable(principal: string): boolean {
|
|
|
54
55
|
return value === "leastprivilege" || value === "highestavailable";
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Batch wrapper the scheduled task runs: restarts the proxy on exit, restores a transactional-update
|
|
60
|
+
* backup when the install is gone, and waits while bundled Bun is still npm's placeholder.
|
|
61
|
+
*/
|
|
57
62
|
export function buildWindowsServiceScript(
|
|
58
63
|
entry = cliEntry(),
|
|
59
64
|
port = resolveServiceListenPort(),
|
|
@@ -101,17 +106,21 @@ export function buildWindowsServiceScript(
|
|
|
101
106
|
'if not exist "%OCX_BUN%" (',
|
|
102
107
|
" call :restore_backup",
|
|
103
108
|
")",
|
|
104
|
-
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
// Locale dates can contain parentheses. Keep timestamp expansion outside blocks.
|
|
110
|
+
'if not exist "%OCX_BUN%" goto bun_missing',
|
|
111
|
+
// An in-place npm install extracts the bun package's tiny placeholder before its postinstall
|
|
112
|
+
// swaps in the real binary. Executing it fails with exit 216 and, in an interactive session,
|
|
113
|
+
// a modal "Unsupported 16-Bit Application" dialog that blocks this loop until dismissed.
|
|
114
|
+
// The install can also remove the file between the exist check and this read; an empty size
|
|
115
|
+
// would turn the comparison into a syntax error that ends the wrapper.
|
|
116
|
+
'set "OCX_BUN_BYTES="',
|
|
117
|
+
'for %%F in ("%OCX_BUN%") do set "OCX_BUN_BYTES=%%~zF"',
|
|
118
|
+
'if not defined OCX_BUN_BYTES goto bun_not_ready',
|
|
119
|
+
`if %OCX_BUN_BYTES% LSS ${REAL_BUN_MIN_BYTES} goto bun_not_ready`,
|
|
108
120
|
cli ? 'if not exist "%OCX_CLI%" (' : null,
|
|
109
121
|
cli ? " call :restore_backup" : null,
|
|
110
122
|
cli ? ")" : null,
|
|
111
|
-
cli ? 'if not exist "%OCX_CLI%"
|
|
112
|
-
cli ? ' >>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: CLI entry is missing; reinstall opencodex, then run ocx service repair' : null,
|
|
113
|
-
cli ? " exit /b 3" : null,
|
|
114
|
-
cli ? ")" : null,
|
|
123
|
+
cli ? 'if not exist "%OCX_CLI%" goto cli_missing' : null,
|
|
115
124
|
cli ? `"%OCX_BUN%" "%OCX_CLI%" start --port ${port} >>"%OCX_SERVICE_LOG%" 2>&1` : `"%OCX_BUN%" start --port ${port} >>"%OCX_SERVICE_LOG%" 2>&1`,
|
|
116
125
|
// Stop commands kill the wrapper; a zero child exit alone is not a stop request.
|
|
117
126
|
`if "%ERRORLEVEL%"=="${WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE}" goto stopped`,
|
|
@@ -123,6 +132,16 @@ export function buildWindowsServiceScript(
|
|
|
123
132
|
":stopped",
|
|
124
133
|
"endlocal",
|
|
125
134
|
"exit /b 0",
|
|
135
|
+
":bun_not_ready",
|
|
136
|
+
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] bundled Bun is not ready (%OCX_BUN_BYTES% bytes, npm placeholder or mid-install); waiting for its postinstall, retrying in 5s - if this persists, reinstall opencodex with bun scripts allowed',
|
|
137
|
+
"ping -n 6 127.0.0.1 >nul",
|
|
138
|
+
"goto loop",
|
|
139
|
+
":bun_missing",
|
|
140
|
+
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: bundled Bun is missing; reinstall opencodex, then run ocx service repair',
|
|
141
|
+
"exit /b 3",
|
|
142
|
+
cli ? ":cli_missing" : null,
|
|
143
|
+
cli ? '>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] installation is incomplete: CLI entry is missing; reinstall opencodex, then run ocx service repair' : null,
|
|
144
|
+
cli ? "exit /b 3" : null,
|
|
126
145
|
"",
|
|
127
146
|
// #1942/#1849: a power loss mid-swap leaves the live package dir missing/broken and
|
|
128
147
|
// a sibling .ocx-backup-* holding the previous version. This wrapper lives OUTSIDE
|
|
@@ -135,13 +154,16 @@ export function buildWindowsServiceScript(
|
|
|
135
154
|
' if exist "%OCX_PKG_DIR%" rmdir /s /q "%OCX_PKG_DIR%" 2>nul',
|
|
136
155
|
' move "%OCX_PKG_DIR%\\..\\%%B\\opencodex" "%OCX_PKG_DIR%" >nul 2>&1',
|
|
137
156
|
' if exist "%OCX_PKG_DIR%\\package.json" (',
|
|
138
|
-
'
|
|
139
|
-
" goto
|
|
157
|
+
' set "OCX_RESTORED_BACKUP=%%B"',
|
|
158
|
+
" goto backup_restored",
|
|
140
159
|
" )",
|
|
141
160
|
" )",
|
|
142
161
|
")",
|
|
143
162
|
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] no restorable backup found',
|
|
144
163
|
"goto :eof",
|
|
164
|
+
":backup_restored",
|
|
165
|
+
'>>"%OCX_SERVICE_LOG%" echo [%DATE% %TIME%] restored previous install from %OCX_RESTORED_BACKUP%',
|
|
166
|
+
"goto :eof",
|
|
145
167
|
].filter((line): line is string => Boolean(line));
|
|
146
168
|
return `${lines.join("\r\n")}\r\n`;
|
|
147
169
|
}
|