@bitkyc08/opencodex 2.88.0 → 2.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
package/src/cli/combo.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeJevQuotaTiers } from "../combos/jev-quota-config";
|
|
1
2
|
import { JEV_DECISION_TIMEOUT_MAX_MS, JEV_DECISION_TIMEOUT_MIN_MS } from "../combos/types";
|
|
2
3
|
import {
|
|
3
4
|
CliUsageError,
|
|
@@ -24,8 +25,10 @@ const USAGE = `Usage:
|
|
|
24
25
|
[--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->]
|
|
25
26
|
(force overrides valid client effort and can increase cost/latency) [--alias <name|->]
|
|
26
27
|
[--native-alias [on|off]] [--allow-metered-targets <on|off>] [--display-name <label|->]
|
|
28
|
+
[--description <text|->]
|
|
27
29
|
[--image-input <auto|disabled>] [--reasoning-effort-mode <strict|adaptive>]
|
|
28
30
|
[--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->]
|
|
31
|
+
[--decision-quota-signals <on|off|->] [--decision-quota-tiers <JSON|->]
|
|
29
32
|
(jev only; the provider must be a configured jev-decision row)
|
|
30
33
|
[--rename-from <id>] [--json]
|
|
31
34
|
ocx combo stats <id> [--range <7d|30d|all>] [--json]
|
|
@@ -80,6 +83,7 @@ async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
80
83
|
printData(combo, wantsJson);
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/** Handle `ocx combo set`: parse the combo id and flags (including `--decision-quota-signals` and `--decision-quota-tiers`), then create or update the combo through the management API. */
|
|
83
87
|
async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
84
88
|
const args = [...argv];
|
|
85
89
|
const id = args.shift()?.trim();
|
|
@@ -118,6 +122,7 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
118
122
|
const alias = takeOption(args, "--alias");
|
|
119
123
|
|
|
120
124
|
const displayName = takeOption(args, "--display-name");
|
|
125
|
+
const description = takeOption(args, "--description");
|
|
121
126
|
const decisionProvider = takeOption(args, "--decision-provider");
|
|
122
127
|
const decisionModel = takeOption(args, "--decision-model");
|
|
123
128
|
if (decisionModel !== undefined && decisionModel !== "-" && decisionProvider !== undefined && decisionProvider !== "-") {
|
|
@@ -129,6 +134,13 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
129
134
|
if (decisionProvider !== undefined && decisionProvider !== "-" && strategy !== "jev") {
|
|
130
135
|
throw new CliUsageError("--decision-provider applies only to the jev strategy", USAGE);
|
|
131
136
|
}
|
|
137
|
+
const quotaSignals = takeOption(args, "--decision-quota-signals");
|
|
138
|
+
const quotaTiers = takeOption(args, "--decision-quota-tiers");
|
|
139
|
+
if (quotaSignals !== undefined && !["on", "off", "-"].includes(quotaSignals)) throw new CliUsageError("--decision-quota-signals must be on, off or -", USAGE);
|
|
140
|
+
let parsedQuotaTiers: ReturnType<typeof normalizeJevQuotaTiers> | null;
|
|
141
|
+
try { parsedQuotaTiers = quotaTiers === undefined || quotaTiers === "-" ? null : normalizeJevQuotaTiers(JSON.parse(quotaTiers)); } catch { throw new CliUsageError("invalid --decision-quota-tiers JSON", USAGE); }
|
|
142
|
+
if (quotaTiers !== undefined && quotaTiers !== "-" && !parsedQuotaTiers) throw new CliUsageError("quota tiers must be finite 0..100 and strictly ascending", USAGE);
|
|
143
|
+
if (strategy !== "jev" && (quotaSignals !== undefined && quotaSignals !== "-" || quotaTiers !== undefined && quotaTiers !== "-")) throw new CliUsageError("quota settings apply only to jev", USAGE);
|
|
132
144
|
const decisionTimeout = takeOption(args, "--decision-timeout");
|
|
133
145
|
let decisionTimeoutMs: number | null | undefined;
|
|
134
146
|
if (decisionTimeout !== undefined) {
|
|
@@ -164,15 +176,20 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
164
176
|
if (additional.imageInput !== undefined) combo.imageInput = additional.imageInput;
|
|
165
177
|
if (additional.reasoningEffortMode !== undefined) combo.reasoningEffortMode = additional.reasoningEffortMode;
|
|
166
178
|
if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
|
|
179
|
+
if (description !== undefined) combo.description = description === "-" ? "" : description;
|
|
167
180
|
if (decisionProvider !== undefined) combo.decisionProvider = decisionProvider === "-" ? null : decisionProvider;
|
|
168
181
|
if (decisionModel !== undefined) combo.decisionModel = decisionModel === "-" ? null : decisionModel;
|
|
169
182
|
if (decisionProvider !== undefined && decisionProvider !== "-") combo.decisionModel = null;
|
|
170
183
|
if (decisionModel !== undefined && decisionModel !== "-") combo.decisionProvider = null;
|
|
171
184
|
if (decisionTimeoutMs !== undefined) combo.decisionTimeoutMs = decisionTimeoutMs;
|
|
185
|
+
if (quotaSignals !== undefined) combo.decisionQuotaSignals = quotaSignals === "-" ? null : quotaSignals === "on";
|
|
186
|
+
if (quotaTiers !== undefined) combo.decisionQuotaTiers = parsedQuotaTiers;
|
|
172
187
|
if (strategy !== "jev") {
|
|
173
188
|
delete combo.decisionProvider;
|
|
174
189
|
delete combo.decisionModel;
|
|
175
190
|
delete combo.decisionTimeoutMs;
|
|
191
|
+
delete combo.decisionQuotaSignals;
|
|
192
|
+
delete combo.decisionQuotaTiers;
|
|
176
193
|
}
|
|
177
194
|
const current = partialCurrent ?? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", { redirect: "error" }, deps);
|
|
178
195
|
const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
|
|
@@ -241,12 +241,12 @@ function consentedDesktopBundle(home: string, installId: string, run: Execute):
|
|
|
241
241
|
const plistPath = realpathSync(join(home, "Library", "LaunchAgents", `${DESKTOP_PLIST_LABEL}.plist`));
|
|
242
242
|
const registration = JSON.parse(
|
|
243
243
|
run("/usr/bin/plutil", ["-convert", "json", "-o", "-", plistPath], 3_000),
|
|
244
|
-
) as { Label?: unknown; ProgramArguments?: unknown; Program?: unknown };
|
|
244
|
+
) as { Label?: unknown; RunAtLoad?: unknown; ProgramArguments?: unknown; Program?: unknown };
|
|
245
245
|
const args = registration.ProgramArguments;
|
|
246
246
|
// The shapes auto-launch writes: [program] or [program, "--autostart"]; see the migration
|
|
247
247
|
// rules in desktop/src-tauri/src/login_registration.rs, which classify anything else Leave.
|
|
248
248
|
// This is a macOS plist path, regardless of the host running an injected launch probe.
|
|
249
|
-
if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args)
|
|
249
|
+
if (registration.Label !== DESKTOP_PLIST_LABEL || registration.RunAtLoad !== true || !Array.isArray(args)
|
|
250
250
|
|| (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" || !posix.isAbsolute(args[0])
|
|
251
251
|
|| (args.length === 2 && args[1] !== "--autostart")
|
|
252
252
|
|| (registration.Program !== undefined && registration.Program !== args[0])) return null;
|
package/src/cli/help.ts
CHANGED
|
@@ -79,7 +79,7 @@ Usage:
|
|
|
79
79
|
ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
|
|
80
80
|
ocx combo <sub> Combo routing strategies and failover
|
|
81
81
|
ocx agent <sub> Subagents, injection, effort caps, and sidecars
|
|
82
|
-
ocx message <sub>
|
|
82
|
+
ocx message <sub> Codex queued messages, explicit SSH peers and foreground ownership
|
|
83
83
|
ocx effort [sub] Inspect and configure reasoning effort caps and defaults
|
|
84
84
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
85
85
|
ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
|
|
@@ -2,13 +2,16 @@ import { MessageBudget } from "../messaging/budget";
|
|
|
2
2
|
import { readMessageInput } from "../messaging/input";
|
|
3
3
|
import { localSessions, messageFailure, sendLocalMessage } from "../messaging/send";
|
|
4
4
|
import { parseMessageArgs } from "./message-args";
|
|
5
|
+
import { parseRemoteMessageArgs } from "./message-remote-args";
|
|
5
6
|
import { terminalSafeText } from "./runtime-api";
|
|
6
7
|
|
|
7
8
|
/** Run one local messaging operation after pure syntax validation, then release owned resources. */
|
|
8
9
|
export async function runMessageCommand(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
|
10
|
+
const remote = parseRemoteMessageArgs(argv);
|
|
11
|
+
if (remote) return (await import("./message-remote-command")).runRemoteMessageCommand(remote, env);
|
|
9
12
|
const args = parseMessageArgs(argv);
|
|
10
13
|
if (!args) {
|
|
11
|
-
console.error("Usage: ocx message sessions [--json] | send (--thread <uuid> | --name <exact-name>) --stdin [--kind request|response|notification] [--in-reply-to <uuid>] [--json]");
|
|
14
|
+
console.error("Usage: ocx message sessions [--host <peer>] [--json] | send (--thread <uuid> | --name <exact-name>) --stdin [--host <peer>] [--kind request|response|notification] [--in-reply-to <uuid>] [--json] | enable [--port <port>] | disable | status | hosts <probe|add|list|remove|abandon|allow-inbound|deny-inbound> | serve [--host <peer>]...");
|
|
12
15
|
return 64;
|
|
13
16
|
}
|
|
14
17
|
const controller = new AbortController();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { validAlias, validPort, REMOTE_LIMITS } from "../messaging/remote-contract";
|
|
2
|
+
import { isSshAlias } from "../link/ssh-argv";
|
|
3
|
+
import { parseMessageArgs, type MessageArgs } from "./message-args";
|
|
4
|
+
import { isThreadId } from "../messaging/types";
|
|
5
|
+
|
|
6
|
+
export type RemoteMessageArgs = { action: "enable"; port?: number; json: boolean }
|
|
7
|
+
| { action: "disable" | "status" | "hosts-list" | "_control" | "_port"; json: boolean }
|
|
8
|
+
| { action: "serve"; hosts: string[]; json: boolean }
|
|
9
|
+
| { action: "hosts-probe"; ssh: string; json: boolean }
|
|
10
|
+
| { action: "hosts-add"; alias: string; ssh: string; fingerprint: string; json: boolean }
|
|
11
|
+
| { action: "hosts-remove"; host: string; transaction?: string; json: boolean }
|
|
12
|
+
| { action: "hosts-allow-inbound" | "hosts-deny-inbound"; host: string; json: boolean }
|
|
13
|
+
| { action: "hosts-abandon"; transaction: string; json: boolean }
|
|
14
|
+
| { action: "remote-operation"; host: string; local: MessageArgs; json: boolean };
|
|
15
|
+
|
|
16
|
+
/** Pure remote syntax validation precedes state, home selection, listeners or SSH allocation. */
|
|
17
|
+
export function parseRemoteMessageArgs(argv: readonly string[]): RemoteMessageArgs | null {
|
|
18
|
+
let action = argv[0], start = 1, positional: string | undefined;
|
|
19
|
+
if (action === "sessions" || action === "send") {
|
|
20
|
+
const hostIndex = argv.indexOf("--host");
|
|
21
|
+
if (hostIndex < 0 || argv.lastIndexOf("--host") !== hostIndex || !validAlias(argv[hostIndex + 1])) return null;
|
|
22
|
+
const local = parseMessageArgs([...argv.slice(0, hostIndex), ...argv.slice(hostIndex + 2)]);
|
|
23
|
+
return local ? { action: "remote-operation", host: argv[hostIndex + 1]!, local, json: local.json } : null;
|
|
24
|
+
}
|
|
25
|
+
if (action === "hosts") {
|
|
26
|
+
if (!["list", "probe", "add", "remove", "abandon", "allow-inbound", "deny-inbound"].includes(argv[1] ?? "")) return null;
|
|
27
|
+
action = `hosts-${argv[1]}`; start = 2;
|
|
28
|
+
if (["hosts-add", "hosts-remove", "hosts-allow-inbound", "hosts-deny-inbound"].includes(action)) {
|
|
29
|
+
positional = argv[start++]; if (!validAlias(positional)) return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const seen = new Set<string>(), flags: Record<string, string> = {}, hosts: string[] = [];
|
|
33
|
+
for (let index = start; index < argv.length; index++) {
|
|
34
|
+
const flag = argv[index]!;
|
|
35
|
+
if (seen.has(flag) && !(action === "serve" && flag === "--host")) return null;
|
|
36
|
+
seen.add(flag);
|
|
37
|
+
if (flag === "--json") continue;
|
|
38
|
+
const allowed = action === "enable" ? ["--port"] : action === "serve" ? ["--host"]
|
|
39
|
+
: action === "hosts-probe" ? ["--ssh"] : action === "hosts-add" ? ["--ssh", "--fingerprint"]
|
|
40
|
+
: action === "hosts-abandon" || action === "hosts-remove" ? ["--transaction"] : [];
|
|
41
|
+
if (!allowed.includes(flag)) return null;
|
|
42
|
+
const value = argv[++index]; if (!value || value.startsWith("--")) return null;
|
|
43
|
+
flags[flag] = value; if (flag === "--host") hosts.push(value);
|
|
44
|
+
}
|
|
45
|
+
const json = seen.has("--json");
|
|
46
|
+
if (action === "enable") {
|
|
47
|
+
if (flags["--port"] !== undefined && (!/^\d{4,5}$/.test(flags["--port"]) || !validPort(Number(flags["--port"])))) return null;
|
|
48
|
+
return { action, json, ...(flags["--port"] ? { port: Number(flags["--port"]) } : {}) };
|
|
49
|
+
}
|
|
50
|
+
if (action === "serve") return hosts.length <= REMOTE_LIMITS.peers && hosts.every(validAlias)
|
|
51
|
+
&& new Set(hosts).size === hosts.length ? { action, hosts, json } : null;
|
|
52
|
+
if (["disable", "status", "hosts-list", "_control", "_port"].includes(action ?? "")) {
|
|
53
|
+
return { action: action as "disable", json };
|
|
54
|
+
}
|
|
55
|
+
if (action === "hosts-remove") {
|
|
56
|
+
const transaction = flags["--transaction"];
|
|
57
|
+
if (transaction !== undefined && !isThreadId(transaction)) return null;
|
|
58
|
+
return { action, host: positional!, json, ...(transaction !== undefined ? { transaction } : {}) };
|
|
59
|
+
}
|
|
60
|
+
if (action === "hosts-allow-inbound" || action === "hosts-deny-inbound") return { action, host: positional!, json };
|
|
61
|
+
if (action === "hosts-abandon" && isThreadId(flags["--transaction"])) return { action, transaction: flags["--transaction"]!, json };
|
|
62
|
+
if ((action === "hosts-probe" || action === "hosts-add") && flags["--ssh"] && isSshAlias(flags["--ssh"])) {
|
|
63
|
+
if (action === "hosts-probe") return { action, ssh: flags["--ssh"], json };
|
|
64
|
+
if (/^SHA256:[A-Za-z0-9+/=]{1,64}$/.test(flags["--fingerprint"] ?? "")) return {
|
|
65
|
+
action, alias: positional!, ssh: flags["--ssh"], fingerprint: flags["--fingerprint"], json,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getConfigDir } from "../config/paths";
|
|
2
|
+
import { MessageBudget } from "../messaging/budget";
|
|
3
|
+
import { readMessageInput } from "../messaging/input";
|
|
4
|
+
import { RemoteCapacity } from "../messaging/remote-contract";
|
|
5
|
+
import { enrollRemoteHost, handleEnrollmentControl, probeRemoteHost, removeRemoteHost } from "../messaging/remote-enrollment";
|
|
6
|
+
import { abandonRemoteEnrollment, pendingRemoteEnrollment } from "../messaging/remote-enrollment-recovery";
|
|
7
|
+
import { remoteControl } from "../messaging/remote-auth";
|
|
8
|
+
import { ownerEndpoint, startRemoteOwner } from "../messaging/remote-owner";
|
|
9
|
+
import { remotePortCandidate } from "../messaging/remote-ports";
|
|
10
|
+
import { remoteSessions, sendRemoteMessage } from "../messaging/remote-send";
|
|
11
|
+
import { RemoteMessageStore } from "../messaging/remote-store";
|
|
12
|
+
import { messageFailure } from "../messaging/send";
|
|
13
|
+
import { messageCodexHome } from "./message-runtime";
|
|
14
|
+
import { terminalSafeText } from "./runtime-api";
|
|
15
|
+
import type { RemoteMessageArgs } from "./message-remote-args";
|
|
16
|
+
|
|
17
|
+
/** Activate remote resources only after an explicit remote command's pure parser succeeds. */
|
|
18
|
+
export async function runRemoteMessageCommand(args: RemoteMessageArgs, env: NodeJS.ProcessEnv): Promise<number> {
|
|
19
|
+
const store = new RemoteMessageStore(getConfigDir()), controller = new AbortController(), capacity = new RemoteCapacity();
|
|
20
|
+
const cancel = () => controller.abort();
|
|
21
|
+
process.once("SIGINT", cancel); process.once("SIGTERM", cancel);
|
|
22
|
+
const emit = (value: unknown) => console.log(args.json || args.action.startsWith("_")
|
|
23
|
+
? JSON.stringify(value) : terminalSafeText(JSON.stringify(value)));
|
|
24
|
+
try {
|
|
25
|
+
if (args.action === "serve") {
|
|
26
|
+
const owner = await startRemoteOwner(store, messageCodexHome(env), args.hosts, controller.signal);
|
|
27
|
+
try {
|
|
28
|
+
emit({ protocol: "ocx-message-remote/1", running: true, port: owner.port });
|
|
29
|
+
await Promise.race([owner.finished, new Promise<void>(resolve => {
|
|
30
|
+
controller.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
31
|
+
if (controller.signal.aborted) resolve();
|
|
32
|
+
})]);
|
|
33
|
+
if (!controller.signal.aborted) { emit({ running: false, error: { code: "owner_retired", message: "Foreground messaging owner retired; restart explicitly after resolving its route/configuration failure." } }); return 1; }
|
|
34
|
+
} finally { await owner.close(); }
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const budget = new MessageBudget(30000, controller.signal);
|
|
38
|
+
try {
|
|
39
|
+
if (args.action === "enable") { store.enable(args.port ?? store.read()?.port ?? 39176); emit(store.publicState()); }
|
|
40
|
+
else if (args.action === "disable") { store.disable(); emit(store.publicState()); }
|
|
41
|
+
else if (args.action === "hosts-list") emit({ ...store.publicState(), pendingEnrollment: pendingRemoteEnrollment(store) });
|
|
42
|
+
else if (args.action === "hosts-abandon") { emit(abandonRemoteEnrollment(store, args.transaction)); return 3; }
|
|
43
|
+
else if (args.action === "status") {
|
|
44
|
+
const state = store.read(); let running = false, routes: unknown = [];
|
|
45
|
+
if (state?.enabled) {
|
|
46
|
+
try { const result = await remoteControl(state, ownerEndpoint(state), "message/routes", {}, budget);
|
|
47
|
+
running = true; routes = result; } catch { /* Disabled/offline is not activation or repair. */ }
|
|
48
|
+
}
|
|
49
|
+
emit({ ...store.publicState(), running, routes, pendingEnrollment: pendingRemoteEnrollment(store) });
|
|
50
|
+
} else if (args.action === "hosts-probe") emit({ ssh: args.ssh, fingerprint: (await probeRemoteHost(args.ssh, budget, capacity)).fingerprint });
|
|
51
|
+
else if (args.action === "hosts-add") emit(await enrollRemoteHost(store, args.alias, args.ssh, args.fingerprint, budget, capacity));
|
|
52
|
+
else if (args.action === "hosts-allow-inbound" || args.action === "hosts-deny-inbound") {
|
|
53
|
+
emit(store.setInbound(args.host, args.action === "hosts-allow-inbound"));
|
|
54
|
+
}
|
|
55
|
+
else if (args.action === "hosts-remove") {
|
|
56
|
+
const receipt = await removeRemoteHost(store, args.host, budget, capacity, undefined, undefined, args.transaction); emit(receipt);
|
|
57
|
+
return receipt.remote === "removed" ? 0 : 3;
|
|
58
|
+
} else if (args.action === "_control") {
|
|
59
|
+
emit(handleEnrollmentControl(store, JSON.parse(await readMessageInput(Bun.stdin.stream(), budget))));
|
|
60
|
+
} else if (args.action === "_port") { store.requireEnabled(); emit({ port: remotePortCandidate() }); }
|
|
61
|
+
else if (args.action === "remote-operation") {
|
|
62
|
+
if (args.local.action === "sessions") emit(await remoteSessions(store, args.host, budget));
|
|
63
|
+
else {
|
|
64
|
+
const body = await readMessageInput(Bun.stdin.stream(), budget);
|
|
65
|
+
const receipt = await sendRemoteMessage(store, { ...args.local, host: args.host, body },
|
|
66
|
+
{ home: messageCodexHome(env), senderId: env.CODEX_THREAD_ID }, budget);
|
|
67
|
+
emit(receipt); return receipt.status === "queued" ? 0 : receipt.status === "unknown" ? 3 : 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
} finally { budget.dispose(); }
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Control errors never echo stdin, SSH output, keys or paths; a failed enrollment keeps its transaction.
|
|
74
|
+
const failure = messageFailure(error), unknown = ["enrollment_unknown", "remote_control_unknown", "remote_storage_unknown", "cleanup_incomplete"].includes(failure.code);
|
|
75
|
+
emit({ schema: "ocx-message-error/1", ...(unknown ? { status: "unknown" } : args.action === "remote-operation" && args.local.action === "send" ? { status: "not_sent" } : {}), error: failure }); return unknown ? 3 : 1;
|
|
76
|
+
} finally { process.removeListener("SIGINT", cancel); process.removeListener("SIGTERM", cancel); }
|
|
77
|
+
}
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
takeBooleanOption,
|
|
11
11
|
takeFlag,
|
|
12
12
|
takeOption,
|
|
13
|
+
terminalSafeText,
|
|
13
14
|
type RuntimeApiDeps,
|
|
14
15
|
} from "./runtime-api";
|
|
16
|
+
import { interactiveConfirm } from "./interactive-confirm";
|
|
15
17
|
import { takeProviderEditSettings } from "./provider-settings";
|
|
16
18
|
import { printProviderReceipt, runProviderAction } from "./provider-result";
|
|
17
19
|
import { providerQuotaLine } from "./account-extended";
|
|
@@ -46,7 +48,7 @@ const USAGE = `Usage:
|
|
|
46
48
|
[--retain-models <id,id|->] [--model <id> --text-only]
|
|
47
49
|
[--xai-chat <on|off>] [--upstream-http-version <http1.1|->] [--fast <on|off>] [--context-window <tokens|->]
|
|
48
50
|
[--allow-private-network <on|off>] [--model-context-tier <model=default|long_context>] [--json]
|
|
49
|
-
ocx provider test <name> [--json]
|
|
51
|
+
ocx provider test <name> [--model <id> [--yes]] [--json]
|
|
50
52
|
ocx provider quota [--refresh] [--json]
|
|
51
53
|
ocx provider resets [--limit <n>] [--json]
|
|
52
54
|
ocx provider presets [--json]
|
|
@@ -147,12 +149,78 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
|
147
149
|
return printProviderReceipt(result, wantsJson, "Provider edit");
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
interface CompletionTestDto {
|
|
153
|
+
applicable: boolean;
|
|
154
|
+
reason?: string;
|
|
155
|
+
detail?: string;
|
|
156
|
+
adapter?: string;
|
|
157
|
+
ok?: boolean;
|
|
158
|
+
wireModel?: string;
|
|
159
|
+
upstreamModel?: string | null;
|
|
160
|
+
modelComparison?: "match" | "mismatch" | "unknown";
|
|
161
|
+
comparisonNote?: "missing_metadata" | "conflicting_metadata";
|
|
162
|
+
ttftMs?: number | null;
|
|
163
|
+
totalLatencyMs?: number;
|
|
164
|
+
error?: { code: string; message: string; status?: number } | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function testCompletion(name: string, model: string, yes: boolean, wantsJson: boolean, deps: RuntimeApiDeps): Promise<number> {
|
|
168
|
+
const input = deps.stdinImpl ?? process.stdin;
|
|
169
|
+
if (!yes && (wantsJson || !input.isTTY)) {
|
|
170
|
+
throw new CliUsageError("completion tests consume quota and may incur cost; --yes is required with --json or non-interactive input", USAGE);
|
|
171
|
+
}
|
|
172
|
+
const warning = terminalSafeText(`${name}: completion test for model ${model}. Warning: this sends an upstream request, consumes quota and may incur cost.`);
|
|
173
|
+
if (wantsJson) console.error(warning);
|
|
174
|
+
else console.log(warning);
|
|
175
|
+
if (!yes && !await interactiveConfirm({
|
|
176
|
+
question: "Run this completion test?", defaultYes: false, input: input as NodeJS.ReadStream,
|
|
177
|
+
})) {
|
|
178
|
+
console.log("cancelled");
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
const result = await runtimeRequest<CompletionTestDto>(`/api/providers/test-completion?name=${encodeURIComponent(name)}`, {
|
|
182
|
+
method: "POST", redirect: "error", body: JSON.stringify({ model, confirm: true }),
|
|
183
|
+
}, deps);
|
|
184
|
+
if (result.applicable === false) {
|
|
185
|
+
printData(result, wantsJson, [
|
|
186
|
+
`${name}: not applicable — ${result.reason}`,
|
|
187
|
+
...(result.detail ? [`Detail: ${result.detail}`] : []),
|
|
188
|
+
...(result.adapter ? [`Adapter: ${result.adapter}`] : []),
|
|
189
|
+
]);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (result.ok === true) {
|
|
193
|
+
printData(result, wantsJson, [
|
|
194
|
+
`${name}: connected`,
|
|
195
|
+
`TTFT: ${result.ttftMs ?? "unknown"} ms`,
|
|
196
|
+
`Total latency: ${result.totalLatencyMs ?? "unknown"} ms`,
|
|
197
|
+
`Upstream reported ${result.upstreamModel ?? "unknown"} for ${result.wireModel} — ${result.modelComparison}${result.comparisonNote ? ` (${result.comparisonNote})` : ""}`,
|
|
198
|
+
"metadata only — not proof of model identity",
|
|
199
|
+
]);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
printData(result, wantsJson, [
|
|
203
|
+
`${name}: failed`,
|
|
204
|
+
`${result.error?.code ?? "unknown"}: ${result.error?.message ?? "Completion test failed"}${result.error?.status === undefined ? "" : ` (HTTP ${result.error.status})`}`,
|
|
205
|
+
]);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
150
209
|
async function testProvider(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
151
210
|
const args = [...argv];
|
|
152
211
|
const name = args.shift()?.trim();
|
|
153
212
|
const wantsJson = takeFlag(args, "--json");
|
|
154
213
|
if (!name) throw new CliUsageError("provider name is required", USAGE);
|
|
214
|
+
const model = takeOption(args, "--model")?.trim();
|
|
215
|
+
const yes = takeFlag(args, "--yes");
|
|
155
216
|
rejectArgs(args, USAGE);
|
|
217
|
+
if (yes && model === undefined) throw new CliUsageError("--yes requires --model", USAGE);
|
|
218
|
+
if (model !== undefined) {
|
|
219
|
+
if (!model || model.length > 256 || /[\x00-\x1f\x7f-\x9f]/.test(model)) {
|
|
220
|
+
throw new CliUsageError("--model must contain 1..256 characters without control characters", USAGE);
|
|
221
|
+
}
|
|
222
|
+
return testCompletion(name, model, yes, wantsJson, deps);
|
|
223
|
+
}
|
|
156
224
|
const result = await runtimeRequest<Record<string, unknown>>(`/api/providers/test?name=${encodeURIComponent(name)}`, {
|
|
157
225
|
method: "POST",
|
|
158
226
|
}, deps);
|
package/src/cli/registry.ts
CHANGED
|
@@ -16,9 +16,9 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "message",
|
|
19
|
-
usage: "ocx message <sessions|send> [options]",
|
|
20
|
-
summary: "Discover loaded
|
|
21
|
-
details: ["
|
|
19
|
+
usage: "ocx message <sessions|send|enable|disable|status|hosts|serve> [options]",
|
|
20
|
+
summary: "Discover loaded Codex sessions and queue messages locally or through explicit SSH peers.",
|
|
21
|
+
details: ["Remote listeners/tunnels require explicit foreground serve; no daemon start. queued is submission, not processing; never replay an unknown send."],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
name: "chatgpt",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { observeSignedInCodexIdentity } from "./signed-in-identity";
|
|
2
|
+
import { observeMainCodexCollisionIdentity } from "./collision-identity";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import {
|
|
4
5
|
deleteConfigTopLevelKey,
|
|
@@ -9,11 +10,12 @@ import {
|
|
|
9
10
|
import { captureConfigTopLevelRollback } from "../config/rebase-provenance";
|
|
10
11
|
import { removeCodexAccountCredential } from "./account-store";
|
|
11
12
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
12
|
-
import {
|
|
13
|
+
import { readMainAuth } from "./auth-collision";
|
|
13
14
|
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
14
15
|
import { clearAccountQuota } from "./quota";
|
|
15
16
|
import { forgetManualPreference } from "./routing/active-account";
|
|
16
17
|
import { deleteAllHealthForAccount } from "./routing/health-store";
|
|
18
|
+
import { forgetCodexFailureWindow } from "./routing/failure-window";
|
|
17
19
|
import { clearThreadAccountMapForAccount } from "./routing/thread-affinity";
|
|
18
20
|
|
|
19
21
|
import { clearCodexPoolRefreshFailure } from "./pool-refresh-backoff";
|
|
@@ -49,11 +51,15 @@ export function purgeCodexAccountRuntimeState(accountId: string): void {
|
|
|
49
51
|
clearAccountNeedsReauth(accountId);
|
|
50
52
|
clearAccountQuota(accountId);
|
|
51
53
|
clearThreadAccountMapForAccount(accountId);
|
|
52
|
-
// Deliberately not via `./routing`: the account-delete path only needs the health rows
|
|
53
|
-
//
|
|
54
|
-
// routing/health-store and routing/active-account.
|
|
54
|
+
// Deliberately not via `./routing`: the account-delete path only needs the health rows
|
|
55
|
+
// and their sibling routing state gone, and importing the router barrel here closed a
|
|
56
|
+
// cycle through routing/health-store and routing/active-account. The leaf imports below
|
|
57
|
+
// carry every per-account forget that barrel helper used to perform — including the
|
|
58
|
+
// failure window, whose degrade and pin-warning state must not leak into a replacement
|
|
59
|
+
// account or a new physical login behind __main__.
|
|
55
60
|
deleteAllHealthForAccount(accountId);
|
|
56
61
|
forgetManualPreference(accountId);
|
|
62
|
+
forgetCodexFailureWindow(accountId);
|
|
57
63
|
clearCodexPoolRefreshFailure(accountId);
|
|
58
64
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
59
65
|
clearMainAccountInfoCache();
|
|
@@ -74,7 +80,8 @@ function purgeMainCodexAccountRuntimeState(): void {
|
|
|
74
80
|
* cooldown, quota, reauth, and thread affinity do not leak across accounts.
|
|
75
81
|
*/
|
|
76
82
|
export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
77
|
-
const read =
|
|
83
|
+
const read = readMainAuth();
|
|
84
|
+
if (read.status === "missing" || read.status === "api-key-only") observeMainCodexCollisionIdentity();
|
|
78
85
|
const currentAccountId = read.status === "ok"
|
|
79
86
|
? extractAccountId(read.tokens.id_token, read.tokens.access_token) ?? (read.tokens.account_id || null)
|
|
80
87
|
: null;
|
|
@@ -86,11 +93,13 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
|
86
93
|
observedMainChatgptAccountId = currentAccountId;
|
|
87
94
|
if (previousAccountId === undefined || previousAccountId === currentAccountId) {
|
|
88
95
|
observeMainQuotaIdentity(currentAccountId);
|
|
96
|
+
if (read.status === "ok") observeMainCodexCollisionIdentity(read.tokens);
|
|
89
97
|
return false;
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
purgeMainCodexAccountRuntimeState();
|
|
93
101
|
observeMainQuotaIdentity(currentAccountId);
|
|
102
|
+
if (read.status === "ok") observeMainCodexCollisionIdentity(read.tokens);
|
|
94
103
|
return true;
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -103,7 +112,8 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
|
103
112
|
export function initializeMainAccountPolicyBinding(authPath: string): boolean {
|
|
104
113
|
// Startup observes the pinned owned path inside the exclusive claim: bound the read so a
|
|
105
114
|
// replaced non-regular or oversized file cannot stall startup inside that claim.
|
|
106
|
-
const result =
|
|
115
|
+
const result = readMainAuth(authPath);
|
|
116
|
+
if (result.status === "missing" || result.status === "api-key-only") observeMainCodexCollisionIdentity();
|
|
107
117
|
if (result.status !== "ok") return false;
|
|
108
118
|
const { tokens } = result;
|
|
109
119
|
if (typeof tokens.access_token !== "string" || !tokens.access_token
|
|
@@ -124,6 +134,7 @@ export function initializeMainAccountPolicyBinding(authPath: string): boolean {
|
|
|
124
134
|
if (previousAccountId !== undefined && previousAccountId !== accountId) purgeMainCodexAccountRuntimeState();
|
|
125
135
|
observeSignedInCodexIdentity(tokens);
|
|
126
136
|
observeMainQuotaIdentity(accountId);
|
|
137
|
+
observeMainCodexCollisionIdentity(tokens);
|
|
127
138
|
return observeMainQuotaCredential(tokens.access_token, accountId) !== undefined;
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -136,6 +147,7 @@ export function applyConfirmedMainCodexAccountTransition(
|
|
|
136
147
|
toAccountId: string,
|
|
137
148
|
): boolean {
|
|
138
149
|
observeSignedInCodexIdentity();
|
|
150
|
+
observeMainCodexCollisionIdentity();
|
|
139
151
|
if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) {
|
|
140
152
|
if (toAccountId) {
|
|
141
153
|
observedMainChatgptAccountId = toAccountId;
|
|
@@ -150,6 +162,7 @@ export function applyConfirmedMainCodexAccountTransition(
|
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
export function resetMainCodexAccountIdentityTrackingForTests(): void {
|
|
165
|
+
observeMainCodexCollisionIdentity();
|
|
153
166
|
observedMainChatgptAccountId = undefined;
|
|
154
167
|
observeSignedInCodexIdentity();
|
|
155
168
|
clearMainAccountCredentialPresence();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AccountPauseDetails } from "../types/account-auto-pause";
|
|
1
3
|
import type { OcxConfig } from "../types";
|
|
2
4
|
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
3
5
|
|
|
@@ -7,7 +9,9 @@ export function isCodexAccountPaused(config: OcxConfig, accountId: string): bool
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/** Persist the account's pool eligibility without changing credentials or runtime health. */
|
|
10
|
-
export function setCodexAccountPaused(config: OcxConfig, accountId: string, paused: boolean): void {
|
|
12
|
+
export function setCodexAccountPaused(config: OcxConfig, accountId: string, paused: boolean, details?: AccountPauseDetails): void {
|
|
13
|
+
config.codexAccountPauseState = { ...config.codexAccountPauseState,
|
|
14
|
+
[accountId]: { revision: randomUUID(), ...(paused && details ? { details } : {}) } };
|
|
11
15
|
const pausedIds = new Set(config.pausedCodexAccountIds ?? []);
|
|
12
16
|
if (paused) pausedIds.add(accountId);
|
|
13
17
|
else pausedIds.delete(accountId);
|
|
@@ -18,4 +22,5 @@ export function setCodexAccountPaused(config: OcxConfig, accountId: string, paus
|
|
|
18
22
|
|
|
19
23
|
export function forgetCodexAccountPause(config: OcxConfig, accountId: string): void {
|
|
20
24
|
setCodexAccountPaused(config, accountId, false);
|
|
25
|
+
if (config.codexAccountPauseState) delete config.codexAccountPauseState[accountId];
|
|
21
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setDecisionAccountUsable } from "../lib/decision-quota-sink";
|
|
1
2
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
2
3
|
import { isCodexAccountGenerationLive } from "./account-store";
|
|
3
4
|
|
|
@@ -39,12 +40,14 @@ export function clearMainRefreshGrantRejection(key: string): void {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
/** Quarantine a Codex account (or one credential generation) after an auth failure, and mark its advisory decision-quota row unusable so routing evidence cannot treat it as healthy. */
|
|
42
44
|
export function markAccountNeedsReauth(
|
|
43
45
|
id: string,
|
|
44
46
|
writerGeneration = captureConfigGeneration(),
|
|
45
47
|
credentialGeneration?: number,
|
|
46
48
|
): void {
|
|
47
49
|
if (writerGeneration < lastReconciledGeneration && !liveAccountIds.has(id)) return;
|
|
50
|
+
setDecisionAccountUsable(id === "__main__" ? "codex-main" : "codex", id, false, credentialGeneration);
|
|
48
51
|
// An account-wide mark supersedes a generation-scoped one: it is the stronger claim.
|
|
49
52
|
if (credentialGeneration === undefined || !reauthAccounts.has(id)) {
|
|
50
53
|
reauthAccounts.set(id, credentialGeneration);
|
|
@@ -81,6 +84,7 @@ export function isAccountNeedsReauth(id: string): boolean {
|
|
|
81
84
|
return true;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** Lift a reauth quarantine only when the proof matches the quarantined credential generation, and restore the advisory decision-quota row's usability. */
|
|
84
88
|
export function clearAccountNeedsReauth(id: string, credentialGeneration?: number): void {
|
|
85
89
|
// A model response proves only the credential it used. Keep account-wide
|
|
86
90
|
// quarantine and evidence from another generation intact.
|
|
@@ -88,4 +92,5 @@ export function clearAccountNeedsReauth(id: string, credentialGeneration?: numbe
|
|
|
88
92
|
&& (reauthAccounts.get(id) !== credentialGeneration
|
|
89
93
|
|| !isCodexAccountGenerationLive(id, credentialGeneration))) return;
|
|
90
94
|
reauthAccounts.delete(id);
|
|
95
|
+
setDecisionAccountUsable(id === "__main__" ? "codex-main" : "codex", id, true, credentialGeneration);
|
|
91
96
|
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
hardenExistingSecret,
|
|
15
15
|
withConfigMutationLockSync,
|
|
16
16
|
} from "../config";
|
|
17
|
+
import { notifyCodexStoreLoaded } from "../lib/decision-quota-sink";
|
|
17
18
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
18
19
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
19
20
|
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
@@ -113,30 +114,68 @@ function normalizeRecord(value: CodexAccountCredentials | CodexAccountCredential
|
|
|
113
114
|
return undefined;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Observe-only read for the JEV quota activation owner: the same normalization as the loader,
|
|
119
|
+
* without hardening, invalid-file backup or slot notification. Absent or invalid stores read as empty.
|
|
120
|
+
*/
|
|
121
|
+
export function peekCodexAccountRecordStore(): Record<string, CodexAccountCredentialRecord> {
|
|
122
|
+
const path = codexAccountsPath();
|
|
123
|
+
if (!existsSync(path)) return {};
|
|
124
|
+
const normalized: Record<string, CodexAccountCredentialRecord> = {};
|
|
125
|
+
try {
|
|
126
|
+
const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
|
|
127
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
128
|
+
const record = normalizeRecord(value);
|
|
129
|
+
if (record) normalized[id] = record;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read the Codex account record store from disk with hardened permissions, and hand it to the opt-in decision-quota slot (a no-op unless JEV quota signals are active). */
|
|
116
138
|
function loadCodexAccountRecordStore(): CodexAccountStore {
|
|
139
|
+
const result = loadCodexAccountRecordStoreResult();
|
|
140
|
+
return result.status === "ok" ? result.records : {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type CodexAccountRecordStoreResult = { status: "ok"; records: CodexAccountStore } | { status: "failed" };
|
|
144
|
+
|
|
145
|
+
/** Retain read/parse failure separately for consumers that must preserve existing evidence. */
|
|
146
|
+
function loadCodexAccountRecordStoreResult(): CodexAccountRecordStoreResult {
|
|
117
147
|
const path = codexAccountsPath();
|
|
118
148
|
hardenConfigDir();
|
|
119
149
|
hardenExistingSecret(path);
|
|
120
|
-
|
|
150
|
+
const normalized: CodexAccountStore = {};
|
|
121
151
|
try {
|
|
122
152
|
const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
|
|
123
|
-
|
|
153
|
+
if (!isObject(raw)) throw new Error("invalid Codex account store shape");
|
|
124
154
|
for (const [id, value] of Object.entries(raw)) {
|
|
125
155
|
const record = normalizeRecord(value);
|
|
126
156
|
if (record) normalized[id] = record;
|
|
127
157
|
}
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
160
|
+
notifyCodexStoreLoaded({});
|
|
161
|
+
return { status: "ok", records: {} };
|
|
162
|
+
}
|
|
130
163
|
backupInvalidConfig(path);
|
|
131
|
-
|
|
164
|
+
notifyCodexStoreLoaded({});
|
|
165
|
+
return { status: "failed" };
|
|
132
166
|
}
|
|
167
|
+
// Outside the parse try: roster publication is advisory and must never route a loaded store into the invalid-config backup path.
|
|
168
|
+
notifyCodexStoreLoaded(normalized);
|
|
169
|
+
return { status: "ok", records: normalized };
|
|
133
170
|
}
|
|
134
171
|
|
|
172
|
+
/** Atomically write the Codex account store, then hand it to the opt-in decision-quota slot so active advisory evidence tracks the persisted credentials. */
|
|
135
173
|
function persist(store: CodexAccountStore): void {
|
|
136
174
|
const dir = getConfigDir();
|
|
137
175
|
assertNotRealHomeUnderTest(dir);
|
|
138
176
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
139
177
|
atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
|
|
178
|
+
notifyCodexStoreLoaded(store);
|
|
140
179
|
}
|
|
141
180
|
|
|
142
181
|
function persistCredentialMutation(store: CodexAccountStore): void {
|
|
@@ -373,6 +412,10 @@ export function isCodexAccountGenerationLive(id: string, generation: number): bo
|
|
|
373
412
|
return !!record?.credential && record.deletedAt == null && record.generation === generation;
|
|
374
413
|
}
|
|
375
414
|
|
|
415
|
+
/** Callable for existing liveness consumers; stateful consumers can distinguish unknown snapshots. */
|
|
416
|
+
export type CodexAccountGenerationLiveCheck = ((id: string, generation: number) => boolean)
|
|
417
|
+
& { readonly snapshotStatus: "ok" | "failed" };
|
|
418
|
+
|
|
376
419
|
/**
|
|
377
420
|
* The same verdict as {@link isCodexAccountGenerationLive}, over ONE store load.
|
|
378
421
|
*
|
|
@@ -382,12 +425,15 @@ export function isCodexAccountGenerationLive(id: string, generation: number): bo
|
|
|
382
425
|
* several accounts in one synchronous pass on the request path, so it opens one checker and
|
|
383
426
|
* closes over the snapshot instead.
|
|
384
427
|
*/
|
|
385
|
-
export function beginCodexAccountGenerationLiveCheck():
|
|
386
|
-
|
|
387
|
-
|
|
428
|
+
export function beginCodexAccountGenerationLiveCheck(): CodexAccountGenerationLiveCheck {
|
|
429
|
+
let result: CodexAccountRecordStoreResult;
|
|
430
|
+
try { result = loadCodexAccountRecordStoreResult(); }
|
|
431
|
+
catch { result = { status: "failed" }; }
|
|
432
|
+
const snapshot = result.status === "ok" ? result.records : {};
|
|
433
|
+
return Object.assign((id: string, generation: number) => {
|
|
388
434
|
const record = snapshot[id];
|
|
389
435
|
return !!record?.credential && record.deletedAt == null && record.generation === generation;
|
|
390
|
-
};
|
|
436
|
+
}, { snapshotStatus: result.status });
|
|
391
437
|
}
|
|
392
438
|
|
|
393
439
|
export function saveCodexAccountCredentialIfGeneration(
|