@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../combos/failover";
|
|
22
22
|
import { reconcileComboWarningMemos } from "../combos/request";
|
|
23
23
|
import { reconcileComboRotationState } from "../combos/resolve";
|
|
24
|
-
import { reconcileComboRecall } from "../server/responses/combo-session-recall";
|
|
24
|
+
import { reconcileComboRecall, sweepExpiredComboRecall } from "../server/responses/combo-session-recall";
|
|
25
25
|
import { listLiveComboTargetKeys } from "../combos/types";
|
|
26
26
|
import {
|
|
27
27
|
listLiveConfigOwnershipRoots,
|
|
@@ -112,7 +112,11 @@ export const STATE_STORE_REGISTRATIONS = [
|
|
|
112
112
|
{ name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration },
|
|
113
113
|
{ name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState },
|
|
114
114
|
{ name: "combo-rotation", reconcileGeneration: reconcileComboRotationState },
|
|
115
|
-
{
|
|
115
|
+
{
|
|
116
|
+
name: "combo-session-recall",
|
|
117
|
+
sweepExpired: sweepExpiredComboRecall,
|
|
118
|
+
reconcileGeneration: reconcileComboRecall,
|
|
119
|
+
},
|
|
116
120
|
{ name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff },
|
|
117
121
|
{ name: "codex-reauth", reconcileGeneration: reconcileCodexReauthState },
|
|
118
122
|
{ name: "oauth-reauth", reconcileGeneration: reconcileOAuthReauthState },
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* how this incident happened.
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
23
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
24
24
|
import { realpathSync } from "node:fs";
|
|
25
25
|
|
|
26
26
|
const GUARD_ENV = "OCX_TEST_HOME_GUARD";
|
|
@@ -152,3 +152,87 @@ export function assertNotRealCodexHomeUnderTest(dir: string): void {
|
|
|
152
152
|
+ "Point CODEX_HOME at a temp directory for this test before writing native auth.json.",
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The trees a removal must never reach, and the reason each one is named.
|
|
158
|
+
*
|
|
159
|
+
* The writer guard above cannot help here. `rmSync` is plain `node:fs`: it calls no writer of
|
|
160
|
+
* ours, so no assertion of ours runs, and by the time anything could observe the damage the
|
|
161
|
+
* directory is already gone. On 2026-09-15 that is exactly what happened — a test resolved the
|
|
162
|
+
* process-global config directory and removed it, taking every OAuth login, the Codex account
|
|
163
|
+
* store, the service tokens and a 372MB usage ledger with it.
|
|
164
|
+
*/
|
|
165
|
+
const PROTECTED_TREES: ReadonlyArray<{ path: string; lexical: string; label: string }> = [
|
|
166
|
+
{ path: PROTECTED_HOME, lexical: resolve(join(REAL_HOME, ".opencodex")), label: "the real OpenCodex home" },
|
|
167
|
+
{ path: PROTECTED_CODEX_HOME, lexical: resolve(join(REAL_HOME, ".codex")), label: "the real Codex home" },
|
|
168
|
+
{
|
|
169
|
+
path: PROTECTED_LAUNCH_AGENTS,
|
|
170
|
+
lexical: resolve(join(REAL_HOME, "Library", "LaunchAgents")),
|
|
171
|
+
label: "the real LaunchAgents directory",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
const PROTECTED_REAL_HOME = canonicalize(REAL_HOME);
|
|
175
|
+
const LEXICAL_REAL_HOME = resolve(REAL_HOME);
|
|
176
|
+
|
|
177
|
+
/** Canonical paths whose removal is refused. Exported so the guard's tests cannot drift off them. */
|
|
178
|
+
export function protectedRemovalTreesForTests(): readonly string[] {
|
|
179
|
+
return [PROTECTED_REAL_HOME, ...PROTECTED_TREES.map(tree => tree.path)];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether `child` sits strictly below `parent`, both already canonicalized. */
|
|
183
|
+
function isInside(parent: string, child: string): boolean {
|
|
184
|
+
const rel = relative(parent, child);
|
|
185
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Why removing `target` is refused, or `null` when it is not a protected location.
|
|
190
|
+
*
|
|
191
|
+
* Three relations are refused, not one. Equality alone would still permit
|
|
192
|
+
* `rmSync(getConfigPath())` against a live `config.json`, and it would permit
|
|
193
|
+
* `rmSync(homedir())`, which takes the protected tree with it. So a target is refused when it
|
|
194
|
+
* IS a protected tree, when it sits INSIDE one, or when it is an ANCESTOR of one.
|
|
195
|
+
*
|
|
196
|
+
* Canonicalization is what makes a symlink useless as a bypass: a temp path that merely points
|
|
197
|
+
* at the real home resolves to the real home before any comparison happens.
|
|
198
|
+
*/
|
|
199
|
+
export function protectedRemovalReason(target: string): string | null {
|
|
200
|
+
// Both spellings are judged, not just the canonical one. Canonicalization is what defeats a
|
|
201
|
+
// symlink alias, but it also resolves the target away: if `~/.opencodex` is itself a link,
|
|
202
|
+
// the literal path a caller passed is the thing that gets unlinked, and only the lexical
|
|
203
|
+
// form still names it. Upstream Codex makes the same distinction in its writable-root
|
|
204
|
+
// handling, keeping logical and resolved forms side by side rather than collapsing to one.
|
|
205
|
+
for (const candidate of [canonicalize(target), resolve(target)]) {
|
|
206
|
+
if (candidate === PROTECTED_REAL_HOME || candidate === LEXICAL_REAL_HOME) {
|
|
207
|
+
return `the real home directory (${PROTECTED_REAL_HOME})`;
|
|
208
|
+
}
|
|
209
|
+
for (const tree of PROTECTED_TREES) {
|
|
210
|
+
for (const protectedPath of [tree.path, tree.lexical]) {
|
|
211
|
+
if (candidate === protectedPath) return `${tree.label} (${protectedPath})`;
|
|
212
|
+
if (isInside(protectedPath, candidate)) return `a path inside ${tree.label} (${protectedPath})`;
|
|
213
|
+
if (isInside(candidate, protectedPath)) return `an ancestor of ${tree.label} (${protectedPath})`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Throw before a removal that would reach a protected tree.
|
|
222
|
+
*
|
|
223
|
+
* Deliberately NOT gated on {@link isTestHomeGuardArmed}. Arming happens in `tests/preload.ts`,
|
|
224
|
+
* which Bun loads from the `bunfig.toml` it finds in the CURRENT WORKING DIRECTORY — so a run
|
|
225
|
+
* started outside the repository arms nothing, leaves OPENCODEX_HOME unset, and resolves the
|
|
226
|
+
* developer's real home. That unarmed run is precisely the one that caused the incident, so the
|
|
227
|
+
* refusal has to hold without it. Nothing in production calls this; the callers are test
|
|
228
|
+
* helpers, where the only cost of an unconditional check is a path comparison.
|
|
229
|
+
*/
|
|
230
|
+
export function assertRemovalOutsideProtectedTrees(target: string): void {
|
|
231
|
+
const reason = protectedRemovalReason(target);
|
|
232
|
+
if (reason === null) return;
|
|
233
|
+
throw new Error(
|
|
234
|
+
`refusing to remove ${reason} from a test process: "${target}" resolves there. `
|
|
235
|
+
+ "Create the directory this test owns with createTempHome() from tests/helpers/temp-home "
|
|
236
|
+
+ "and remove that handle instead (see devlog 260730_codex_rs_upstream_v2_live_handoff/070).",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Retry guard for upstream fetches that die on stale pooled keep-alive sockets.
|
|
3
3
|
*
|
|
4
4
|
* chatgpt.com (Cloudflare) closes idle keep-alive connections server-side; Bun's fetch pool
|
|
5
|
-
* reuses the half-closed socket and
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* a
|
|
5
|
+
* reuses the half-closed socket and a request can fail before response headers arrive.
|
|
6
|
+
* A pre-header rejection does not prove that the origin did not process the request.
|
|
7
|
+
* Mechanically reusable bytes do not make a model POST idempotent: an ambiguous reset
|
|
8
|
+
* becomes a terminal, non-replayable response unless the operation is explicitly safe.
|
|
9
9
|
*
|
|
10
10
|
* Deliberately narrow: timeouts, aborts, ECONNREFUSED/DNS/TLS failures, and HTTP error
|
|
11
11
|
* statuses (returned as Response, never thrown) are NOT retried. Mid-stream SSE resets are
|
|
@@ -36,19 +36,66 @@ export function isNonReplayableResponse(response: Response): boolean {
|
|
|
36
36
|
return nonReplayableResponses.has(response);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The narrower marker: responses this proxy synthesized as a replay refusal.
|
|
41
|
+
*
|
|
42
|
+
* {@link isNonReplayableResponse} answers "must not be sent again", which the WebSocket
|
|
43
|
+
* post-send verdicts share. This one answers "the upstream never said this", and that is the
|
|
44
|
+
* question a quota recorder or a `Retry-After` synthesizer has to ask. Both were written for
|
|
45
|
+
* a status that only ever arrived from a provider, so a synthetic 429 reads to them as a
|
|
46
|
+
* credential that rate-limited us and as a wait worth honouring -- one writes a cooldown
|
|
47
|
+
* against a credential that refused nothing, the other instructs the client to send the turn
|
|
48
|
+
* again. A marker rather than a body check, because it has to be answerable before the body
|
|
49
|
+
* is read and cannot be spoofed by an upstream that happens to echo the code.
|
|
50
|
+
*/
|
|
51
|
+
const replayRefusalResponses = new WeakSet<Response>();
|
|
52
|
+
|
|
53
|
+
export function markReplayRefusalResponse(response: Response): void {
|
|
54
|
+
replayRefusalResponses.add(response);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isReplayRefusalResponse(response: Response): boolean {
|
|
58
|
+
return replayRefusalResponses.has(response);
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
/** Origin never produced a response event; the turn may still be executing. */
|
|
40
62
|
export const UPSTREAM_NO_RESPONSE_CODE = "upstream_no_response";
|
|
41
63
|
/** Transport closed after the send, before any response event. */
|
|
42
64
|
export const UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE = "upstream_closed_before_response";
|
|
65
|
+
/**
|
|
66
|
+
* This proxy refused to replay a pre-header fetch rejection.
|
|
67
|
+
*
|
|
68
|
+
* Distinct from {@link UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE}, which the Codex WebSocket
|
|
69
|
+
* transport settles as a 502 after the create frame was already sent. Both are ambiguous,
|
|
70
|
+
* but only this one is a refusal this process made before any response existed, so it
|
|
71
|
+
* follows the send-budget precedent and answers 429: the Codex client is configured with
|
|
72
|
+
* `retry_429: false` and `retry_5xx: true` over four attempts, so a 5xx here multiplies
|
|
73
|
+
* the duplicate send the refusal exists to prevent. See
|
|
74
|
+
* structure/transports/responses.md#ambiguous-connection-reset-replay-boundary.
|
|
75
|
+
*/
|
|
76
|
+
export const UPSTREAM_RESET_REPLAY_REFUSED_CODE = "upstream_reset_replay_refused";
|
|
43
77
|
const NON_REPLAYABLE_UPSTREAM_CODES: ReadonlySet<string> = new Set([
|
|
44
78
|
UPSTREAM_NO_RESPONSE_CODE,
|
|
45
79
|
UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
|
|
80
|
+
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
46
81
|
]);
|
|
47
82
|
|
|
48
83
|
export function isNonReplayableUpstreamCode(code: unknown): boolean {
|
|
49
84
|
return typeof code === "string" && NON_REPLAYABLE_UPSTREAM_CODES.has(code);
|
|
50
85
|
}
|
|
51
86
|
|
|
87
|
+
/**
|
|
88
|
+
* True for the one non-replayable code this proxy owns end to end. The status it carries is
|
|
89
|
+
* a local decision, so a re-wrapping formatter must restate it rather than inherit the
|
|
90
|
+
* caller's upstream-shaped status.
|
|
91
|
+
*/
|
|
92
|
+
export function isReplayRefusalCode(code: unknown): boolean {
|
|
93
|
+
return code === UPSTREAM_RESET_REPLAY_REFUSED_CODE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Client-facing status for {@link UPSTREAM_RESET_REPLAY_REFUSED_CODE}. */
|
|
97
|
+
export const REPLAY_REFUSED_STATUS = 429;
|
|
98
|
+
|
|
52
99
|
// 1 initial + 2 retries: the pool may hold more than one stale socket.
|
|
53
100
|
const RESET_RETRY_MAX_ATTEMPTS = 3;
|
|
54
101
|
const RESET_RETRY_BASE_DELAY_MS = 150;
|
|
@@ -139,7 +186,11 @@ export interface RetryBackoffOptions {
|
|
|
139
186
|
* first instead of silently lengthening every adapter's backoff.
|
|
140
187
|
*/
|
|
141
188
|
retryAfterIsLowerBound?: boolean;
|
|
142
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* The wait deadline a caller applies to an honoured `Retry-After`. The delay itself is
|
|
191
|
+
* never shortened: an instruction longer than the deadline is a reason to END with the
|
|
192
|
+
* upstream answer, not to send early. Kept for callers that still pass it.
|
|
193
|
+
*/
|
|
143
194
|
retryAfterCeilingMs?: number;
|
|
144
195
|
}
|
|
145
196
|
|
|
@@ -305,10 +356,10 @@ export function retryBackoffDelayMs(attempt: number, opts: RetryBackoffOptions):
|
|
|
305
356
|
// A provider that names a wait is stating when it will serve again; sending earlier is a
|
|
306
357
|
// request we already know will be refused, and refusing it twice is the retry storm the
|
|
307
358
|
// header exists to prevent. The local maximum bounds our OWN exponential backoff and has no
|
|
308
|
-
// business shortening someone else's instruction
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
return Math.
|
|
359
|
+
// business shortening someone else's instruction, so the instruction is returned in full.
|
|
360
|
+
// Whether the request can afford to wait that long is the caller's deadline decision --
|
|
361
|
+
// fetchWithTransientRetry ends with the upstream answer rather than retrying early.
|
|
362
|
+
return Math.max(retryAfter, jittered);
|
|
312
363
|
}
|
|
313
364
|
|
|
314
365
|
export function cancelResponseBodyBestEffort(res: Response): void {
|
|
@@ -348,22 +399,42 @@ export async function fetchWithAttemptDeadline(
|
|
|
348
399
|
}
|
|
349
400
|
|
|
350
401
|
export interface ResetRetryOptions {
|
|
402
|
+
/**
|
|
403
|
+
* Opt in only when repeating this operation cannot duplicate upstream effects.
|
|
404
|
+
* This permits reset retries, not extra sends: attempts and onSendsConsumed still
|
|
405
|
+
* bound and count every physical send. A string body is not replay-safety proof.
|
|
406
|
+
*/
|
|
407
|
+
replaySafe?: boolean;
|
|
351
408
|
abortSignal?: AbortSignal;
|
|
352
409
|
/** Short host/path label for the retry warn log (no secrets/query strings). */
|
|
353
410
|
label?: string;
|
|
354
411
|
/** Total upstream sends allowed, including the first one. Not a per-layer retry count. */
|
|
355
412
|
attempts?: number;
|
|
413
|
+
/**
|
|
414
|
+
* Reports how many upstream sends this call actually consumed, so a caller that spans
|
|
415
|
+
* several legs of one request (initial send, then a 429/account-recovery refetch) can
|
|
416
|
+
* keep them on ONE budget instead of handing each leg a fresh one.
|
|
417
|
+
*
|
|
418
|
+
* It lives on the RESET options, not on the transient ones, because every leg that falls
|
|
419
|
+
* back to reset-only retry -- the non-policy adapter initial send, and every
|
|
420
|
+
* `rebuildAndRefetch` recovery kind whose provider has no transient policy -- was not merely
|
|
421
|
+
* uncounted but UNCOUNTABLE: the callback existed on a type those call sites never reach.
|
|
422
|
+
*/
|
|
423
|
+
onSendsConsumed?: (sends: number) => void;
|
|
356
424
|
}
|
|
357
425
|
|
|
358
426
|
export interface TransientRetryOptions extends ResetRetryOptions {
|
|
359
427
|
/** Test seam: per-attempt slow budget override (defaults to TRANSIENT_RETRY_SLOW_ATTEMPT_MS). */
|
|
360
428
|
slowAttemptMs?: number;
|
|
361
429
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
430
|
+
* How long this caller can wait on an honoured `Retry-After`, defaulting to
|
|
431
|
+
* {@link RETRY_AFTER_CEILING_MS}. It is a deadline, never a clamp: an instruction inside it
|
|
432
|
+
* is slept in full, and an instruction past it ends the call with the upstream answer and
|
|
433
|
+
* its `Retry-After` intact rather than sending early at a provider that already said it
|
|
434
|
+
* would refuse. A caller with a shorter budget than a minute says so and is not parked past
|
|
435
|
+
* it; a caller that can genuinely wait longer says so and is not cut short.
|
|
365
436
|
*/
|
|
366
|
-
|
|
437
|
+
retryAfterCeilingMs?: number;
|
|
367
438
|
}
|
|
368
439
|
|
|
369
440
|
export type UpstreamSendRecovery = "connection-reset" | "transient-5xx";
|
|
@@ -424,9 +495,9 @@ export function applyUpstreamRecoveryInit<T extends RequestInit>(
|
|
|
424
495
|
}
|
|
425
496
|
|
|
426
497
|
/**
|
|
427
|
-
* Run `doFetch
|
|
428
|
-
*
|
|
429
|
-
*
|
|
498
|
+
* Run `doFetch` within one send budget. Connection-reset-shaped rejections are
|
|
499
|
+
* terminal by default; only an explicitly replay-safe operation receives reset retries
|
|
500
|
+
* with jittered backoff. HTTP responses retain the caller's existing retry policy.
|
|
430
501
|
*/
|
|
431
502
|
export async function fetchWithResetRetry(
|
|
432
503
|
doFetch: ReplayableFetch,
|
|
@@ -442,6 +513,10 @@ export async function fetchWithResetRetry(
|
|
|
442
513
|
let sawReset = false;
|
|
443
514
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
444
515
|
if (opts.abortSignal?.aborted) throw abortError(opts.abortSignal);
|
|
516
|
+
// Reported before the await, one physical send at a time: a send that rejects has still
|
|
517
|
+
// been made, and this helper leaves through four exits (return, reset give-up, non-reset
|
|
518
|
+
// rethrow, abort), so a per-send report is the only shape that is correct on all of them.
|
|
519
|
+
opts.onSendsConsumed?.(1);
|
|
445
520
|
try {
|
|
446
521
|
return await doFetch(attempt === 0 ? firstRecovery : "connection-reset");
|
|
447
522
|
} catch (err) {
|
|
@@ -453,6 +528,20 @@ export async function fetchWithResetRetry(
|
|
|
453
528
|
if (sawReset) throw new UpstreamRetryEvidenceError([], err, true);
|
|
454
529
|
throw err;
|
|
455
530
|
}
|
|
531
|
+
if (opts.replaySafe !== true) {
|
|
532
|
+
// Return evidence instead of throwing a generic transport error: outer catches
|
|
533
|
+
// otherwise turn it into a replayable 502 and a combo/account recovery resends it.
|
|
534
|
+
// The WeakSet protects in-process recovery; the code survives JSON re-wrapping.
|
|
535
|
+
// Never expose the raw exception, which can contain credentials or request data.
|
|
536
|
+
const response = new Response(JSON.stringify({ error: {
|
|
537
|
+
type: "upstream_error",
|
|
538
|
+
code: UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
539
|
+
message: "The upstream connection closed before a response was received. The request may already have been processed; automatic replay was stopped.",
|
|
540
|
+
} }), { status: REPLAY_REFUSED_STATUS, headers: { "content-type": "application/json" } });
|
|
541
|
+
markResponseNonReplayable(response);
|
|
542
|
+
markReplayRefusalResponse(response);
|
|
543
|
+
return response;
|
|
544
|
+
}
|
|
456
545
|
if (attempt === attempts - 1) throw err;
|
|
457
546
|
sawReset = true;
|
|
458
547
|
lastError = err;
|
|
@@ -469,9 +558,9 @@ export async function fetchWithResetRetry(
|
|
|
469
558
|
}
|
|
470
559
|
|
|
471
560
|
/**
|
|
472
|
-
* fetchWithResetRetry plus
|
|
473
|
-
*
|
|
474
|
-
*
|
|
561
|
+
* fetchWithResetRetry plus the caller-selected transient-5xx policy, PRE-STREAM only.
|
|
562
|
+
* A received HTTP error follows that policy; an ambiguous reset's non-replayable
|
|
563
|
+
* verdict always stops it. The failed attempt's body is cancelled before the
|
|
475
564
|
* retry; every returned response (ok, non-transient, aborted, slow, exhausted) keeps
|
|
476
565
|
* its body intact. Honors Retry-After via retryBackoffDelayMs.
|
|
477
566
|
*
|
|
@@ -504,13 +593,22 @@ export async function fetchWithTransientRetry(
|
|
|
504
593
|
// more send -- the loop condition alone was never enough, because every later recovery leg
|
|
505
594
|
// called this helper again and the floor funded each of them.
|
|
506
595
|
const remaining = () => Math.max(0, budget - sent);
|
|
596
|
+
// The inner reset layer now has its own `onSendsConsumed`, and these are the same physical
|
|
597
|
+
// sends `countedFetch` already counts. Forwarding the reporter down the `remaining()` path
|
|
598
|
+
// would report each of them twice, which is how a four-send cap becomes a two-send cap. One
|
|
599
|
+
// send is counted once, by the outermost layer that owns the budget.
|
|
600
|
+
const innerResetOptions = (): ResetRetryOptions => ({
|
|
601
|
+
...opts,
|
|
602
|
+
attempts: remaining(),
|
|
603
|
+
onSendsConsumed: undefined,
|
|
604
|
+
});
|
|
507
605
|
// Reported in `finally` rather than at each exit: this function returns from five places
|
|
508
606
|
// and throws from one, and a caller sharing the budget across request legs must be told the
|
|
509
607
|
// real count on every one of them.
|
|
510
608
|
try {
|
|
511
609
|
if (budget === 0) throw new SendBudgetExhaustedError(opts.label);
|
|
512
610
|
let attemptStart = Date.now();
|
|
513
|
-
let res = await fetchWithResetRetry(countedFetch,
|
|
611
|
+
let res = await fetchWithResetRetry(countedFetch, innerResetOptions());
|
|
514
612
|
for (let attempt = 0; sent < budget; attempt++) {
|
|
515
613
|
// A non-replayable gateway status was settled after the request body had already left
|
|
516
614
|
// for the origin; retrying it here is the automatic resend the marker exists to forbid.
|
|
@@ -519,6 +617,19 @@ export async function fetchWithTransientRetry(
|
|
|
519
617
|
// a response whose body we just cancelled.
|
|
520
618
|
if (opts.abortSignal?.aborted) return res;
|
|
521
619
|
if (Date.now() - attemptStart > slowAttemptMs) return res;
|
|
620
|
+
const instructedDelay = retryAfterDelayMs(res.headers);
|
|
621
|
+
// The deadline is the CALLER'S, not this module's default. Reading the constant directly
|
|
622
|
+
// broke it in both directions: a caller with a 30s budget slept the full 45s an upstream
|
|
623
|
+
// asked for, and a caller that could genuinely wait 120s was handed the error back for a
|
|
624
|
+
// 90s instruction it was willing to honour.
|
|
625
|
+
const waitDeadlineMs = opts.retryAfterCeilingMs ?? RETRY_AFTER_CEILING_MS;
|
|
626
|
+
if (instructedDelay !== undefined && instructedDelay > waitDeadlineMs) {
|
|
627
|
+
// Honouring the stated wait would park this request past the deadline it can commit
|
|
628
|
+
// to, and sleeping only up to the deadline is a send the provider already said it will
|
|
629
|
+
// refuse. End here instead: the caller receives the upstream answer with its
|
|
630
|
+
// Retry-After intact and applies its own policy, exactly as on the direct path.
|
|
631
|
+
return res;
|
|
632
|
+
}
|
|
522
633
|
console.warn(
|
|
523
634
|
`[upstream-retry] transient ${res.status}${opts.label ? ` (${opts.label})` : ""} — retrying (${sent + 1}/${budget})`,
|
|
524
635
|
);
|
|
@@ -535,7 +646,7 @@ export async function fetchWithTransientRetry(
|
|
|
535
646
|
attemptStart = Date.now();
|
|
536
647
|
transientStatuses.push(res.status);
|
|
537
648
|
try {
|
|
538
|
-
res = await fetchWithResetRetry(countedFetch,
|
|
649
|
+
res = await fetchWithResetRetry(countedFetch, innerResetOptions(), "transient-5xx");
|
|
539
650
|
} catch (err) {
|
|
540
651
|
// Keep the prior 5xx evidence attached: the origin already responded, so
|
|
541
652
|
// this rejection is not pre-connection and must not classify as neutral.
|
|
@@ -250,6 +250,21 @@ export const OCX_ELEVATED_PROTOCOL_FAILED = 13;
|
|
|
250
250
|
/** Windows ERROR_CANCELLED — reserved for UAC denial; never emitted by the elevated script. */
|
|
251
251
|
export const OCX_ELEVATED_UAC_CANCELLED = 1223;
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* The elevated process could not read a staged payload (#4692).
|
|
255
|
+
*
|
|
256
|
+
* `hardenSecretPath` grants the staging account and strips inheritance, so a split-token
|
|
257
|
+
* elevation of the same user reads the file and an elevation answered with a DIFFERENT
|
|
258
|
+
* administrator's credentials does not. The elevated side cannot explain that itself: it
|
|
259
|
+
* runs hidden, so its stderr goes nowhere and only the exit code survives the boundary.
|
|
260
|
+
* Without a code of its own the operator would be told "exit code 1" for a cause that
|
|
261
|
+
* names its own remedy — the same undiagnosable failure this change set exists to remove.
|
|
262
|
+
*
|
|
263
|
+
* Deliberately outside OCX_ELEVATED_PROTOCOL_CODES: that list is the create-and-run
|
|
264
|
+
* transaction's alphabet, and this code belongs to the registration path.
|
|
265
|
+
*/
|
|
266
|
+
export const OCX_ELEVATED_STAGING_UNREADABLE = 14;
|
|
267
|
+
|
|
253
268
|
export const OCX_ELEVATED_PROTOCOL_CODES = [
|
|
254
269
|
OCX_ELEVATED_SUCCESS,
|
|
255
270
|
OCX_ELEVATED_CREATE_FAILED,
|
|
@@ -645,36 +660,83 @@ export function runWindowsElevated(file: string, args: string[]): Promise<number
|
|
|
645
660
|
}
|
|
646
661
|
|
|
647
662
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
663
|
+
* A task definition staged for the elevated process.
|
|
664
|
+
*
|
|
665
|
+
* The bytes live in a freshly created, ACL-hardened private directory, and the digest is
|
|
666
|
+
* taken over exactly those bytes by the caller that validated them. The elevated script
|
|
667
|
+
* reads the file once, hashes what it read, and refuses unless the digest matches, so a
|
|
668
|
+
* pathname is no longer a promise about content — it is a claim the receiver checks.
|
|
669
|
+
*/
|
|
670
|
+
export interface StagedWindowsTaskXml {
|
|
671
|
+
/** Path inside the caller's hardened staging directory. */
|
|
672
|
+
readonly path: string;
|
|
673
|
+
/** Lowercase hex SHA-256 of the staged bytes (UTF-16LE, no BOM). */
|
|
674
|
+
readonly sha256: string;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Read a staged payload, prove it is the one that was validated, and decode it.
|
|
679
|
+
*
|
|
680
|
+
* One read: the bytes that are hashed are the same array that is decoded and registered.
|
|
681
|
+
* Hashing a path and then reopening it would reintroduce the swap window this check
|
|
682
|
+
* exists to close.
|
|
683
|
+
*/
|
|
684
|
+
const READ_STAGED_TASK_XML = "function Read-OcxStagedTaskXml([string]$path, [string]$expectedHash) {"
|
|
685
|
+
// An unreadable payload is a diagnosable condition, not a generic throw: a hidden
|
|
686
|
+
// elevated process has nowhere to print, so the cause has to ride the exit code.
|
|
687
|
+
+ " try { $bytes = [IO.File]::ReadAllBytes($path) }"
|
|
688
|
+
+ " catch [System.UnauthorizedAccessException] { exit " + OCX_ELEVATED_STAGING_UNREADABLE + " }"
|
|
689
|
+
+ " catch [System.Security.SecurityException] { exit " + OCX_ELEVATED_STAGING_UNREADABLE + " };"
|
|
690
|
+
+ " $sha = [Security.Cryptography.SHA256]::Create();"
|
|
691
|
+
+ " try { $actual = [BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() };"
|
|
692
|
+
+ " if ($actual -cne $expectedHash) { throw 'Task Scheduler staged payload failed its integrity check.' };"
|
|
693
|
+
+ " return [Text.Encoding]::Unicode.GetString($bytes) }";
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Register one scheduled-task definition from staged, digest-verified bytes.
|
|
697
|
+
*
|
|
698
|
+
* The payloads used to be embedded as base64(utf16le) inside an inner PowerShell script
|
|
699
|
+
* that was itself base64(utf16le)-encoded into `-EncodedCommand`. Two layers of base64
|
|
700
|
+
* over UTF-16 cost about 14.2 command-line characters per XML character, and a
|
|
701
|
+
* replacement carries two payloads, so a ~2 KB task definition pushed the outer command
|
|
702
|
+
* past the Windows limit and the spawn failed with ENAMETOOLONG before UAC ever
|
|
703
|
+
* appeared (#4692). On a host where the trigger scope exports as an account name the
|
|
704
|
+
* re-register path runs on every repair, so repair could never succeed.
|
|
705
|
+
*
|
|
706
|
+
* The command now carries two paths and two 64-character digests, so its length no
|
|
707
|
+
* longer depends on the size of the XML at all.
|
|
708
|
+
*
|
|
709
|
+
* The original design goal was "immutable bytes, never a caller-writable pathname".
|
|
710
|
+
* That goal is kept by different means rather than abandoned: the staging directory is
|
|
711
|
+
* private and ACL-hardened, the files are created exclusively so nothing can be waiting
|
|
712
|
+
* at the path, and the digest makes a same-account swap during the UAC prompt fail
|
|
713
|
+
* closed instead of registering something else. An ACL alone could not do that last
|
|
714
|
+
* part, because a process running as the same user has the same SID.
|
|
715
|
+
*
|
|
716
|
+
* The replacement precondition is unchanged: the elevated process still re-queries the
|
|
717
|
+
* live registration and compares it to the captured predecessor before passing -Force.
|
|
651
718
|
*/
|
|
652
719
|
export function runWindowsElevatedScheduledTaskRegistration(
|
|
653
720
|
taskName: string,
|
|
654
|
-
xml:
|
|
721
|
+
xml: StagedWindowsTaskXml,
|
|
655
722
|
replace = false,
|
|
656
|
-
|
|
723
|
+
expectedExisting?: StagedWindowsTaskXml,
|
|
657
724
|
): Promise<number> {
|
|
658
|
-
if (replace && !
|
|
725
|
+
if (replace && !expectedExisting) {
|
|
659
726
|
throw new Error("Elevated Task Scheduler replacement requires a captured existing definition.");
|
|
660
727
|
}
|
|
661
|
-
const xmlBase64 = Buffer.from(xml, "utf16le").toString("base64");
|
|
662
|
-
const expectedExistingBase64 = expectedExistingXml === undefined
|
|
663
|
-
? null
|
|
664
|
-
: Buffer.from(expectedExistingXml, "utf16le").toString("base64");
|
|
665
728
|
const powerShellPath = windowsPowerShell();
|
|
666
729
|
const powerShellDirectory = powerShellPath.replace(/[\\/][^\\/]+$/, "");
|
|
667
730
|
const scheduledTasksModule = `${powerShellDirectory}\\Modules\\ScheduledTasks\\ScheduledTasks.psd1`;
|
|
668
731
|
const inner = [
|
|
669
732
|
`$taskName = ${psSingleQuote(taskName)}`,
|
|
670
|
-
|
|
671
|
-
|
|
733
|
+
READ_STAGED_TASK_XML,
|
|
734
|
+
`$xml = Read-OcxStagedTaskXml ${psSingleQuote(xml.path)} ${psSingleQuote(xml.sha256)}`,
|
|
672
735
|
`$module = Microsoft.PowerShell.Core\\Import-Module -Name ${psSingleQuote(scheduledTasksModule)} -PassThru -Force -ErrorAction Stop`,
|
|
673
736
|
"$registerTask = $module.ExportedCommands['Register-ScheduledTask']",
|
|
674
737
|
"if ($null -eq $registerTask) { throw 'Trusted ScheduledTasks module does not export Register-ScheduledTask.' }",
|
|
675
738
|
...(replace ? [
|
|
676
|
-
`$
|
|
677
|
-
"$expectedXml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($expectedBase64))",
|
|
739
|
+
`$expectedXml = Read-OcxStagedTaskXml ${psSingleQuote(expectedExisting!.path)} ${psSingleQuote(expectedExisting!.sha256)}`,
|
|
678
740
|
`$schtasks = ${psSingleQuote(resolveTrustedWindowsSchtasksExe())}`,
|
|
679
741
|
"$currentXml = & $schtasks /query /tn $taskName /xml 2>$null | Out-String",
|
|
680
742
|
"if ($LASTEXITCODE -ne 0) { throw 'Task Scheduler replacement precondition could not be read.' }",
|