@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place that knows how this proxy refuses a turn on its own workflow budget.
|
|
3
|
+
*
|
|
4
|
+
* It is a module rather than two inline blocks because the two call sites -- the HTTP admission
|
|
5
|
+
* check in `src/server/index.ts` and the pre-dispatch ceiling check in
|
|
6
|
+
* `src/server/responses/core.ts` -- had drifted into saying different things about the same
|
|
7
|
+
* refusal, and because the non-obvious part below has to be stated once and not twice.
|
|
8
|
+
*/
|
|
9
|
+
import { formatErrorResponse } from "../bridge";
|
|
10
|
+
import {
|
|
11
|
+
addFinalRequestLog,
|
|
12
|
+
markLocalRequestLogRefusal,
|
|
13
|
+
type RequestLogContext,
|
|
14
|
+
} from "./request-log";
|
|
15
|
+
import {
|
|
16
|
+
WORKFLOW_LOCAL_REFUSAL_HEADER,
|
|
17
|
+
workflowDenialSummary,
|
|
18
|
+
recordWorkflowRefusalEvent,
|
|
19
|
+
type WorkflowDenial,
|
|
20
|
+
} from "../lib/workflow-budget";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a caller needs to hand over for the refusal to become a row on `/api/logs`.
|
|
24
|
+
*
|
|
25
|
+
* The HTTP admission check refuses before the body is parsed, so its `logCtx` still carries the
|
|
26
|
+
* `unknown` model and provider the caller seeded it with. That is the honest record -- this
|
|
27
|
+
* request genuinely never resolved either -- and it is the same placeholder the native
|
|
28
|
+
* passthrough path already writes. Skipping the row entirely was the worse option: an operator
|
|
29
|
+
* reading the logs saw no trace at all of a request the proxy had refused.
|
|
30
|
+
*/
|
|
31
|
+
export interface WorkflowRefusalLog {
|
|
32
|
+
readonly requestId: string;
|
|
33
|
+
readonly start: number;
|
|
34
|
+
readonly logCtx: RequestLogContext;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the 429 for a refusal this proxy made itself.
|
|
39
|
+
*
|
|
40
|
+
* The status and type arguments below do not reach the client: `classifyError` rewrites every
|
|
41
|
+
* 429 to `rate_limit_error` / `rate_limit_exceeded`, so the body is shaped exactly like a
|
|
42
|
+
* provider rate limit. That is a deliberate wire contract -- changing it would change how every
|
|
43
|
+
* client retries -- which leaves two places to carry the truth. The message names the ceiling
|
|
44
|
+
* that fired and says no provider was contacted, and the header carries the machine-readable
|
|
45
|
+
* name. Nothing upstream sets that header, so its presence is conclusive.
|
|
46
|
+
*
|
|
47
|
+
* The row is where an operator actually looks, so it gets the same treatment #4639 established:
|
|
48
|
+
* `terminalSource: "synthetic"`, a local reason, and an error code naming the ceiling. Pass
|
|
49
|
+
* `logCtx` when the caller is inside a turn that will write its own row, or `refusalLog` when
|
|
50
|
+
* the refusal happens before any row exists and this is the only chance to write one.
|
|
51
|
+
*/
|
|
52
|
+
export function workflowRefusalResponse(
|
|
53
|
+
reason: WorkflowDenial,
|
|
54
|
+
logCtx?: RequestLogContext,
|
|
55
|
+
refusalLog?: WorkflowRefusalLog,
|
|
56
|
+
rootId?: string,
|
|
57
|
+
): Response {
|
|
58
|
+
const summary = workflowDenialSummary(reason);
|
|
59
|
+
// Only a caller that decided the refusal ITSELF passes a root id. admitWorkflowTurn already
|
|
60
|
+
// records its own denials, so passing one there would double-count them.
|
|
61
|
+
if (rootId) recordWorkflowRefusalEvent(rootId, reason);
|
|
62
|
+
const recordOn = logCtx ?? refusalLog?.logCtx;
|
|
63
|
+
if (recordOn) {
|
|
64
|
+
markLocalRequestLogRefusal(recordOn, summary.code);
|
|
65
|
+
// A locally assigned code wins in addFinalRequestLog, so this is what names the ceiling in
|
|
66
|
+
// the logs column rather than the generic rate-limit classification a 429 would get.
|
|
67
|
+
recordOn.errorCode = summary.code;
|
|
68
|
+
}
|
|
69
|
+
if (refusalLog) {
|
|
70
|
+
addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, 429, {
|
|
71
|
+
closeReason: "terminal",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const refusal = formatErrorResponse(
|
|
75
|
+
429,
|
|
76
|
+
reason === "workflow-sends-exhausted" ? "workflow_budget_exhausted" : "queue_capacity_exceeded",
|
|
77
|
+
summary.message,
|
|
78
|
+
);
|
|
79
|
+
refusal.headers.set(WORKFLOW_LOCAL_REFUSAL_HEADER, summary.code);
|
|
80
|
+
// Without this a browser dashboard cannot read the header at all: the data plane never sets
|
|
81
|
+
// Access-Control-Expose-Headers, so a cross-origin reader sees only the CORS-safelisted ones.
|
|
82
|
+
refusal.headers.set("Access-Control-Expose-Headers", WORKFLOW_LOCAL_REFUSAL_HEADER);
|
|
83
|
+
return refusal;
|
|
84
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { win32 } from "node:path";
|
|
3
4
|
import { winswXmlPath } from "../lib/winsw";
|
|
4
5
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
@@ -9,7 +10,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmdirSync, unlinkSync } from "node:
|
|
|
9
10
|
import { tmpdir } from "node:os";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import { getConfigDir } from "../config";
|
|
12
|
-
import { runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError } from "../lib/windows-elevation";
|
|
13
|
+
import { OCX_ELEVATED_STAGING_UNREADABLE, runWindowsElevatedScheduledTaskRegistration, WindowsSchtasksError, type StagedWindowsTaskXml } from "../lib/windows-elevation";
|
|
13
14
|
import { defaultWinswEntry, installWinswService, statusWinswRaw, uninstallWinswService, WINSW_SERVICE_ID, type WinswStatus } from "../lib/winsw";
|
|
14
15
|
import { forgetEphemeralSecretDir, forgetEphemeralSecretPath, hardenSecretDir } from "../lib/windows-secret-acl";
|
|
15
16
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
@@ -165,6 +166,197 @@ function cleanupWindowsSchedulerStage(
|
|
|
165
166
|
if (cleanupError) throw cleanupError;
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/** A staged payload set for one elevated registration, plus the way to remove it. */
|
|
170
|
+
export interface StagedElevatedSchedulerRegistration {
|
|
171
|
+
readonly xml: StagedWindowsTaskXml;
|
|
172
|
+
readonly expectedExisting?: StagedWindowsTaskXml;
|
|
173
|
+
/** Remove every staged artifact. Idempotent, so a second call after success is a no-op. */
|
|
174
|
+
cleanup(): void;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ElevatedSchedulerStagingDeps {
|
|
178
|
+
createStageDir?: () => string;
|
|
179
|
+
hardenDir?: (path: string) => void;
|
|
180
|
+
writePayload?: (path: string, bytes: Buffer) => void;
|
|
181
|
+
hardenPath?: (path: string) => void;
|
|
182
|
+
inspect?: (path: string) => { isSymbolicLink(): boolean; isFile(): boolean; isDirectory(): boolean };
|
|
183
|
+
removeStageDir?: (path: string) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stage the captured definitions an elevated registration needs, as files rather than
|
|
188
|
+
* as command-line payloads (#4692).
|
|
189
|
+
*
|
|
190
|
+
* A file that an administrator process will read is itself a privilege-escalation
|
|
191
|
+
* surface, so three properties have to hold together and none of them is sufficient
|
|
192
|
+
* alone:
|
|
193
|
+
*
|
|
194
|
+
* - **Access.** The directory is created fresh by `mkdtemp`, then ACL-hardened before
|
|
195
|
+
* anything is written into it, so another local account cannot read or replace the
|
|
196
|
+
* payload while the UAC prompt is open. Hardening the directory first is what makes
|
|
197
|
+
* the file private from the moment it exists.
|
|
198
|
+
* - **No reparse point.** Each artifact is inspected with `lstat` and rejected unless it
|
|
199
|
+
* is what it claims to be. `wx` already refuses to create over an existing name, which
|
|
200
|
+
* is the atomic step here — there is no replace path to race, because every path is
|
|
201
|
+
* inside a directory that did not exist a moment ago. The explicit check is what keeps
|
|
202
|
+
* that guarantee from depending on a reading of `O_EXCL` semantics.
|
|
203
|
+
* - **Tamper evidence.** The digest is taken over the exact bytes written, and the
|
|
204
|
+
* elevated script recomputes it over the bytes it reads. An ACL cannot cover this:
|
|
205
|
+
* a process running as the same user has the same SID and can rewrite the file, so
|
|
206
|
+
* the digest is the only thing that makes such a swap fail closed rather than
|
|
207
|
+
* silently register a different task definition.
|
|
208
|
+
*
|
|
209
|
+
* Payloads are UTF-16LE with no BOM, and the elevated process decodes them straight into
|
|
210
|
+
* `Register-ScheduledTask`. What is hashed is therefore exactly what is registered, with
|
|
211
|
+
* no trimming step in between that the two sides could disagree about.
|
|
212
|
+
*/
|
|
213
|
+
export function stageElevatedSchedulerRegistration(
|
|
214
|
+
xml: string,
|
|
215
|
+
expectedExistingXml?: string,
|
|
216
|
+
deps: ElevatedSchedulerStagingDeps = {},
|
|
217
|
+
): StagedElevatedSchedulerRegistration {
|
|
218
|
+
const createStageDir = deps.createStageDir
|
|
219
|
+
?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
|
|
220
|
+
const hardenDir = deps.hardenDir ?? ((path: string) => { hardenSecretDir(path, { required: true }); });
|
|
221
|
+
const writePayload = deps.writePayload ?? ((path: string, bytes: Buffer) => {
|
|
222
|
+
writeFileSync(path, bytes, { flag: "wx", mode: 0o600 });
|
|
223
|
+
});
|
|
224
|
+
const hardenPath = deps.hardenPath ?? ((path: string) => { hardenSecretPath(path, { required: true }); });
|
|
225
|
+
const inspect = deps.inspect ?? ((path: string) => lstatSync(path));
|
|
226
|
+
const removeStageDir = deps.removeStageDir ?? ((path: string) => { rmdirSync(path); });
|
|
227
|
+
|
|
228
|
+
const stageDir = createStageDir();
|
|
229
|
+
const files: string[] = [];
|
|
230
|
+
const cleanup = (): void => {
|
|
231
|
+
let failure: unknown;
|
|
232
|
+
for (const file of files.splice(0)) {
|
|
233
|
+
try {
|
|
234
|
+
unlinkSync(file);
|
|
235
|
+
forgetEphemeralSecretPath(file);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") forgetEphemeralSecretPath(file);
|
|
238
|
+
else if (failure === undefined) failure = error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
removeStageDir(stageDir);
|
|
243
|
+
forgetEphemeralSecretDir(stageDir);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") forgetEphemeralSecretDir(stageDir);
|
|
246
|
+
else if (failure) throw new AggregateError([failure, error], "Elevated Task Scheduler staging cleanup failed.");
|
|
247
|
+
else failure = error;
|
|
248
|
+
}
|
|
249
|
+
if (failure) throw failure;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
|
|
254
|
+
const dirStats = inspect(stageDir);
|
|
255
|
+
if (dirStats.isSymbolicLink() || !dirStats.isDirectory()) {
|
|
256
|
+
throw new Error(`Refusing to stage an elevated Task Scheduler payload under a redirected path: ${stageDir}`);
|
|
257
|
+
}
|
|
258
|
+
hardenDir(stageDir);
|
|
259
|
+
const stage = (name: string, value: string): StagedWindowsTaskXml => {
|
|
260
|
+
const path = join(stageDir, name);
|
|
261
|
+
const bytes = Buffer.from(value, "utf16le");
|
|
262
|
+
writePayload(path, bytes);
|
|
263
|
+
files.push(path);
|
|
264
|
+
const stats = inspect(path);
|
|
265
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
266
|
+
throw new Error(`Refusing to stage an elevated Task Scheduler payload through a redirected path: ${path}`);
|
|
267
|
+
}
|
|
268
|
+
hardenPath(path);
|
|
269
|
+
return { path, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
xml: stage("register.xml", xml),
|
|
273
|
+
...(expectedExistingXml === undefined
|
|
274
|
+
? {}
|
|
275
|
+
: { expectedExisting: stage("expected.xml", expectedExistingXml) }),
|
|
276
|
+
cleanup,
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
try {
|
|
280
|
+
cleanup();
|
|
281
|
+
} catch (cleanupError) {
|
|
282
|
+
throw new AggregateError(
|
|
283
|
+
[error, cleanupError],
|
|
284
|
+
"Elevated Task Scheduler staging failed and could not be cleaned up.",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Turn an elevated registration exit code into something an operator can act on.
|
|
293
|
+
*
|
|
294
|
+
* The elevated process runs hidden, so nothing it writes survives; only the exit code
|
|
295
|
+
* crosses back. That makes an unexplained code the whole user-facing error, which is
|
|
296
|
+
* exactly what made the ENAMETOOLONG in #4692 expensive to diagnose. Staging introduces
|
|
297
|
+
* one new failure of its own — the payload is readable only by the account that created
|
|
298
|
+
* it, so an elevation answered with a different administrator's credentials cannot open
|
|
299
|
+
* it — and that one gets named along with its remedy rather than surfacing as a number.
|
|
300
|
+
*/
|
|
301
|
+
export function describeElevatedRegistrationFailure(
|
|
302
|
+
failureLabel: string,
|
|
303
|
+
exitCode: number,
|
|
304
|
+
stageDir: string,
|
|
305
|
+
): string {
|
|
306
|
+
if (exitCode === OCX_ELEVATED_STAGING_UNREADABLE) {
|
|
307
|
+
return `${failureLabel}: the elevated process could not read the staged task definition in `
|
|
308
|
+
+ `${stageDir}. That directory is readable only by the account that staged it, so this `
|
|
309
|
+
+ "happens when the UAC prompt was answered with a different administrator account. "
|
|
310
|
+
+ "Approve the prompt as the signed-in user, or run the command again from a session "
|
|
311
|
+
+ "already elevated as that user.";
|
|
312
|
+
}
|
|
313
|
+
return `${failureLabel} with exit code ${exitCode}.`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Stage, elevate, and clean up — on every exit, including UAC cancellation and a
|
|
318
|
+
* synchronous spawn failure.
|
|
319
|
+
*
|
|
320
|
+
* A cleanup failure never replaces the registration failure it followed: an operator
|
|
321
|
+
* told only that a temp directory could not be removed would have no idea the task was
|
|
322
|
+
* never registered.
|
|
323
|
+
*/
|
|
324
|
+
async function runStagedElevatedSchedulerRegistration(
|
|
325
|
+
taskName: string,
|
|
326
|
+
xml: string,
|
|
327
|
+
replace: boolean,
|
|
328
|
+
expectedExistingXml: string | undefined,
|
|
329
|
+
failureLabel: string,
|
|
330
|
+
): Promise<void> {
|
|
331
|
+
const staged = stageElevatedSchedulerRegistration(xml, expectedExistingXml);
|
|
332
|
+
let failure: unknown;
|
|
333
|
+
try {
|
|
334
|
+
const exitCode = await runWindowsElevatedScheduledTaskRegistration(
|
|
335
|
+
taskName,
|
|
336
|
+
staged.xml,
|
|
337
|
+
replace,
|
|
338
|
+
staged.expectedExisting,
|
|
339
|
+
);
|
|
340
|
+
if (exitCode !== 0) {
|
|
341
|
+
failure = new Error(describeElevatedRegistrationFailure(failureLabel, exitCode, dirname(staged.xml.path)));
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
failure = error;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
staged.cleanup();
|
|
348
|
+
} catch (cleanupError) {
|
|
349
|
+
if (failure) {
|
|
350
|
+
throw new AggregateError(
|
|
351
|
+
[failure, cleanupError],
|
|
352
|
+
"Elevated Task Scheduler registration failed and its staging could not be cleaned up.",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
throw cleanupError;
|
|
356
|
+
}
|
|
357
|
+
if (failure) throw failure;
|
|
358
|
+
}
|
|
359
|
+
|
|
168
360
|
export function stageWindowsSchedulerRegistrationXml(
|
|
169
361
|
attemptNonce: string,
|
|
170
362
|
deps: WindowsSchedulerRegistrationStageDeps = {},
|
|
@@ -294,7 +486,8 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
294
486
|
throw error;
|
|
295
487
|
}
|
|
296
488
|
// Register from the captured XML string inside the elevated process. Another
|
|
297
|
-
// same-user process can mutate its own temp files,
|
|
489
|
+
// same-user process can mutate its own temp files, so the captured bytes are staged
|
|
490
|
+
// privately and the elevated script verifies their digest before registering them.
|
|
298
491
|
// UAC can remain open for an arbitrary amount of time. Recheck the captured predecessor
|
|
299
492
|
// before launch; the elevated helper repeats the same check after consent and before Force.
|
|
300
493
|
assertReplacementPrecondition();
|
|
@@ -303,15 +496,13 @@ export async function registerFreshWindowsSchedulerTask(
|
|
|
303
496
|
xml: string,
|
|
304
497
|
replaceCurrent: boolean,
|
|
305
498
|
previousXml?: string,
|
|
306
|
-
) =>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (exitCode !== 0) throw new Error(`Background service install failed with exit code ${exitCode}.`);
|
|
314
|
-
});
|
|
499
|
+
) => runStagedElevatedSchedulerRegistration(
|
|
500
|
+
taskName,
|
|
501
|
+
xml,
|
|
502
|
+
replaceCurrent,
|
|
503
|
+
previousXml,
|
|
504
|
+
"Background service install failed",
|
|
505
|
+
));
|
|
315
506
|
await elevate(TASK, expectedXml, replace, expectedExistingXml);
|
|
316
507
|
}
|
|
317
508
|
|
|
@@ -501,10 +692,13 @@ export async function restoreWindowsSchedulerTaskIfAbsent(registeredXml: string)
|
|
|
501
692
|
) {
|
|
502
693
|
throw error;
|
|
503
694
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
695
|
+
await runStagedElevatedSchedulerRegistration(
|
|
696
|
+
TASK,
|
|
697
|
+
registeredXml,
|
|
698
|
+
false,
|
|
699
|
+
undefined,
|
|
700
|
+
"Task Scheduler rollback failed",
|
|
701
|
+
);
|
|
508
702
|
}
|
|
509
703
|
const recoveredXml = statusWindowsXml();
|
|
510
704
|
if (!windowsSchedulerRegistrationMatchesSnapshot(recoveredXml, registeredXml)) {
|
|
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { ELEVATION_REQUEST_TIMEOUT_MS, OCX_ELEVATED_PROTOCOL_FAILED, raceWithTimeout, resolveTrustedWindowsSchtasksExe, startElevatedSchtasksCreateAndRun, runWindowsElevated, toWindowsSchtasksError, WindowsElevationError, type ElevatedSchedulerOutcome, type ElevatedSchtasksCreateAndRunExecution, type ElevatedSchtasksCreateAndRunResult } from "../lib/windows-elevation";
|
|
7
7
|
import { statusWinswRaw } from "../lib/winsw";
|
|
8
|
+
import { decodeWindowsTextBytes, type WindowsTextDecodeOptions } from "../lib/windows-text";
|
|
8
9
|
import { isTestHomeGuardArmed } from "../lib/test-home-guard";
|
|
9
10
|
import { TASK, windowsServiceScriptPath, windowsLauncherVbsPath, windowsTaskXmlPath, writeServiceInstallState } from "./state";
|
|
10
11
|
import { buildWindowsSchtasksCreateArgs, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy } from "./windows-taskxml";
|
|
@@ -16,28 +17,34 @@ import { WINSW_SERVICE_ID } from "../lib/winsw";
|
|
|
16
17
|
* Decode schtasks stdout. `/query /xml` emits UTF-16LE (often with BOM) because the
|
|
17
18
|
* registered task document is UTF-16; reading that as UTF-8 makes every health check
|
|
18
19
|
* fail ("registration present but unhealthy") and rolls back a successful elevated create.
|
|
20
|
+
*
|
|
21
|
+
* Redirected output is NOT always UTF-16. Its encoding follows the console output code
|
|
22
|
+
* page of the spawning process tree rather than the XML declaration, so on a zh-CN host
|
|
23
|
+
* (ACP/OEMCP 936) the bytes are GBK — including inside a no-console background service.
|
|
24
|
+
* Decoding those as UTF-8 turned a CJK account name in
|
|
25
|
+
* `<SessionStateChangeTrigger><UserId>` into U+FFFD, the trigger scope then failed to
|
|
26
|
+
* match the correctly resolved `[SID, MACHINE\<name>]`, and `ocx service repair`
|
|
27
|
+
* aborted at its recognition gate on a registration OpenCodex had itself created. The
|
|
28
|
+
* same mojibake rolled back fresh installs at post-create verification (#4691).
|
|
29
|
+
*
|
|
30
|
+
* The fix is entirely in byte decoding, before any XML is parsed. The trigger scope stays
|
|
31
|
+
* an exact identity comparison: forgiving a replacement character there would let two
|
|
32
|
+
* different non-ASCII accounts collapse to the same value, which is a worse failure than
|
|
33
|
+
* the refusal it replaces.
|
|
34
|
+
*
|
|
35
|
+
* `decodeWindowsTextBytes` is the decoder this project already built for this class
|
|
36
|
+
* (UTF-16 -> strict UTF-8 -> the locale's legacy code page), and it already fixed the
|
|
37
|
+
* sibling `whoami`/PowerShell decode in `src/lib/windows-user-principal.ts` (#2914, and
|
|
38
|
+
* #722 for CP949). This call site was the last one still ending in a lossy UTF-8 decode.
|
|
19
39
|
*/
|
|
20
|
-
export function decodeSchtasksOutput(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (bomUtf16Le || looksUtf16Le) {
|
|
29
|
-
return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
|
|
30
|
-
}
|
|
31
|
-
if (bomUtf16Be) {
|
|
32
|
-
// Swap pairs then decode as utf16le.
|
|
33
|
-
const swapped = Buffer.alloc(buffer.length - 2);
|
|
34
|
-
for (let i = 2; i + 1 < buffer.length; i += 2) {
|
|
35
|
-
swapped[i - 2] = buffer[i + 1]!;
|
|
36
|
-
swapped[i - 1] = buffer[i]!;
|
|
37
|
-
}
|
|
38
|
-
return swapped.toString("utf16le").trim();
|
|
39
|
-
}
|
|
40
|
-
return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
|
|
40
|
+
export function decodeSchtasksOutput(
|
|
41
|
+
buffer: Buffer,
|
|
42
|
+
options: WindowsTextDecodeOptions = {},
|
|
43
|
+
): string {
|
|
44
|
+
// `options` exists so a test can pin the code page; every production call passes the
|
|
45
|
+
// buffer alone and uses the active Intl locale, which is available to a service with no
|
|
46
|
+
// console because the selection reads the process locale rather than a console handle.
|
|
47
|
+
return decodeWindowsTextBytes(buffer, options);
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
function runFile(file: string, args: string[]): string {
|
package/src/service.ts
CHANGED
|
@@ -19,7 +19,7 @@ export { decodeSchtasksOutput, setQuerySchtasksForTests, formatWindowsSchedulerS
|
|
|
19
19
|
export type { WindowsSchedulerXmlState } from "./service/windows-taskxml";
|
|
20
20
|
export { buildWindowsServiceScript, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsLauncherVbs, buildWindowsTaskXml, buildWindowsTaskXmlDocument, windowsTaskRegistrationOwnedByAttempt, windowsTaskRegistrationHealthy, readWindowsSchedulerXmlState } from "./service/windows-taskxml";
|
|
21
21
|
export type { WindowsSchedulerRegistrationStageDeps, FreshWindowsSchedulerRegistrationDeps, RemoveNativeWindowsServiceDeps } from "./service/windows-ops";
|
|
22
|
-
export { windowsListenPort, winswListenPort, writeServiceDefinitionFile, definitionCarriesCredential, stageWindowsSchedulerRegistrationXml, registerFreshWindowsSchedulerTask, removeNativeWindowsServiceForScheduler, assertWindowsNativeServiceAccountSupported, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, classifyWindowsServiceStop } from "./service/windows-ops";
|
|
22
|
+
export { windowsListenPort, winswListenPort, writeServiceDefinitionFile, definitionCarriesCredential, stageWindowsSchedulerRegistrationXml, stageElevatedSchedulerRegistration, describeElevatedRegistrationFailure, registerFreshWindowsSchedulerTask, removeNativeWindowsServiceForScheduler, assertWindowsNativeServiceAccountSupported, isWindowsSchedulerEndBenign, stopWindows, stopWindowsChecked, classifyWindowsServiceStop } from "./service/windows-ops";
|
|
23
23
|
export type { ServiceRepairVerb, RepairServiceDeps } from "./service/repair";
|
|
24
24
|
export { repairService } from "./service/repair";
|
|
25
25
|
export type { ServiceInstallPreparationDeps, FreshWindowsSchedulerInstallDeps, ServiceStopOutcome, ServiceUninstallOutcome } from "./service/orchestration";
|
package/src/types/config.ts
CHANGED
|
@@ -882,6 +882,36 @@ export interface OcxConfig {
|
|
|
882
882
|
* binding under either setting -- neither is a cache-affinity preference.
|
|
883
883
|
*/
|
|
884
884
|
cacheAffinity?: boolean;
|
|
885
|
+
/**
|
|
886
|
+
* Operator-declared quota domains: groups of credential ids that demonstrably share
|
|
887
|
+
* one upstream usage limit (#4546, wp6). Members of one group count once toward
|
|
888
|
+
* available capacity, and a quota refusal inside a group is never answered by
|
|
889
|
+
* rotating to another member -- the limit is the same, so the move would pay a cold
|
|
890
|
+
* prefix for zero new capacity.
|
|
891
|
+
*
|
|
892
|
+
* Declared groups speak only to quota. Sharing a usage limit says nothing about
|
|
893
|
+
* prompt-cache compatibility, which keeps its own provider-documented domain.
|
|
894
|
+
* Absent or empty means no declared grouping, so an unconfigured install behaves
|
|
895
|
+
* exactly as before.
|
|
896
|
+
*
|
|
897
|
+
* A declaration has to mean exactly one thing, so the config rejects the spellings
|
|
898
|
+
* that could mean two. Credential ids are provider-scoped elsewhere (the auth store
|
|
899
|
+
* keys an account by provider and id), so each member is written
|
|
900
|
+
* `"<provider>:<credential-id>"` -- a bare `"acct-1"` names one credential per
|
|
901
|
+
* provider and would merge unrelated domains. The provider segment is matched
|
|
902
|
+
* case-insensitively through the usual aliases, so `chatgpt:` and `codex:` both mean
|
|
903
|
+
* OpenAI. Group ids must be unique, `credentials` must be non-empty, and a credential
|
|
904
|
+
* may belong to at most one group; a declaration that breaks any of those is rejected
|
|
905
|
+
* on write and dropped with a warning on load, never resolved by list order.
|
|
906
|
+
*/
|
|
907
|
+
credentialGroups?: Array<{
|
|
908
|
+
/** Operator-chosen group identifier; only equality matters. */
|
|
909
|
+
id: string;
|
|
910
|
+
/** Provider-qualified credential ids (`"<provider>:<credential-id>"`), non-empty. */
|
|
911
|
+
credentials: string[];
|
|
912
|
+
/** Free-text provenance note for the operator's own records. */
|
|
913
|
+
note?: string;
|
|
914
|
+
}>;
|
|
885
915
|
};
|
|
886
916
|
/** Active pool account id for next session. undefined = main (passthrough as-is). */
|
|
887
917
|
activeCodexAccountId?: string;
|
|
@@ -981,6 +1011,7 @@ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilizatio
|
|
|
981
1011
|
|
|
982
1012
|
export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window";
|
|
983
1013
|
export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
|
|
1014
|
+
export type OcxComboDefaultEffortMode = "fallback" | "force";
|
|
984
1015
|
|
|
985
1016
|
/**
|
|
986
1017
|
* How a combo derives the reasoning ladder it publishes to the picker.
|
|
@@ -1016,8 +1047,10 @@ export interface OcxComboConfig {
|
|
|
1016
1047
|
cooldownMs?: number;
|
|
1017
1048
|
/** Maximum wait for an eligible target cooldown to expire before failing closed. Default 0; range 0..600000, per selection attempt. */
|
|
1018
1049
|
waitForCooldownMs?: number;
|
|
1019
|
-
/** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */
|
|
1050
|
+
/** Used as a fallback when the client omits reasoning.effort, or as an override in `force` mode. null/omitted leaves the target default unchanged. */
|
|
1020
1051
|
defaultEffort?: OcxComboDefaultEffort | null;
|
|
1052
|
+
/** `force` makes the combo default override a valid client effort. Omitted / `fallback` preserves client precedence. */
|
|
1053
|
+
defaultEffortMode?: OcxComboDefaultEffortMode;
|
|
1021
1054
|
/**
|
|
1022
1055
|
* Picker-ladder derivation policy. Omitted / `"strict"` keeps the legacy rule where an
|
|
1023
1056
|
* explicitly empty target ladder suppresses the whole combo's effort control.
|
package/src/types/request.ts
CHANGED
|
@@ -154,9 +154,11 @@ export interface OcxAssistantMessage {
|
|
|
154
154
|
model?: string;
|
|
155
155
|
timestamp: number;
|
|
156
156
|
/**
|
|
157
|
-
* Kiro
|
|
158
|
-
*
|
|
159
|
-
*
|
|
157
|
+
* Kiro's encrypted reasoning blob for THIS assistant turn — the opaque value from the turn's
|
|
158
|
+
* `reasoningContentEvent` (`signature` for the GPT-5.6 family, `redactedContent` for the base64
|
|
159
|
+
* shape), tagged with the wire field it must be replayed on (see kiro/reasoning.ts). Kiro
|
|
160
|
+
* replays it to preserve model reasoning across turns. Provider-specific and unrenderable, so it
|
|
161
|
+
* rides the message rather than a content part: any other adapter simply ignores it.
|
|
160
162
|
*/
|
|
161
163
|
kiroRedactedReasoning?: string;
|
|
162
164
|
}
|
|
@@ -317,8 +319,9 @@ export type AdapterEvent =
|
|
|
317
319
|
// opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400.
|
|
318
320
|
| { type: "thinking_signature"; signature: string }
|
|
319
321
|
| { type: "redacted_thinking"; data: string }
|
|
320
|
-
// Kiro reasoning round-trip: the encrypted
|
|
321
|
-
// Never rendered — it only rides the reasoning item's envelope
|
|
322
|
+
// Kiro reasoning round-trip: the encrypted reasoning blob for the CURRENT assistant turn, tagged
|
|
323
|
+
// with the wire field it arrived on. Never rendered — it only rides the reasoning item's envelope
|
|
324
|
+
// so the next request can replay it verbatim.
|
|
322
325
|
| { type: "kiro_redacted_reasoning"; data: string }
|
|
323
326
|
| { type: "reasoning_raw_delta"; text: string }
|
|
324
327
|
| { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata }
|
package/src/types/tools.ts
CHANGED
|
@@ -67,6 +67,30 @@ const CODE_MODE_HELPER_TOOL_NAMES = [
|
|
|
67
67
|
*/
|
|
68
68
|
export const CODE_MODE_EXEC_TOOL_NAME = "exec";
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Spellings that may never be MANUFACTURED as a bare alias for a namespaced tool.
|
|
72
|
+
*
|
|
73
|
+
* A bare alias is an ordinary compatibility affordance -- providers echo a namespaced tool
|
|
74
|
+
* without its prefix, and restoring the identity needs the bare spelling registered. For these
|
|
75
|
+
* six it is also an authorization decision, because a declared-name set is what
|
|
76
|
+
* `normalizeDeclaredToolName` and `declaresCodeModeExec` read: bare `exec` turns nested-helper
|
|
77
|
+
* normalization on for a catalog that never declared the shell, bare `exec_command` or
|
|
78
|
+
* `shell_command` turns it off for one that did, and the rest are accepted as declared calls the
|
|
79
|
+
* caller only ever authorized under a namespace.
|
|
80
|
+
*
|
|
81
|
+
* This is a property of the SPELLING, not of the namespace that declared it and not of the reason
|
|
82
|
+
* the alias was being added. It lives here, beside the names it protects, because every site that
|
|
83
|
+
* builds a declared-name set has to apply the same list -- the two that kept their own copies each
|
|
84
|
+
* drifted, once to a single namespace and once to a single name.
|
|
85
|
+
*
|
|
86
|
+
* A genuine namespace-free declaration is NOT covered: that is the caller declaring the tool, not
|
|
87
|
+
* a namespace being discarded to synthesize a bare name.
|
|
88
|
+
*/
|
|
89
|
+
export const NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES: ReadonlySet<string> = new Set<string>([
|
|
90
|
+
CODE_MODE_EXEC_TOOL_NAME,
|
|
91
|
+
...CODE_MODE_HELPER_TOOL_NAMES,
|
|
92
|
+
]);
|
|
93
|
+
|
|
70
94
|
/**
|
|
71
95
|
* Normalizes provider-emitted tool names against declared tool catalogs.
|
|
72
96
|
*
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
isAllowedToolChoice,
|
|
17
17
|
toolChoiceToolPredicate,
|
|
18
18
|
declaresCodeModeExec,
|
|
19
|
+
NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
|
|
19
20
|
} from "./types/tools";
|
|
20
21
|
|
|
21
22
|
export type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire";
|
|
@@ -74,6 +75,7 @@ export type {
|
|
|
74
75
|
OcxAccountPoolQuotaWindow,
|
|
75
76
|
OcxComboStrategy,
|
|
76
77
|
OcxComboDefaultEffort,
|
|
78
|
+
OcxComboDefaultEffortMode,
|
|
77
79
|
OcxComboReasoningEffortMode,
|
|
78
80
|
OcxComboTarget,
|
|
79
81
|
OcxComboConfig,
|
package/src/update/index.ts
CHANGED
|
@@ -481,6 +481,16 @@ export async function runUpdate(): Promise<void> {
|
|
|
481
481
|
" After the update: close the Codex app, run 'ocx doctor', then run 'ocx stop' once to retry.",
|
|
482
482
|
);
|
|
483
483
|
}
|
|
484
|
+
if (decision.reason === "history-deferred") {
|
|
485
|
+
// Not the same warning: nothing was restored here. Saying "history metadata is
|
|
486
|
+
// incomplete" would imply config and catalog came back, and an operator who
|
|
487
|
+
// believed that would not know a teardown is still owed.
|
|
488
|
+
console.warn(
|
|
489
|
+
"⚠️ The shared teardown was refused by the Codex history preflight and restored nothing.\n" +
|
|
490
|
+
" Config, catalog, history and provenance were preserved, and the teardown receipt was kept.\n" +
|
|
491
|
+
" The proxy is down, so the update continues; close the Codex app and run 'ocx stop' once afterwards to finish the restore.",
|
|
492
|
+
);
|
|
493
|
+
}
|
|
484
494
|
}
|
|
485
495
|
|
|
486
496
|
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
@@ -13,3 +13,22 @@
|
|
|
13
13
|
* the child's code faithfully enough to propagate the confusion.
|
|
14
14
|
*/
|
|
15
15
|
export const STOP_HISTORY_INCOMPLETE_EXIT_CODE = 79;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The exit code `ocx stop` uses to say "the proxy is down and the shared teardown was
|
|
19
|
+
* refused before it changed anything" (#4718).
|
|
20
|
+
*
|
|
21
|
+
* This is NOT 79. Seventy-nine means the teardown ran: config and catalog came back to
|
|
22
|
+
* their native values and only the Codex history metadata could not be finalized, so the
|
|
23
|
+
* receipt is discharged. Eighty means the Codex history preflight refused FIRST, so
|
|
24
|
+
* config, catalog, history and provenance are all untouched, the client is still routed
|
|
25
|
+
* at the proxy that just stopped, and the receipt stays outstanding for a later stop.
|
|
26
|
+
*
|
|
27
|
+
* Collapsing the two would be a data-loss bug in the quiet direction: a caller reading 79
|
|
28
|
+
* discharges an obligation that was never performed.
|
|
29
|
+
*
|
|
30
|
+
* Eighty sits in the same unoccupied window as 79 — above `sysexits.h` (64-78), below
|
|
31
|
+
* `128 + signal`, and outside 0, 1, 2, 4, 64 and 130, which are the codes this CLI and its
|
|
32
|
+
* dispatcher already emit.
|
|
33
|
+
*/
|
|
34
|
+
export const STOP_HISTORY_DEFERRED_EXIT_CODE = 80;
|
|
@@ -6,5 +6,5 @@ export declare function decidePostStopUpdate(input: {
|
|
|
6
6
|
teardownOutstanding?: boolean;
|
|
7
7
|
}): {
|
|
8
8
|
proceed: boolean;
|
|
9
|
-
reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "ok";
|
|
9
|
+
reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "history-deferred" | "ok";
|
|
10
10
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
1
|
+
import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* May an update replace package files after `ocx stop` returned?
|
|
@@ -22,13 +22,22 @@ import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
|
22
22
|
* absence, and replacing files under a live server leaves it running a mix of old and
|
|
23
23
|
* new modules.
|
|
24
24
|
* - `ok` / `history-only` — proceed; the second also prints the manifest warning.
|
|
25
|
+
* - `history-deferred` — proceed; the stop is down but restored nothing, because the
|
|
26
|
+
* Codex history preflight refused first (#4718). The receipts it kept are the ONLY
|
|
27
|
+
* obligations it left, which the child proved before choosing this status, so
|
|
28
|
+
* `teardownOutstanding` seeing them is expected rather than disqualifying. Every other
|
|
29
|
+
* gate still applies: runtime records and a live or unreadable endpoint abort exactly
|
|
30
|
+
* as they do for a clean stop, because package replacement under a live server is the
|
|
31
|
+
* danger this function exists to prevent, and a history refusal says nothing about it.
|
|
25
32
|
*/
|
|
26
33
|
export function decidePostStopUpdate({ status, hasRuntimeState, liveness, teardownOutstanding = false }) {
|
|
27
34
|
const historyOnly = status === STOP_HISTORY_INCOMPLETE_EXIT_CODE;
|
|
28
|
-
|
|
35
|
+
const historyDeferred = status === STOP_HISTORY_DEFERRED_EXIT_CODE;
|
|
36
|
+
if (status !== 0 && !historyOnly && !historyDeferred) return { proceed: false, reason: "stop-failed" };
|
|
29
37
|
if (hasRuntimeState) return { proceed: false, reason: "runtime-state" };
|
|
30
|
-
if (teardownOutstanding) return { proceed: false, reason: "teardown-outstanding" };
|
|
38
|
+
if (teardownOutstanding && !historyDeferred) return { proceed: false, reason: "teardown-outstanding" };
|
|
31
39
|
if (liveness === "live") return { proceed: false, reason: "proxy-live" };
|
|
32
40
|
if (liveness !== "dead") return { proceed: false, reason: "proxy-unknown" };
|
|
41
|
+
if (historyDeferred) return { proceed: true, reason: "history-deferred" };
|
|
33
42
|
return { proceed: true, reason: historyOnly ? "history-only" : "ok" };
|
|
34
43
|
}
|