@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913
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/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-CWXut3rG.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modelCapabilitiesConfigError, mergeModelCapabilities } from "../../config/provider-validation";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
mutatePersistedConfig,
|
|
16
17
|
nonBlankStringArrayConfigError,
|
|
17
18
|
normalizeNonBlankStringArray,
|
|
19
|
+
normalizeAutoReviewModelOverrides,
|
|
18
20
|
providerBaseUrlConfigError,
|
|
19
21
|
providerHeadersConfigError,
|
|
20
22
|
requestPacingConfigError,
|
|
@@ -34,7 +36,7 @@ import {
|
|
|
34
36
|
upsertOAuthProvider,
|
|
35
37
|
} from "../../oauth";
|
|
36
38
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
37
|
-
import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
|
|
39
|
+
import { canonicalAutoReviewModelKey, mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
|
|
38
40
|
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
39
41
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
40
42
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
@@ -257,7 +259,10 @@ function providerEditorCandidate(
|
|
|
257
259
|
if (namespaceCollision) return { ok: false, status: 409, error: namespaceCollision, code: "provider_namespace_conflict" };
|
|
258
260
|
const merged = mergeProviderEditorRow(persisted.providers[name], baseline.providers[name], publicProvider);
|
|
259
261
|
const transportCandidate = providerTransportValidationCandidate(merged as unknown as Record<string, unknown>);
|
|
260
|
-
|
|
262
|
+
// The editor merges onto the persisted row, so stored operator overlays (selectedModels,
|
|
263
|
+
// disabled, …) ride along in the candidate. They are owned by their own write boundaries;
|
|
264
|
+
// the seed check must only pin the canonical transport/auth keys.
|
|
265
|
+
const providerError = providerManagementConfigError(name, transportCandidate, { allowOperatorOverlays: true })
|
|
261
266
|
?? providerEmptyToolOutputConfigError(name, transportCandidate)
|
|
262
267
|
?? providerServiceTierConfigError(name, transportCandidate);
|
|
263
268
|
if (providerError) return { ok: false, status: 400, error: providerError, code: "invalid_provider" };
|
|
@@ -287,6 +292,13 @@ function providerEditorCandidate(
|
|
|
287
292
|
if (provider.modelPinnedReasoningEfforts !== undefined) {
|
|
288
293
|
provider.modelPinnedReasoningEfforts = validated.config.providers[name]!.modelPinnedReasoningEfforts;
|
|
289
294
|
}
|
|
295
|
+
const normalized = validated.config.providers[name]!;
|
|
296
|
+
if (normalized.autoReviewModel === undefined) delete provider.autoReviewModel;
|
|
297
|
+
else provider.autoReviewModel = normalized.autoReviewModel;
|
|
298
|
+
if (normalized.autoReviewModelOverrides === undefined) delete provider.autoReviewModelOverrides;
|
|
299
|
+
else provider.autoReviewModelOverrides = normalized.autoReviewModelOverrides;
|
|
300
|
+
if (normalized.modelCapabilities === undefined) delete provider.modelCapabilities;
|
|
301
|
+
else provider.modelCapabilities = normalized.modelCapabilities;
|
|
290
302
|
}
|
|
291
303
|
return { ok: true, config: candidate, removedProviders };
|
|
292
304
|
}
|
|
@@ -490,6 +502,14 @@ function applyProviderPatchFields(
|
|
|
490
502
|
}
|
|
491
503
|
touched = true;
|
|
492
504
|
}
|
|
505
|
+
if (Object.hasOwn(rawBody, "modelCapabilities")) {
|
|
506
|
+
const error = modelCapabilitiesConfigError(rawBody.modelCapabilities, true);
|
|
507
|
+
if (error) return { error };
|
|
508
|
+
const capabilities = mergeModelCapabilities(next.modelCapabilities, rawBody.modelCapabilities);
|
|
509
|
+
if (capabilities === undefined) delete next.modelCapabilities;
|
|
510
|
+
else next.modelCapabilities = capabilities;
|
|
511
|
+
touched = true;
|
|
512
|
+
}
|
|
493
513
|
if (Object.hasOwn(rawBody, "modelContextWindows")) {
|
|
494
514
|
const value = rawBody.modelContextWindows;
|
|
495
515
|
if (value === null) {
|
|
@@ -518,6 +538,61 @@ function applyProviderPatchFields(
|
|
|
518
538
|
if (error) return { error };
|
|
519
539
|
touched = true;
|
|
520
540
|
}
|
|
541
|
+
if (Object.hasOwn(rawBody, "autoReviewModel")) {
|
|
542
|
+
const value = rawBody.autoReviewModel;
|
|
543
|
+
if (value === null || value === "") {
|
|
544
|
+
delete next.autoReviewModel;
|
|
545
|
+
} else if (typeof value === "string" && value.trim()) {
|
|
546
|
+
next.autoReviewModel = value.trim();
|
|
547
|
+
} else {
|
|
548
|
+
return { error: "autoReviewModel must be a catalog selector string or null" };
|
|
549
|
+
}
|
|
550
|
+
touched = true;
|
|
551
|
+
}
|
|
552
|
+
if (Object.hasOwn(rawBody, "autoReviewModelOverrides")) {
|
|
553
|
+
const value = rawBody.autoReviewModelOverrides;
|
|
554
|
+
if (value === null) {
|
|
555
|
+
delete next.autoReviewModelOverrides;
|
|
556
|
+
} else if (isPlainRecord(value)) {
|
|
557
|
+
const merged: Record<string, string> = { ...(next.autoReviewModelOverrides ?? {}) };
|
|
558
|
+
const existingByCanonical = new Map<string, string>();
|
|
559
|
+
for (const existingKey of Object.keys(merged)) {
|
|
560
|
+
existingByCanonical.set(canonicalAutoReviewModelKey(existingKey), existingKey);
|
|
561
|
+
}
|
|
562
|
+
const submittedCanonicalKeys = new Set<string>();
|
|
563
|
+
for (const [model, target] of Object.entries(value)) {
|
|
564
|
+
const key = model.trim();
|
|
565
|
+
if (["__proto__", "prototype", "constructor"].includes(key)) {
|
|
566
|
+
return { error: "autoReviewModelOverrides keys must be non-reserved model ids" };
|
|
567
|
+
}
|
|
568
|
+
const canonicalKey = canonicalAutoReviewModelKey(model);
|
|
569
|
+
// Uniqueness is enforced before the tombstone branch: a clear and a set that normalize to
|
|
570
|
+
// the same key would otherwise resolve in object order instead of being rejected.
|
|
571
|
+
if (submittedCanonicalKeys.has(canonicalKey)) {
|
|
572
|
+
return { error: "autoReviewModelOverrides keys must be unique after trimming and slash normalization" };
|
|
573
|
+
}
|
|
574
|
+
submittedCanonicalKeys.add(canonicalKey);
|
|
575
|
+
if (target === null || target === "") {
|
|
576
|
+
const previousKey = existingByCanonical.get(canonicalKey);
|
|
577
|
+
if (previousKey !== undefined) delete merged[previousKey];
|
|
578
|
+
if (Object.hasOwn(merged, key)) delete merged[key];
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (typeof target !== "string" || !target.trim()) {
|
|
582
|
+
return { error: "autoReviewModelOverrides values must be catalog selectors, null, or empty to remove" };
|
|
583
|
+
}
|
|
584
|
+
const previousKey = existingByCanonical.get(canonicalKey);
|
|
585
|
+
if (previousKey !== undefined && previousKey !== key) delete merged[previousKey];
|
|
586
|
+
merged[key] = target.trim();
|
|
587
|
+
existingByCanonical.set(canonicalKey, key);
|
|
588
|
+
}
|
|
589
|
+
if (Object.keys(merged).length > 0) next.autoReviewModelOverrides = merged;
|
|
590
|
+
else delete next.autoReviewModelOverrides;
|
|
591
|
+
} else {
|
|
592
|
+
return { error: "autoReviewModelOverrides must be a plain object or null" };
|
|
593
|
+
}
|
|
594
|
+
touched = true;
|
|
595
|
+
}
|
|
521
596
|
if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
|
|
522
597
|
const value = rawBody.modelAutoCompactTokenLimits;
|
|
523
598
|
const error = modelAutoCompactTokenLimitsConfigError(value, {
|
|
@@ -784,6 +859,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
784
859
|
models: p.models ?? [],
|
|
785
860
|
contextWindow: p.contextWindow,
|
|
786
861
|
modelContextWindows: p.modelContextWindows,
|
|
862
|
+
modelCapabilities: p.modelCapabilities,
|
|
787
863
|
pinnedReasoningEffort: p.pinnedReasoningEffort,
|
|
788
864
|
modelPinnedReasoningEfforts: p.modelPinnedReasoningEfforts,
|
|
789
865
|
modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
|
|
@@ -829,6 +905,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
829
905
|
const providerError = providerManagementConfigError(
|
|
830
906
|
name,
|
|
831
907
|
providerTransportValidationCandidate(provider as unknown as Record<string, unknown>),
|
|
908
|
+
// Reload validates a row straight off disk, which legitimately carries stored
|
|
909
|
+
// operator overlays; only the canonical transport/auth keys need to match the seed.
|
|
910
|
+
{ allowOperatorOverlays: true },
|
|
832
911
|
)
|
|
833
912
|
?? providerEmptyToolOutputConfigError(name, provider);
|
|
834
913
|
if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
|
|
@@ -996,6 +1075,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
996
1075
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
997
1076
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
998
1077
|
if (!isPlainRecord(body.provider)) return jsonResponse({ error: "provider must be a plain object" }, 400);
|
|
1078
|
+
// Same prohibition as PATCH: the canonical OpenAI row never carries these fields, and a clear
|
|
1079
|
+
// form would otherwise be normalized away before the merged-row guard could see it.
|
|
1080
|
+
if (name === "openai" && (Object.hasOwn(body.provider, "autoReviewModel") || Object.hasOwn(body.provider, "autoReviewModelOverrides"))) {
|
|
1081
|
+
return jsonResponse({ error: "provider openai must not include autoReviewModel or autoReviewModelOverrides" }, 400);
|
|
1082
|
+
}
|
|
999
1083
|
const existing = config.providers[name];
|
|
1000
1084
|
const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
|
|
1001
1085
|
if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
|
|
@@ -1069,6 +1153,29 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1069
1153
|
// erase hand-edited per-model prices from Logs/Usage estimates.
|
|
1070
1154
|
const existingCosts = config.providers[name]?.modelCosts;
|
|
1071
1155
|
if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
|
|
1156
|
+
// The add/edit form also omits auto-review selectors. Preserve hand-configured
|
|
1157
|
+
// provider-wide and per-model targets across an unrelated overwrite; clearing is
|
|
1158
|
+
// explicit through PATCH with null.
|
|
1159
|
+
const submittedAutoReviewModel = Object.hasOwn(body.provider, "autoReviewModel");
|
|
1160
|
+
const submittedAutoReviewOverrides = Object.hasOwn(body.provider, "autoReviewModelOverrides");
|
|
1161
|
+
const existingAutoReviewModel = config.providers[name]?.autoReviewModel;
|
|
1162
|
+
if (!submittedAutoReviewModel && existingAutoReviewModel && !prov.autoReviewModel) prov.autoReviewModel = existingAutoReviewModel;
|
|
1163
|
+
const existingAutoReviewOverrides = config.providers[name]?.autoReviewModelOverrides;
|
|
1164
|
+
if (!submittedAutoReviewOverrides && existingAutoReviewOverrides && !prov.autoReviewModelOverrides) {
|
|
1165
|
+
prov.autoReviewModelOverrides = { ...existingAutoReviewOverrides };
|
|
1166
|
+
}
|
|
1167
|
+
if (prov.autoReviewModel !== undefined) {
|
|
1168
|
+
if (typeof prov.autoReviewModel === "string" && prov.autoReviewModel.trim()) {
|
|
1169
|
+
prov.autoReviewModel = prov.autoReviewModel.trim();
|
|
1170
|
+
} else {
|
|
1171
|
+
delete prov.autoReviewModel;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (prov.autoReviewModelOverrides !== undefined) {
|
|
1175
|
+
const normalizedOverrides = normalizeAutoReviewModelOverrides(prov.autoReviewModelOverrides);
|
|
1176
|
+
if (normalizedOverrides) prov.autoReviewModelOverrides = normalizedOverrides;
|
|
1177
|
+
else delete prov.autoReviewModelOverrides;
|
|
1178
|
+
}
|
|
1072
1179
|
// And to the per-provider account-failover opt-out (#2568d). `ProviderPayload` has no
|
|
1073
1180
|
// member for it either, so an add/edit save structurally cannot carry it — and dropping it
|
|
1074
1181
|
// silently ENABLES rotation, because activation is presence-driven once the knob is gone.
|
|
@@ -1117,6 +1224,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1117
1224
|
// completed during that wait remains authoritative instead of being overwritten by the
|
|
1118
1225
|
// older ownership snapshot used to admit this POST.
|
|
1119
1226
|
restorePersistedAliasOverlays(prov, config.providers[name]);
|
|
1227
|
+
const capabilities = Object.hasOwn(body.provider, "modelCapabilities")
|
|
1228
|
+
? mergeModelCapabilities(undefined, prov.modelCapabilities)
|
|
1229
|
+
: mergeModelCapabilities(config.providers[name]?.modelCapabilities, undefined);
|
|
1230
|
+
if (capabilities === undefined) delete prov.modelCapabilities;
|
|
1231
|
+
else prov.modelCapabilities = capabilities;
|
|
1120
1232
|
// The add/edit form omits wire choices. Read after DNS so a concurrent switch
|
|
1121
1233
|
// remains authoritative, including the marker that protects it on the next boot.
|
|
1122
1234
|
if (name === "xai") {
|
|
@@ -1260,6 +1372,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1260
1372
|
if (Object.hasOwn(rawBody, "apiKey")) {
|
|
1261
1373
|
return jsonResponse({ error: "apiKey cannot be patched here; use the provider API-key endpoints" }, 400);
|
|
1262
1374
|
}
|
|
1375
|
+
// The canonical OpenAI row rejects these fields outright on create. A PATCH that only clears
|
|
1376
|
+
// or restates them would otherwise slip past the merged-row guard and answer 200, which reads
|
|
1377
|
+
// as acceptance for a field the provider does not support.
|
|
1378
|
+
if (name === "openai" && (Object.hasOwn(rawBody, "autoReviewModel") || Object.hasOwn(rawBody, "autoReviewModelOverrides"))) {
|
|
1379
|
+
return jsonResponse({ error: "provider openai must not include autoReviewModel or autoReviewModelOverrides" }, 400);
|
|
1380
|
+
}
|
|
1263
1381
|
const applied = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
|
|
1264
1382
|
if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
|
|
1265
1383
|
const next = applied.next;
|
|
@@ -1271,6 +1389,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1271
1389
|
: providerManagementConfigError(
|
|
1272
1390
|
name,
|
|
1273
1391
|
providerTransportValidationCandidate(next as unknown as Record<string, unknown>),
|
|
1392
|
+
// PATCH merges the mask onto the persisted row, which legitimately carries
|
|
1393
|
+
// stored operator overlays (selectedModels, disabled, …); only the canonical
|
|
1394
|
+
// transport/auth keys need to match the seed.
|
|
1395
|
+
{ allowOperatorOverlays: true },
|
|
1274
1396
|
)
|
|
1275
1397
|
?? providerEmptyToolOutputConfigError(name, next);
|
|
1276
1398
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
@@ -1314,6 +1436,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1314
1436
|
: providerManagementConfigError(
|
|
1315
1437
|
name,
|
|
1316
1438
|
providerTransportValidationCandidate(replay.next as unknown as Record<string, unknown>),
|
|
1439
|
+
{ allowOperatorOverlays: true },
|
|
1317
1440
|
)
|
|
1318
1441
|
?? providerEmptyToolOutputConfigError(name, replay.next);
|
|
1319
1442
|
if (syncError) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
2
|
+
import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
|
|
3
|
+
import type { ManagementContext } from "./context";
|
|
4
|
+
import { isRemoteWorkspaceAgentProfile } from "../../remote-control/workspace-agent-protocol";
|
|
5
|
+
|
|
6
|
+
const REMOTE_WORKSPACE_PREFIX = "/api/remote-workspace";
|
|
7
|
+
|
|
8
|
+
function response(body: unknown, status = 200): Response {
|
|
9
|
+
return Response.json(body, { status, headers: { "cache-control": "no-store" } });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sessionOnly(ctx: ManagementContext): Response | null {
|
|
13
|
+
return ctx.principal === "gui-session"
|
|
14
|
+
? null
|
|
15
|
+
: response({ error: "A dashboard session is required for Remote Workspace changes." }, 403);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function jsonObject(req: Request): Promise<Record<string, unknown>> {
|
|
19
|
+
let value: unknown;
|
|
20
|
+
try { value = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); throw new Error("Remote Workspace request body must be JSON."); }
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
+
throw new Error("Remote Workspace request body must be an object.");
|
|
23
|
+
}
|
|
24
|
+
return value as Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exactBodyKeys(
|
|
28
|
+
body: Record<string, unknown>,
|
|
29
|
+
required: readonly string[],
|
|
30
|
+
optional: readonly string[] = [],
|
|
31
|
+
): void {
|
|
32
|
+
const allowed = new Set([...required, ...optional]);
|
|
33
|
+
if (required.some(key => !Object.hasOwn(body, key))
|
|
34
|
+
|| Object.keys(body).some(key => !allowed.has(key))) {
|
|
35
|
+
throw new Error("Remote Workspace request body contains invalid fields.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handleRemoteWorkspaceRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
40
|
+
const { req, url, config } = ctx;
|
|
41
|
+
if (!url.pathname.startsWith(REMOTE_WORKSPACE_PREFIX)) return null;
|
|
42
|
+
if (url.pathname.length > REMOTE_WORKSPACE_PREFIX.length
|
|
43
|
+
&& url.pathname[REMOTE_WORKSPACE_PREFIX.length] !== "/") return null;
|
|
44
|
+
if (!remoteWorkspaceEnabled(config)) {
|
|
45
|
+
return response({ available: false, reason: "Remote Workspace requires this OpenCodex instance to run as a hub." }, 409);
|
|
46
|
+
}
|
|
47
|
+
if (req.method !== "GET") {
|
|
48
|
+
const denied = sessionOnly(ctx);
|
|
49
|
+
if (denied) return denied;
|
|
50
|
+
}
|
|
51
|
+
if (ctx.deps.remoteWorkspaceStopping?.()) return response({ error: "Remote Workspace is stopping." }, 503);
|
|
52
|
+
const runtime = await import("../../remote-control/workspace-runtime");
|
|
53
|
+
if (ctx.deps.remoteWorkspaceStopping?.()) return response({ error: "Remote Workspace is stopping." }, 503);
|
|
54
|
+
const hub = ctx.deps.remoteWorkspaceHub ?? runtime.remoteWorkspaceHubForConfig(config);
|
|
55
|
+
const sessions = ctx.deps.remoteWorkspaceSessions ?? runtime.remoteWorkspaceSessionsForConfig(config);
|
|
56
|
+
ctx.deps.onRemoteWorkspaceShutdown?.(async () => {
|
|
57
|
+
const sessions = ctx.deps.remoteWorkspaceSessions ?? runtime.initializedRemoteWorkspaceSessionsForConfig(config);
|
|
58
|
+
const hub = ctx.deps.remoteWorkspaceHub ?? runtime.initializedRemoteWorkspaceHubForConfig(config);
|
|
59
|
+
try { await sessions?.shutdown(); } finally { hub?.closeAllConnections(); }
|
|
60
|
+
});
|
|
61
|
+
if (url.pathname === "/api/remote-workspace" && req.method === "GET") {
|
|
62
|
+
return response({
|
|
63
|
+
available: true,
|
|
64
|
+
devices: hub.listDevices(),
|
|
65
|
+
runtimes: await sessions.availability(),
|
|
66
|
+
sessions: sessions.list(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (url.pathname === "/api/remote-workspace/pairing" && req.method === "POST") {
|
|
70
|
+
const denied = sessionOnly(ctx);
|
|
71
|
+
if (denied) return denied;
|
|
72
|
+
try {
|
|
73
|
+
return response(hub.createPairingGrant(), 201);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
rethrowManagementBodyTooLarge(error);
|
|
76
|
+
return response({ error: error instanceof Error ? error.message : "Remote Workspace pairing failed." }, 429);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const match = /^\/api\/remote-workspace\/devices\/([0-9a-f-]+)$/i.exec(url.pathname);
|
|
80
|
+
if (match && req.method === "DELETE") {
|
|
81
|
+
const denied = sessionOnly(ctx);
|
|
82
|
+
if (denied) return denied;
|
|
83
|
+
if (!hub.revokeDevice(match[1]!)) return response({ error: "Remote Workspace device not found." }, 404);
|
|
84
|
+
await Promise.all(sessions.list()
|
|
85
|
+
.filter(session => session.deviceId === match[1] && session.status !== "stopped")
|
|
86
|
+
.map(session => sessions.stop(session.id)));
|
|
87
|
+
return response({ ok: true });
|
|
88
|
+
}
|
|
89
|
+
if (url.pathname === "/api/remote-workspace/sessions" && req.method === "GET") {
|
|
90
|
+
return response({ sessions: sessions.list() });
|
|
91
|
+
}
|
|
92
|
+
if (url.pathname === "/api/remote-workspace/runtimes" && req.method === "GET") {
|
|
93
|
+
return response({ runtimes: await sessions.availability() });
|
|
94
|
+
}
|
|
95
|
+
if (url.pathname === "/api/remote-workspace/sessions" && req.method === "POST") {
|
|
96
|
+
const denied = sessionOnly(ctx);
|
|
97
|
+
if (denied) return denied;
|
|
98
|
+
try {
|
|
99
|
+
const body = await jsonObject(req);
|
|
100
|
+
exactBodyKeys(body, ["profile", "deviceId", "rootId"], ["accessMode"]);
|
|
101
|
+
if (!isRemoteWorkspaceAgentProfile(body.profile)
|
|
102
|
+
|| typeof body.deviceId !== "string"
|
|
103
|
+
|| typeof body.rootId !== "string"
|
|
104
|
+
|| (body.accessMode !== undefined && body.accessMode !== "read-only" && body.accessMode !== "workspace")) {
|
|
105
|
+
throw new Error("profile, deviceId, rootId, and a valid accessMode are required.");
|
|
106
|
+
}
|
|
107
|
+
return response(await sessions.create({
|
|
108
|
+
profile: body.profile,
|
|
109
|
+
deviceId: body.deviceId,
|
|
110
|
+
rootId: body.rootId,
|
|
111
|
+
accessMode: body.accessMode === "workspace" ? "workspace" : "read-only",
|
|
112
|
+
}), 201);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
rethrowManagementBodyTooLarge(error);
|
|
115
|
+
return response({ error: error instanceof Error ? error.message : "Remote Workspace session failed." }, 409);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const promptMatch = /^\/api\/remote-workspace\/sessions\/([0-9a-f-]+)\/prompt$/i.exec(url.pathname);
|
|
119
|
+
if (promptMatch && req.method === "POST") {
|
|
120
|
+
const denied = sessionOnly(ctx);
|
|
121
|
+
if (denied) return denied;
|
|
122
|
+
try {
|
|
123
|
+
const body = await jsonObject(req);
|
|
124
|
+
exactBodyKeys(body, ["prompt"]);
|
|
125
|
+
return response(sessions.submitPrompt(promptMatch[1]!, body.prompt), 202);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
rethrowManagementBodyTooLarge(error);
|
|
128
|
+
return response({ error: error instanceof Error ? error.message : "Remote Workspace turn failed." }, 409);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const sessionMatch = /^\/api\/remote-workspace\/sessions\/([0-9a-f-]+)$/i.exec(url.pathname);
|
|
132
|
+
if (sessionMatch && req.method === "DELETE") {
|
|
133
|
+
const denied = sessionOnly(ctx);
|
|
134
|
+
if (denied) return denied;
|
|
135
|
+
return await sessions.stop(sessionMatch[1]!)
|
|
136
|
+
? response({ ok: true })
|
|
137
|
+
: response({ error: "Remote Workspace session not found." }, 404);
|
|
138
|
+
}
|
|
139
|
+
return response({ error: "Unknown Remote Workspace operation." }, 404);
|
|
140
|
+
}
|
|
@@ -91,11 +91,17 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
91
91
|
{ method: "GET", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: false },
|
|
92
92
|
{ method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api", mutates: false },
|
|
93
93
|
{ method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api", mutates: false },
|
|
94
|
+
{ method: "GET", path: "/api/codex-auth/quota/history", module: "codex/auth-api", mutates: false },
|
|
94
95
|
{ method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api", mutates: false },
|
|
95
96
|
{ method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true },
|
|
96
97
|
{ method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
|
|
97
98
|
{ method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api", mutates: true },
|
|
98
99
|
{ method: "POST", path: "/api/codex-auth/accounts/refresh", module: "codex/auth-api", mutates: true },
|
|
100
|
+
// codex/main-device-reauth-api (#3898): the native-main device reauth namespace;
|
|
101
|
+
// /api/codex-auth/login stays pool-only and keeps rejecting __main__.
|
|
102
|
+
{ method: "POST", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: true },
|
|
103
|
+
{ method: "GET", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: false },
|
|
104
|
+
{ method: "DELETE", path: "/api/codex-auth/main/reauth-device", module: "codex/main-device-reauth-api", mutates: true },
|
|
99
105
|
{ method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api", mutates: true },
|
|
100
106
|
{ method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api", mutates: true },
|
|
101
107
|
{ method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api", mutates: true },
|
|
@@ -322,6 +328,15 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
322
328
|
{ method: "POST", path: "/api/storage/codex-logs/protect", module: "server/management/storage-log-guard-routes", mutates: true },
|
|
323
329
|
{ method: "POST", path: "/api/storage/codex-logs/repair", module: "server/management/storage-log-guard-routes", mutates: true },
|
|
324
330
|
{ method: "POST", path: "/api/storage/codex-logs/unprotect", module: "server/management/storage-log-guard-routes", mutates: true },
|
|
331
|
+
// server/management/remote-workspace-routes
|
|
332
|
+
{ method: "GET", path: "/api/remote-workspace", module: "server/management/remote-workspace-routes", mutates: false, exempt: { reason: "deferred-verb", why: "The first Remote Workspace slice exposes Hub status through the authenticated dashboard; a distinct CLI Hub-status verb is still owed and must not be confused with the Executor-local status command.", owner: "remote-workspace-cli-followup", ownerDoc: "docs-site/src/content/docs/reference/management-api.md" } },
|
|
333
|
+
{ method: "GET", path: "/api/remote-workspace/runtimes", module: "server/management/remote-workspace-routes", mutates: false, exempt: { reason: "deferred-verb", why: "The first Remote Workspace slice exposes Hub runtime availability through the authenticated dashboard; a distinct CLI Hub-status verb is still owed.", owner: "remote-workspace-cli-followup", ownerDoc: "docs-site/src/content/docs/reference/management-api.md" } },
|
|
334
|
+
{ method: "GET", path: "/api/remote-workspace/sessions", module: "server/management/remote-workspace-routes", mutates: false, exempt: { reason: "deferred-verb", why: "The first Remote Workspace slice exposes Hub session snapshots through the authenticated dashboard; a distinct CLI Hub-status verb is still owed.", owner: "remote-workspace-cli-followup", ownerDoc: "docs-site/src/content/docs/reference/management-api.md" } },
|
|
335
|
+
{ method: "POST", path: "/api/remote-workspace/pairing", module: "server/management/remote-workspace-routes", mutates: true, exempt: { reason: "session-only", why: "Creating a device enrollment grant authorizes another computer, so only a dashboard consent session may request one." } },
|
|
336
|
+
{ method: "POST", path: "/api/remote-workspace/sessions", module: "server/management/remote-workspace-routes", mutates: true, exempt: { reason: "session-only", why: "Starting a Hub-authenticated model session against a remote computer requires an interactive dashboard consent session." } },
|
|
337
|
+
{ method: "POST", path: "/api/remote-workspace/sessions/{id}/prompt", module: "server/management/remote-workspace-routes", mutates: true, mechanism: "regex", exempt: { reason: "session-only", why: "A prompt can execute tools on the selected remote computer and therefore requires an interactive dashboard consent session." } },
|
|
338
|
+
{ method: "DELETE", path: "/api/remote-workspace/devices/{id}", module: "server/management/remote-workspace-routes", mutates: true, mechanism: "regex", exempt: { reason: "session-only", why: "Revoking a Remote Workspace computer is an interactive dashboard identity action, not an admin-token automation verb." } },
|
|
339
|
+
{ method: "DELETE", path: "/api/remote-workspace/sessions/{id}", module: "server/management/remote-workspace-routes", mutates: true, mechanism: "regex", exempt: { reason: "session-only", why: "Stopping an interactive Remote Workspace model session belongs to the dashboard session that controls it." } },
|
|
325
340
|
// server/management/system-routes
|
|
326
341
|
{ method: "GET", path: "/api/system/health", module: "server/management/system-routes", mutates: false },
|
|
327
342
|
{ method: "GET", path: "/api/system/memory", module: "server/management/system-routes", mutates: false },
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
|
|
24
24
|
interface RetainedUsageAggregate {
|
|
25
25
|
accumulator: UsageSummaryAccumulator;
|
|
26
|
+
usageIncomplete: boolean;
|
|
26
27
|
revision: UsageLogRevision | null;
|
|
27
28
|
identityKey: string;
|
|
28
29
|
revisionKey: string;
|
|
@@ -35,6 +36,7 @@ interface RetainedUsageAggregate {
|
|
|
35
36
|
|
|
36
37
|
export interface UsageAggregateResult {
|
|
37
38
|
accumulator: UsageSummaryAccumulator;
|
|
39
|
+
usageIncomplete: boolean;
|
|
38
40
|
revision: UsageLogRevision | null;
|
|
39
41
|
processedThroughBytes: number;
|
|
40
42
|
overlayVersion: number;
|
|
@@ -75,6 +77,7 @@ function resultFrom(
|
|
|
75
77
|
): UsageAggregateResult {
|
|
76
78
|
return {
|
|
77
79
|
accumulator: state.accumulator,
|
|
80
|
+
usageIncomplete: state.usageIncomplete,
|
|
78
81
|
revision: state.revision,
|
|
79
82
|
processedThroughBytes: state.processedThroughBytes,
|
|
80
83
|
overlayVersion: state.overlayVersion,
|
|
@@ -99,6 +102,7 @@ function makeRetainedAggregate(
|
|
|
99
102
|
): RetainedUsageAggregate {
|
|
100
103
|
return {
|
|
101
104
|
accumulator,
|
|
105
|
+
usageIncomplete: scan.oversizedRows > 0,
|
|
102
106
|
revision: scan.revision,
|
|
103
107
|
identityKey: usageLogIdentityKey(scan.revision),
|
|
104
108
|
revisionKey: usageLogRevisionKey(scan.revision),
|
|
@@ -126,9 +130,6 @@ async function rebuildAggregate(options: UsageAggregateOptions): Promise<UsageAg
|
|
|
126
130
|
apiKeyAccumulator?.add(entry);
|
|
127
131
|
},
|
|
128
132
|
});
|
|
129
|
-
if (scan.oversizedRows > 0) {
|
|
130
|
-
throw new Error("usage ledger contains an oversized row");
|
|
131
|
-
}
|
|
132
133
|
if (userCostOverlayVersion() !== overlayVersion || currentTimeZone() !== timeZone) {
|
|
133
134
|
lastError = new Error("usage aggregation inputs changed during rebuild");
|
|
134
135
|
continue;
|
|
@@ -138,7 +139,10 @@ async function rebuildAggregate(options: UsageAggregateOptions): Promise<UsageAg
|
|
|
138
139
|
const result = publishRetainedAggregate(state);
|
|
139
140
|
if (apiKeyAccumulator && options.configuredApiKeyIds) {
|
|
140
141
|
cacheApiKeyUsageFromRollup(
|
|
141
|
-
|
|
142
|
+
{
|
|
143
|
+
...apiKeyAccumulator.snapshot(),
|
|
144
|
+
...(state.usageIncomplete ? { usageIncomplete: true as const, usageIncompleteReason: "oversized_rows" as const } : {}),
|
|
145
|
+
},
|
|
142
146
|
options.configuredApiKeyIds,
|
|
143
147
|
state.identityKey,
|
|
144
148
|
state.revision?.size ?? 0,
|
|
@@ -180,8 +184,8 @@ async function appendAggregate(
|
|
|
180
184
|
let rebuildAfterUnpin = false;
|
|
181
185
|
try {
|
|
182
186
|
// Clone first and publish only after the scanner verifies the captured
|
|
183
|
-
// suffix. A callback error
|
|
184
|
-
//
|
|
187
|
+
// suffix. A callback error or mutation leaves retained state untouched.
|
|
188
|
+
// Skipped oversized rows retain the normal rows with an explicit diagnostic.
|
|
185
189
|
const candidate = state.accumulator.clone();
|
|
186
190
|
const scan = await scanUsageLedgerCooperatively({
|
|
187
191
|
startAtBytes: state.processedThroughBytes,
|
|
@@ -189,10 +193,6 @@ async function appendAggregate(
|
|
|
189
193
|
expectedProcessedThroughDigest: state.processedThroughDigest,
|
|
190
194
|
onEntry: entry => candidate.add(entry),
|
|
191
195
|
});
|
|
192
|
-
if (scan.oversizedRows > 0) {
|
|
193
|
-
if (retainedAggregate === state) retainedAggregate = null;
|
|
194
|
-
throw new Error("usage ledger contains an oversized row");
|
|
195
|
-
}
|
|
196
196
|
if (userCostOverlayVersion() !== state.overlayVersion || currentTimeZone() !== state.timeZone) {
|
|
197
197
|
if (retainedAggregate === state) retainedAggregate = null;
|
|
198
198
|
rebuildAfterUnpin = true;
|
|
@@ -200,6 +200,9 @@ async function appendAggregate(
|
|
|
200
200
|
const next: RetainedUsageAggregate = {
|
|
201
201
|
...state,
|
|
202
202
|
accumulator: candidate,
|
|
203
|
+
// A partial unterminated row can be scanned again on the next append.
|
|
204
|
+
// Preserve a boolean diagnostic rather than double-counting omissions.
|
|
205
|
+
usageIncomplete: state.usageIncomplete || scan.oversizedRows > 0,
|
|
203
206
|
revision: scan.revision,
|
|
204
207
|
identityKey: usageLogIdentityKey(scan.revision),
|
|
205
208
|
revisionKey: usageLogRevisionKey(scan.revision),
|
|
@@ -329,7 +332,6 @@ async function rebuildFilteredAggregate(
|
|
|
329
332
|
const accumulator = createUsageSummaryAccumulator({ filter, mode: "row-unique", window });
|
|
330
333
|
try {
|
|
331
334
|
const scan = await scanUsageLedgerCooperatively({ onEntry: entry => accumulator.add(entry) });
|
|
332
|
-
if (scan.oversizedRows > 0) throw new Error("usage ledger contains an oversized row");
|
|
333
335
|
if (userCostOverlayVersion() !== overlayVersion || currentTimeZone() !== timeZone) {
|
|
334
336
|
lastError = new Error("usage aggregation inputs changed during filtered scan");
|
|
335
337
|
continue;
|
|
@@ -362,10 +364,6 @@ async function appendFilteredAggregate(
|
|
|
362
364
|
expectedProcessedThroughDigest: state.processedThroughDigest,
|
|
363
365
|
onEntry: entry => candidate.add(entry),
|
|
364
366
|
});
|
|
365
|
-
if (scan.oversizedRows > 0) {
|
|
366
|
-
if (retainedFilteredAggregates.get(key) === state) retainedFilteredAggregates.delete(key);
|
|
367
|
-
throw new Error("usage ledger contains an oversized row");
|
|
368
|
-
}
|
|
369
367
|
if (userCostOverlayVersion() !== state.overlayVersion || currentTimeZone() !== state.timeZone) {
|
|
370
368
|
if (retainedFilteredAggregates.get(key) === state) retainedFilteredAggregates.delete(key);
|
|
371
369
|
rebuildAfterUnpin = true;
|
|
@@ -373,6 +371,7 @@ async function appendFilteredAggregate(
|
|
|
373
371
|
const next: RetainedUsageAggregate = {
|
|
374
372
|
...state,
|
|
375
373
|
accumulator: candidate,
|
|
374
|
+
usageIncomplete: state.usageIncomplete || scan.oversizedRows > 0,
|
|
376
375
|
revision: scan.revision,
|
|
377
376
|
identityKey: usageLogIdentityKey(scan.revision),
|
|
378
377
|
revisionKey: usageLogRevisionKey(scan.revision),
|
|
@@ -2,6 +2,8 @@ import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../../l
|
|
|
2
2
|
import type { UsageSummary } from "../../usage/summary";
|
|
3
3
|
|
|
4
4
|
export type CachedUsageSummary = UsageSummary & {
|
|
5
|
+
usageIncomplete?: true;
|
|
6
|
+
usageIncompleteReason?: "oversized_rows";
|
|
5
7
|
historyTruncated: boolean;
|
|
6
8
|
truncatedPrefixBytes: number;
|
|
7
9
|
entriesTruncated: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { remoteWorkspaceEnabled } from "../remote-control/workspace-activation";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import type { CatalogModel } from "../codex/catalog";
|
|
@@ -149,6 +150,20 @@ async function handleGrokCouponRoutesOnDemand(ctx: ManagementContext): Promise<R
|
|
|
149
150
|
return handleGrokCouponRoutes(ctx);
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
async function handleRemoteWorkspaceRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
|
|
154
|
+
if (!pathInManagementNamespace(ctx.url.pathname, "/api/remote-workspace")) return null;
|
|
155
|
+
if (!remoteWorkspaceEnabled(ctx.config)) {
|
|
156
|
+
return Response.json({ available: false, reason: "Remote Workspace requires Hub mode and OCX_REMOTE_WORKSPACE_ENABLED=1.", devices: [], runtimes: {}, sessions: [] }, {
|
|
157
|
+
status: ctx.req.method === "GET" ? 200 : 404, headers: { "cache-control": "no-store" },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (ctx.req.method !== "GET" && ctx.principal !== "gui-session") {
|
|
161
|
+
return Response.json({ error: "A dashboard session is required for Remote Workspace changes." }, { status: 403 });
|
|
162
|
+
}
|
|
163
|
+
const { handleRemoteWorkspaceRoutes } = await import("./management/remote-workspace-routes");
|
|
164
|
+
return handleRemoteWorkspaceRoutes(ctx);
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
export async function handleManagementAPI(
|
|
153
168
|
req: Request,
|
|
154
169
|
url: URL,
|
|
@@ -242,6 +257,7 @@ export async function handleManagementAPI(
|
|
|
242
257
|
let routed: Response | null;
|
|
243
258
|
try {
|
|
244
259
|
routed = handleSessionRoutes(ctx)
|
|
260
|
+
?? (await handleRemoteWorkspaceRoutesOnDemand(ctx))
|
|
245
261
|
?? (await handleConfigRoutes(ctx))
|
|
246
262
|
?? (await handleStorageLogGuardRoutes(ctx))
|
|
247
263
|
?? (await handleLogsUsageRoutes(ctx))
|
|
@@ -388,6 +404,13 @@ export async function handleManagementAPI(
|
|
|
388
404
|
}
|
|
389
405
|
|
|
390
406
|
if (url.pathname.startsWith("/api/codex-auth/")) {
|
|
407
|
+
// Native-main device reauth (#3898): a dedicated namespace the generic
|
|
408
|
+
// codex-auth dispatch must not swallow (it would 404 as an unknown pool
|
|
409
|
+
// route). Same management origin/auth/session wrapping as every /api/*.
|
|
410
|
+
if (url.pathname === "/api/codex-auth/main/reauth-device") {
|
|
411
|
+
const { handleMainDeviceReauthAPI } = await import("../codex/main-device-reauth-api");
|
|
412
|
+
return handleMainDeviceReauthAPI(req, url, config);
|
|
413
|
+
}
|
|
391
414
|
const { handleCodexAuthAPI } = await import("../codex/auth-api");
|
|
392
415
|
const { ConfigMutationLockError } = await import("../config");
|
|
393
416
|
const { CodexCredentialRefreshLockTimeoutError } = await import("../codex/account-store");
|
package/src/server/ports.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
2
|
|
|
3
|
+
/** An auxiliary bind cannot be repaired by selecting a different public port. */
|
|
4
|
+
export class AuxiliaryListenerBindError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
readonly listener: "unauthenticatedLoopbackListener" | "hub.managementIngress",
|
|
7
|
+
readonly port: number,
|
|
8
|
+
readonly hostname: string,
|
|
9
|
+
cause: unknown,
|
|
10
|
+
) {
|
|
11
|
+
super(
|
|
12
|
+
`Could not bind ${listener} at ${hostname}:${port}. `
|
|
13
|
+
+ `Check that listener's address and port; the public proxy port was not retried.`,
|
|
14
|
+
{ cause },
|
|
15
|
+
);
|
|
16
|
+
this.name = "AuxiliaryListenerBindError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
/** Temporary bind probes must not let accepted peers hold server.close() open. */
|
|
4
21
|
function createProbeServer(): ReturnType<typeof createServer> {
|
|
5
22
|
const server = createServer();
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
28
|
adapterEofIncompleteFrame,
|
|
29
|
+
type CodexSafetyBufferingFilterOptions,
|
|
29
30
|
createSseTerminalOutputBoundary,
|
|
30
31
|
doneFrame,
|
|
31
32
|
failedTailFrame,
|
|
@@ -84,6 +85,8 @@ export type EagerRelayOptions = {
|
|
|
84
85
|
postCancelDrainBytes?: number;
|
|
85
86
|
/** Last known upstream failure to preserve when EOF would otherwise become adapter_eof. */
|
|
86
87
|
upstreamError?: string;
|
|
88
|
+
/** Optional client-facing hint policy; inspection retains original frames. */
|
|
89
|
+
terminalBoundary?: CodexSafetyBufferingFilterOptions;
|
|
87
90
|
/** Injectable clock for tests. */
|
|
88
91
|
now?: () => number;
|
|
89
92
|
};
|
|
@@ -114,7 +117,7 @@ export function relaySseEagerBounded(
|
|
|
114
117
|
const terminalEncoder = new TextEncoder();
|
|
115
118
|
const adapterEofFrame = adapterEofIncompleteFrame(terminalEncoder);
|
|
116
119
|
const terminalSentinel = doneFrame(terminalEncoder);
|
|
117
|
-
const terminalBoundary = createSseTerminalOutputBoundary();
|
|
120
|
+
const terminalBoundary = createSseTerminalOutputBoundary(opts?.terminalBoundary);
|
|
118
121
|
const activeRewrite: SseBlockRewrite | undefined = hooks.rewriteBlocks
|
|
119
122
|
?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
|
|
120
123
|
const encodeFailedTail = (error: unknown): Uint8Array | null => {
|