@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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { RemoteWorkspaceDeviceState } from "../remote-control/workspace-device";
|
|
2
|
+
import {
|
|
3
|
+
RemoteWorkspaceDeviceFileStore,
|
|
4
|
+
pairRemoteWorkspaceDevice,
|
|
5
|
+
remoteWorkspaceCapabilitiesForCommandRunner,
|
|
6
|
+
runRemoteWorkspaceAgent,
|
|
7
|
+
type PairRemoteWorkspaceDeviceOptions,
|
|
8
|
+
type RemoteWorkspaceAgentRunStatus,
|
|
9
|
+
type RemoteWorkspaceDeviceStateStore,
|
|
10
|
+
} from "../remote-control/workspace-device";
|
|
11
|
+
import { createPlatformRemoteWorkspaceCommandRunner } from "../remote-control/workspace-command-runner";
|
|
12
|
+
import {
|
|
13
|
+
CliUsageError,
|
|
14
|
+
readSecretLine,
|
|
15
|
+
rejectArgs,
|
|
16
|
+
takeFlag,
|
|
17
|
+
takeJsonFlag,
|
|
18
|
+
takeOption,
|
|
19
|
+
type RuntimeApiDeps,
|
|
20
|
+
} from "./runtime-api";
|
|
21
|
+
|
|
22
|
+
export const REMOTE_WORKSPACE_USAGE = `Usage:
|
|
23
|
+
ocx remote-workspace pair <hub-url> --pairing-code-stdin --root <absolute-path> [--root <absolute-path> ...] [--toolchain-root <absolute-directory> ...] [--executor-helper <absolute-file>] [--name <device-name>] [--json]
|
|
24
|
+
ocx remote-workspace agent
|
|
25
|
+
ocx remote-workspace status [--json]`;
|
|
26
|
+
|
|
27
|
+
export interface RemoteWorkspaceCliDeps extends RuntimeApiDeps {
|
|
28
|
+
store?: RemoteWorkspaceDeviceStateStore;
|
|
29
|
+
pair?: (options: PairRemoteWorkspaceDeviceOptions) => Promise<RemoteWorkspaceDeviceState>;
|
|
30
|
+
runAgent?: typeof runRemoteWorkspaceAgent;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
onStatus?: (status: RemoteWorkspaceAgentRunStatus) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function takeRepeatedPathFlag(args: string[], flag: "--root" | "--toolchain-root"): string[] {
|
|
36
|
+
const roots: string[] = [];
|
|
37
|
+
for (;;) {
|
|
38
|
+
const index = args.indexOf(flag);
|
|
39
|
+
if (index < 0) break;
|
|
40
|
+
const value = args[index + 1];
|
|
41
|
+
if (!value || value.startsWith("--")) throw new CliUsageError(`${flag} requires an absolute path`, REMOTE_WORKSPACE_USAGE);
|
|
42
|
+
roots.push(value);
|
|
43
|
+
args.splice(index, 2);
|
|
44
|
+
}
|
|
45
|
+
return roots;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function publicStatus(state: RemoteWorkspaceDeviceState | null): Record<string, unknown> {
|
|
49
|
+
if (!state) return { paired: false };
|
|
50
|
+
const capabilities = remoteWorkspaceCapabilitiesForCommandRunner(
|
|
51
|
+
createPlatformRemoteWorkspaceCommandRunner({
|
|
52
|
+
linux: {
|
|
53
|
+
toolchainRoots: state.toolchainRoots,
|
|
54
|
+
writableRoots: state.roots.map(root => root.path),
|
|
55
|
+
},
|
|
56
|
+
...(state.nativeHelper ? { native: {
|
|
57
|
+
helper: state.nativeHelper,
|
|
58
|
+
toolchainRoots: state.toolchainRoots,
|
|
59
|
+
writableRoots: state.roots.map(root => root.path),
|
|
60
|
+
} } : {}),
|
|
61
|
+
}),
|
|
62
|
+
state.capabilities,
|
|
63
|
+
);
|
|
64
|
+
return {
|
|
65
|
+
paired: true,
|
|
66
|
+
hubUrl: state.hubUrl,
|
|
67
|
+
deviceId: state.deviceId,
|
|
68
|
+
deviceName: state.deviceName,
|
|
69
|
+
devicePlatform: state.devicePlatform,
|
|
70
|
+
capabilities,
|
|
71
|
+
roots: state.roots.map(root => ({ id: root.id, label: root.label, path: root.path })),
|
|
72
|
+
toolchainRoots: state.toolchainRoots,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runRemoteWorkspaceCommand(rawArgs: string[], deps: RemoteWorkspaceCliDeps = {}): Promise<number> {
|
|
77
|
+
const args = [...rawArgs];
|
|
78
|
+
const command = args.shift();
|
|
79
|
+
const store = deps.store ?? new RemoteWorkspaceDeviceFileStore();
|
|
80
|
+
if (command === "status") {
|
|
81
|
+
const wantsJson = takeJsonFlag(args);
|
|
82
|
+
rejectArgs(args, REMOTE_WORKSPACE_USAGE);
|
|
83
|
+
const status = publicStatus(store.load());
|
|
84
|
+
if (wantsJson) console.log(JSON.stringify(status, null, 2));
|
|
85
|
+
else if (!status.paired) console.log("Remote Workspace executor is not paired.");
|
|
86
|
+
else {
|
|
87
|
+
console.log(`Remote Workspace executor: ${status.deviceName}`);
|
|
88
|
+
console.log(`Hub: ${status.hubUrl}`);
|
|
89
|
+
console.log(`Capabilities: ${(status.capabilities as string[]).join(", ")}`);
|
|
90
|
+
console.log(`Workspace roots: ${(status.roots as unknown[]).length}`);
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
if (command === "pair") {
|
|
95
|
+
const wantsJson = takeJsonFlag(args);
|
|
96
|
+
const readCode = takeFlag(args, "--pairing-code-stdin");
|
|
97
|
+
const name = takeOption(args, "--name");
|
|
98
|
+
const nativeHelperPath = takeOption(args, "--executor-helper");
|
|
99
|
+
const roots = takeRepeatedPathFlag(args, "--root");
|
|
100
|
+
const toolchainRoots = takeRepeatedPathFlag(args, "--toolchain-root");
|
|
101
|
+
const hubUrl = args.shift();
|
|
102
|
+
if (!hubUrl || !readCode || roots.length === 0) throw new CliUsageError(
|
|
103
|
+
"pair requires <hub-url>, --pairing-code-stdin, and at least one --root",
|
|
104
|
+
REMOTE_WORKSPACE_USAGE,
|
|
105
|
+
);
|
|
106
|
+
rejectArgs(args, REMOTE_WORKSPACE_USAGE, { redactValues: true });
|
|
107
|
+
const pairingCode = await readSecretLine(deps, "Remote Workspace pairing code");
|
|
108
|
+
const state = await (deps.pair ?? pairRemoteWorkspaceDevice)({
|
|
109
|
+
hubUrl,
|
|
110
|
+
pairingCode,
|
|
111
|
+
...(name ? { name } : {}),
|
|
112
|
+
roots: roots.map(path => ({ path })),
|
|
113
|
+
toolchainRoots,
|
|
114
|
+
...(nativeHelperPath ? { nativeHelperPath } : {}),
|
|
115
|
+
store,
|
|
116
|
+
});
|
|
117
|
+
const status = publicStatus(state);
|
|
118
|
+
if (wantsJson) console.log(JSON.stringify(status, null, 2));
|
|
119
|
+
else {
|
|
120
|
+
console.log(`Paired ${state.deviceName} with ${state.hubUrl}.`);
|
|
121
|
+
console.log("Run `ocx remote-workspace agent` to keep this executor online.");
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (command === "agent") {
|
|
126
|
+
rejectArgs(args, REMOTE_WORKSPACE_USAGE);
|
|
127
|
+
const state = store.load();
|
|
128
|
+
if (!state) throw new CliUsageError("Remote Workspace executor is not paired. Run the pair command first.", REMOTE_WORKSPACE_USAGE);
|
|
129
|
+
const controller = deps.signal ? null : new AbortController();
|
|
130
|
+
const signal = deps.signal ?? controller!.signal;
|
|
131
|
+
const stop = () => controller?.abort();
|
|
132
|
+
if (controller) {
|
|
133
|
+
process.once("SIGINT", stop);
|
|
134
|
+
process.once("SIGTERM", stop);
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await (deps.runAgent ?? runRemoteWorkspaceAgent)({
|
|
138
|
+
state,
|
|
139
|
+
signal,
|
|
140
|
+
onStatus: deps.onStatus ?? (status => {
|
|
141
|
+
if (status.state === "online") console.log(`Remote Workspace executor online: ${state.deviceName}`);
|
|
142
|
+
if (status.state === "reconnecting" && status.message) console.error(`Remote Workspace reconnecting: ${status.message}`);
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
} finally {
|
|
146
|
+
if (controller) {
|
|
147
|
+
process.removeListener("SIGINT", stop);
|
|
148
|
+
process.removeListener("SIGTERM", stop);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
throw new CliUsageError("choose pair, agent, or status", REMOTE_WORKSPACE_USAGE);
|
|
154
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { effectiveLoopbackListenerPort } from "../codex/loopback-target";
|
|
|
18
18
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
19
19
|
import { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy, type ClaudeDesktopPolicyHealth } from "../claude/desktop-policy";
|
|
20
20
|
import { collectClientConnectionStatus, type ClientConnectionStatus } from "./connect";
|
|
21
|
+
import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
|
|
21
22
|
import type { HubStateOAuthEntry, HubStateProvider } from "../remote/hub-state";
|
|
22
23
|
import type { HubStateSource } from "../client/hub-state";
|
|
23
24
|
import { readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
@@ -324,6 +325,35 @@ export function disconnectedRemoteHubStatus(): CliRemoteHubStatus {
|
|
|
324
325
|
};
|
|
325
326
|
}
|
|
326
327
|
|
|
328
|
+
/**
|
|
329
|
+
* The data key this status snapshot may spend, and the cause when it may not.
|
|
330
|
+
*
|
|
331
|
+
* A reconnect or a rotation can replace both files between the snapshot and this read, so a
|
|
332
|
+
* matching cache owner alone does not authorize sending the current token. Withholding is only
|
|
333
|
+
* half the job: reporting every withheld case as "no usable data-plane token" is false for a
|
|
334
|
+
* client that reconnected and holds a perfectly good token for a different hub, and a cause the
|
|
335
|
+
* operator acts on has to be the real one (#4169 is the same defect in the stop path).
|
|
336
|
+
*/
|
|
337
|
+
function boundHubStateToken(
|
|
338
|
+
current: ReturnType<typeof readClientConnectionState>,
|
|
339
|
+
token: ReturnType<typeof readServiceApiTokenState>,
|
|
340
|
+
owner: { serverUrl: string; apiKeyId: string; connectedAt: string },
|
|
341
|
+
): { token: string | null; withheldReason?: string } {
|
|
342
|
+
if (current.kind !== "connected") {
|
|
343
|
+
return { token: null, withheldReason: "this client is no longer connected" };
|
|
344
|
+
}
|
|
345
|
+
if (!sameClientConnectionOwner(current.value, owner)) {
|
|
346
|
+
return { token: null, withheldReason: "the saved connection no longer matches the one this status reports" };
|
|
347
|
+
}
|
|
348
|
+
if (token.kind !== "present") {
|
|
349
|
+
return { token: null, withheldReason: "this client has no usable data-plane token" };
|
|
350
|
+
}
|
|
351
|
+
if (token.fingerprint !== current.value.tokenFingerprint) {
|
|
352
|
+
return { token: null, withheldReason: "the data-plane token no longer belongs to the saved connection" };
|
|
353
|
+
}
|
|
354
|
+
return { token: token.token };
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
/**
|
|
328
358
|
* Ask the hub what it can serve, with a bounded read and a cache fallback.
|
|
329
359
|
*
|
|
@@ -340,14 +370,16 @@ export async function collectRemoteHubStatus(
|
|
|
340
370
|
return disconnectedRemoteHubStatus();
|
|
341
371
|
}
|
|
342
372
|
const { resolveHubState } = await import("../client/hub-state");
|
|
343
|
-
const
|
|
373
|
+
const owner = {
|
|
374
|
+
serverUrl: connection.serverUrl,
|
|
375
|
+
apiKeyId: connection.apiKeyId,
|
|
376
|
+
connectedAt: connection.connectedAt,
|
|
377
|
+
};
|
|
378
|
+
const bound = boundHubStateToken(readClientConnectionState(), readServiceApiTokenState(), owner);
|
|
344
379
|
const resolved = await resolveHubState({
|
|
345
|
-
owner
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
connectedAt: connection.connectedAt,
|
|
349
|
-
},
|
|
350
|
-
token: token.kind === "present" ? token.token : null,
|
|
380
|
+
owner,
|
|
381
|
+
token: bound.token,
|
|
382
|
+
...(bound.withheldReason ? { withheldTokenReason: bound.withheldReason } : {}),
|
|
351
383
|
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
352
384
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
353
385
|
...(options.now === undefined ? {} : { now: options.now }),
|
package/src/cli/usage-report.ts
CHANGED
|
@@ -21,6 +21,10 @@ interface CostRow {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface UsageReportInput {
|
|
24
|
+
source?: "hub";
|
|
25
|
+
scope?: "client";
|
|
26
|
+
usageIncomplete?: true;
|
|
27
|
+
usageIncompleteReason?: "oversized_rows";
|
|
24
28
|
range?: string;
|
|
25
29
|
surface?: string;
|
|
26
30
|
since?: number | null;
|
|
@@ -109,11 +113,19 @@ function describeScope(data: UsageReportInput): string {
|
|
|
109
113
|
export function formatUsageReport(data: UsageReportInput): string[] {
|
|
110
114
|
const summary = data.summary ?? {};
|
|
111
115
|
const lines: string[] = [describeScope(data), ""];
|
|
116
|
+
if (data.source === "hub" && data.scope === "client") {
|
|
117
|
+
lines.push("Source: hub — this client's data key only. Account totals are not shared.", "");
|
|
118
|
+
}
|
|
119
|
+
if (data.usageIncomplete === true) {
|
|
120
|
+
lines.push("WARNING: Usage is incomplete; some records could not be included. Totals and rankings reflect readable records only.", "");
|
|
121
|
+
}
|
|
112
122
|
|
|
113
123
|
if (data.filter && !data.filter.matched) {
|
|
114
124
|
const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
|
|
115
125
|
.filter(Boolean).join(" and ");
|
|
116
|
-
lines.push(
|
|
126
|
+
lines.push(data.usageIncomplete === true
|
|
127
|
+
? `No matching readable usage records for ${terminalText(what)} in this range; skipped records may contain matches.`
|
|
128
|
+
: `No usage recorded for ${terminalText(what)} in this range.`);
|
|
117
129
|
lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
|
|
118
130
|
return lines.map(terminalText);
|
|
119
131
|
}
|
|
@@ -151,7 +163,7 @@ export function formatUsageReport(data: UsageReportInput): string[] {
|
|
|
151
163
|
// server cannot answer it honestly.
|
|
152
164
|
const accountFilterActive = Boolean(data.filter?.provider || data.filter?.model);
|
|
153
165
|
const accounts = (data.accounts ?? []).filter(row => row.requests > 0);
|
|
154
|
-
if (accountFilterActive) {
|
|
166
|
+
if (accountFilterActive && data.scope !== "client") {
|
|
155
167
|
lines.push("");
|
|
156
168
|
lines.push("ACCOUNT: not reported under a provider or model filter; run without filters for per-account totals.");
|
|
157
169
|
} else if (accounts.length > 0) {
|
package/src/client/hub-client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MAX_REMOTE_CATALOG_BYTES } from "../server/catalog-download";
|
|
2
2
|
import { MAX_HUB_STATE_BYTES, parseHubStateBody, type HubStateDTO } from "../remote/hub-state";
|
|
3
|
+
import { MAX_HUB_USAGE_BYTES, parseHubUsage, type HubUsageReport } from "../remote/hub-usage";
|
|
3
4
|
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
4
5
|
import { clearableDeadline } from "../lib/abort";
|
|
5
6
|
import type { Desktop3pModelEntry } from "../claude/desktop-3p";
|
|
@@ -472,6 +473,39 @@ export async function downloadClientCatalog(
|
|
|
472
473
|
return { kind: "fresh", body, ...(keyId ? { keyId } : {}) };
|
|
473
474
|
}
|
|
474
475
|
|
|
476
|
+
/** Bounded own-key usage read; never falls back to a local management endpoint. */
|
|
477
|
+
export async function fetchHubUsage(
|
|
478
|
+
serverUrl: string,
|
|
479
|
+
admissionToken: string,
|
|
480
|
+
query: URLSearchParams,
|
|
481
|
+
options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
|
|
482
|
+
): Promise<HubUsageReport> {
|
|
483
|
+
const origin = normalizeHubOrigin(serverUrl);
|
|
484
|
+
if (!isPairingTransportPermitted(origin)) {
|
|
485
|
+
throw new HubClientError("insecure_http_refused", "Client usage requires HTTPS or loopback HTTP");
|
|
486
|
+
}
|
|
487
|
+
const response = await fetchBounded(options.fetchImpl ?? fetch, `${origin}/v1/usage?${query}`, {
|
|
488
|
+
method: "GET",
|
|
489
|
+
cache: "no-store",
|
|
490
|
+
headers: new Headers({ Accept: "application/json", "x-opencodex-api-key": admissionToken }),
|
|
491
|
+
}, options.timeoutMs);
|
|
492
|
+
if (!response.ok) {
|
|
493
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
494
|
+
const message = response.status === 404 ? "Hub does not support client usage; upgrade the hub"
|
|
495
|
+
: response.status === 401 || response.status === 403 ? "Hub rejected this client's usage credential"
|
|
496
|
+
: `Hub usage request failed (${response.status})`;
|
|
497
|
+
throw new HubClientError(`hub_usage_http_${response.status}`, message, response.status);
|
|
498
|
+
}
|
|
499
|
+
if (!jsonCompatibleContentType(response)) {
|
|
500
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
501
|
+
throw new HubClientError("hub_usage_invalid", "Hub usage response was not JSON");
|
|
502
|
+
}
|
|
503
|
+
const text = await boundedText(response, MAX_HUB_USAGE_BYTES, { inactivityTimeoutMs: safeTimeout(options.timeoutMs) });
|
|
504
|
+
const report = parseHubUsage(parseJson(text, "hub_usage_invalid"));
|
|
505
|
+
if (!report) throw new HubClientError("hub_usage_invalid", "Hub usage response was invalid");
|
|
506
|
+
return report;
|
|
507
|
+
}
|
|
508
|
+
|
|
475
509
|
/**
|
|
476
510
|
* Read the hub's provider/login/roster state with the per-client DATA key (#4236).
|
|
477
511
|
*
|
package/src/client/hub-state.ts
CHANGED
|
@@ -161,6 +161,12 @@ export interface ResolveHubStateOptions {
|
|
|
161
161
|
owner: HubStateOwner;
|
|
162
162
|
/** The per-client data key. Null when the token file is missing or unsafe. */
|
|
163
163
|
token: string | null;
|
|
164
|
+
/**
|
|
165
|
+
* Why the caller withheld the token, when it withheld one it holds. A null token reads the
|
|
166
|
+
* same here whether the file is missing or the caller declined to send a perfectly good
|
|
167
|
+
* token for a different connection, and the operator acts on that difference.
|
|
168
|
+
*/
|
|
169
|
+
withheldTokenReason?: string;
|
|
164
170
|
timeoutMs?: number;
|
|
165
171
|
fetchImpl?: typeof fetch;
|
|
166
172
|
now?: number;
|
|
@@ -195,7 +201,9 @@ export async function resolveHubState(options: ResolveHubStateOptions): Promise<
|
|
|
195
201
|
? withAge("cache", cached.state, cached.fetchedAt, now, reason)
|
|
196
202
|
: withAge("unavailable", null, undefined, now, reason);
|
|
197
203
|
};
|
|
198
|
-
if (!options.token)
|
|
204
|
+
if (!options.token) {
|
|
205
|
+
return fromCache(options.withheldTokenReason ?? "this client has no usable data-plane token");
|
|
206
|
+
}
|
|
199
207
|
if (options.allowNetwork === false) return fromCache("a live hub read was not attempted");
|
|
200
208
|
let state: HubStateDTO;
|
|
201
209
|
try {
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
14
14
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
15
15
|
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
16
|
+
import { isValidCodexAccountId } from "./account-id";
|
|
17
|
+
import type { PoolQuotaWriter } from "./quota-types";
|
|
16
18
|
import { CODEX_REFRESH_FLIGHT_CEILING_MS } from "./quota-recovery-timing";
|
|
17
19
|
|
|
18
20
|
type LegacyCodexAccountStore = Record<string, CodexAccountCredentials>;
|
|
@@ -163,6 +165,7 @@ export function saveCodexAccountCredential(
|
|
|
163
165
|
generation: (current?.generation ?? 0) + 1,
|
|
164
166
|
refreshGrantFingerprint,
|
|
165
167
|
replacedAt: current ? Date.now() : undefined,
|
|
168
|
+
quotaHistoryIdentity: crypto.randomUUID(),
|
|
166
169
|
...preservedValidationMetadata(current),
|
|
167
170
|
...(options.validationPending ? {
|
|
168
171
|
codexValidationPending: true,
|
|
@@ -257,6 +260,75 @@ export function readCodexAccountRecord(id: string): CodexAccountCredentialRecord
|
|
|
257
260
|
return loadCodexAccountRecordStore()[id] ?? null;
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
const QUOTA_HISTORY_IDENTITY_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
264
|
+
|
|
265
|
+
function validQuotaHistoryIdentity(value: unknown): value is string {
|
|
266
|
+
return typeof value === "string" && QUOTA_HISTORY_IDENTITY_RE.test(value);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type DispatchedPoolCredential = Pick<CodexAccountCredentials, "accessToken" | "chatgptAccountId"> & { generation: number };
|
|
270
|
+
|
|
271
|
+
function matchesDispatchedPoolCredential(record: CodexAccountCredentialRecord | undefined | null, dispatched: DispatchedPoolCredential): record is CodexAccountCredentialRecord & { credential: CodexAccountCredentials } {
|
|
272
|
+
return !!record?.credential && record.deletedAt == null
|
|
273
|
+
&& dispatched.accessToken.length > 0 && dispatched.chatgptAccountId.length > 0
|
|
274
|
+
&& Number.isSafeInteger(dispatched.generation) && dispatched.generation >= 0
|
|
275
|
+
&& record.generation === dispatched.generation
|
|
276
|
+
&& record.credential.accessToken === dispatched.accessToken
|
|
277
|
+
&& record.credential.chatgptAccountId === dispatched.chatgptAccountId;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Optional evidence capture; a stale credential or unavailable store never gains a new writer. */
|
|
281
|
+
export function capturePoolQuotaWriter(accountId: string, dispatched: DispatchedPoolCredential): PoolQuotaWriter | undefined {
|
|
282
|
+
if (!isValidCodexAccountId(accountId)) return undefined;
|
|
283
|
+
try {
|
|
284
|
+
const current = readCodexAccountRecord(accountId);
|
|
285
|
+
if (!matchesDispatchedPoolCredential(current, dispatched)) return undefined;
|
|
286
|
+
if (validQuotaHistoryIdentity(current.quotaHistoryIdentity)) {
|
|
287
|
+
return { accountId, credentialGeneration: dispatched.generation, historyIdentity: current.quotaHistoryIdentity };
|
|
288
|
+
}
|
|
289
|
+
return withCredentialMutationLockSync(() => {
|
|
290
|
+
const store = loadCodexAccountRecordStore();
|
|
291
|
+
const locked = store[accountId];
|
|
292
|
+
if (!matchesDispatchedPoolCredential(locked, dispatched)) return undefined;
|
|
293
|
+
if (!validQuotaHistoryIdentity(locked.quotaHistoryIdentity)) {
|
|
294
|
+
locked.quotaHistoryIdentity = crypto.randomUUID();
|
|
295
|
+
// Identity metadata is not a new credential; preserve generation and mutation epoch.
|
|
296
|
+
persist(store);
|
|
297
|
+
}
|
|
298
|
+
return { accountId, credentialGeneration: dispatched.generation, historyIdentity: locked.quotaHistoryIdentity };
|
|
299
|
+
});
|
|
300
|
+
} catch {
|
|
301
|
+
// History is optional evidence. Permission, lock and disk errors cannot fail inference.
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Read-only retention identity; unlike capture this never initializes legacy metadata. */
|
|
307
|
+
export function poolQuotaHistoryIdentity(accountId: string): string | undefined {
|
|
308
|
+
if (!isValidCodexAccountId(accountId)) return undefined;
|
|
309
|
+
try {
|
|
310
|
+
const record = readCodexAccountRecord(accountId);
|
|
311
|
+
return record?.credential && record.deletedAt == null && validQuotaHistoryIdentity(record.quotaHistoryIdentity)
|
|
312
|
+
? record.quotaHistoryIdentity : undefined;
|
|
313
|
+
} catch {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Recheck append admission after upstream I/O; refresh may retire a writer without erasing history. */
|
|
319
|
+
export function isPoolQuotaWriterLive(writer: PoolQuotaWriter): boolean {
|
|
320
|
+
if (!isValidCodexAccountId(writer.accountId)) return false;
|
|
321
|
+
try {
|
|
322
|
+
const record = readCodexAccountRecord(writer.accountId);
|
|
323
|
+
return !!record?.credential && record.deletedAt == null
|
|
324
|
+
&& record.generation === writer.credentialGeneration
|
|
325
|
+
&& validQuotaHistoryIdentity(writer.historyIdentity)
|
|
326
|
+
&& record.quotaHistoryIdentity === writer.historyIdentity;
|
|
327
|
+
} catch {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
260
332
|
export function isCodexAccountGenerationLive(id: string, generation: number): boolean {
|
|
261
333
|
const record = readCodexAccountRecord(id);
|
|
262
334
|
return !!record?.credential && record.deletedAt == null && record.generation === generation;
|
|
@@ -281,6 +353,8 @@ export function saveCodexAccountCredentialIfGeneration(
|
|
|
281
353
|
generation: generation + 1,
|
|
282
354
|
refreshGrantFingerprint,
|
|
283
355
|
replacedAt: current.replacedAt,
|
|
356
|
+
quotaHistoryIdentity: current.credential.chatgptAccountId === cred.chatgptAccountId
|
|
357
|
+
? current.quotaHistoryIdentity : crypto.randomUUID(),
|
|
284
358
|
...preservedValidationMetadata(current),
|
|
285
359
|
};
|
|
286
360
|
persistCredentialMutation(store);
|
|
@@ -340,6 +414,8 @@ export function commitRefreshedCodexCredentialWithAliases(
|
|
|
340
414
|
generation: generation + 1,
|
|
341
415
|
refreshGrantFingerprint,
|
|
342
416
|
replacedAt: current.replacedAt,
|
|
417
|
+
quotaHistoryIdentity: current.credential.chatgptAccountId === cred.chatgptAccountId
|
|
418
|
+
? current.quotaHistoryIdentity : crypto.randomUUID(),
|
|
343
419
|
...preservedValidationMetadata(current),
|
|
344
420
|
};
|
|
345
421
|
|
|
@@ -354,6 +430,7 @@ export function commitRefreshedCodexCredentialWithAliases(
|
|
|
354
430
|
priorFingerprint !== undefined
|
|
355
431
|
&& priorCredential.refreshToken !== cred.refreshToken
|
|
356
432
|
&& !!priorCredential.chatgptAccountId
|
|
433
|
+
&& priorCredential.chatgptAccountId === cred.chatgptAccountId
|
|
357
434
|
) {
|
|
358
435
|
for (const [aliasId, alias] of Object.entries(store)) {
|
|
359
436
|
if (aliasId === id || alias.deletedAt != null || !alias.credential) continue;
|
|
@@ -369,6 +446,7 @@ export function commitRefreshedCodexCredentialWithAliases(
|
|
|
369
446
|
generation: aliasGeneration,
|
|
370
447
|
refreshGrantFingerprint,
|
|
371
448
|
replacedAt: alias.replacedAt,
|
|
449
|
+
quotaHistoryIdentity: alias.quotaHistoryIdentity,
|
|
372
450
|
...preservedValidationMetadata(alias),
|
|
373
451
|
};
|
|
374
452
|
propagatedAliases.push({ id: aliasId, generation: aliasGeneration });
|