@bitkyc08/opencodex 2.39.0 → 2.40.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 +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { DEFAULT_CATALOG_PATH } from "../codex/paths";
|
|
3
|
+
import {
|
|
4
|
+
disconnectClient,
|
|
5
|
+
revokeConnectedClientKey,
|
|
6
|
+
rotateConnectedClientKey,
|
|
7
|
+
connectClient,
|
|
8
|
+
} from "../client/connect";
|
|
9
|
+
import { inspectClientRotationRecoveryGate, readClientConnectionState } from "../client/state";
|
|
10
|
+
import { readServiceApiTokenState } from "../lib/service-secrets";
|
|
11
|
+
import type { OcxConnectedClientId } from "../types";
|
|
12
|
+
import {
|
|
13
|
+
CliUsageError,
|
|
14
|
+
csv,
|
|
15
|
+
printData,
|
|
16
|
+
readSecretLine,
|
|
17
|
+
rejectArgs,
|
|
18
|
+
runCliAction,
|
|
19
|
+
takeFlag,
|
|
20
|
+
takeOption,
|
|
21
|
+
type RuntimeApiDeps,
|
|
22
|
+
} from "./runtime-api";
|
|
23
|
+
|
|
24
|
+
export const CONNECT_USAGE = `Usage:
|
|
25
|
+
ocx connect <url> [--management-url <url>]
|
|
26
|
+
(--pairing-code-stdin | --admin-token-stdin)
|
|
27
|
+
[--clients codex,claude] [--management-transport direct|relay]
|
|
28
|
+
[--no-sync]
|
|
29
|
+
ocx connect status [--json]
|
|
30
|
+
ocx connect rotate (--pairing-code-stdin | --admin-token-stdin)
|
|
31
|
+
[--json]
|
|
32
|
+
ocx connect revoke --admin-token-stdin [--json]`;
|
|
33
|
+
|
|
34
|
+
export const DISCONNECT_USAGE = `Usage:
|
|
35
|
+
ocx disconnect [--keep-catalog] [--json]`;
|
|
36
|
+
|
|
37
|
+
export type ClientConnectionStatus = {
|
|
38
|
+
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
39
|
+
reason?: string;
|
|
40
|
+
serverUrl?: string;
|
|
41
|
+
managementUrl?: string;
|
|
42
|
+
managementTransport?: "direct" | "relay";
|
|
43
|
+
protocolVersion?: number;
|
|
44
|
+
apiKeyId?: string;
|
|
45
|
+
selectedClients?: OcxConnectedClientId[];
|
|
46
|
+
connectedAt?: string;
|
|
47
|
+
catalogSyncedAt?: string;
|
|
48
|
+
catalogAgeSeconds?: number;
|
|
49
|
+
catalog: "present" | "missing" | "unsafe";
|
|
50
|
+
token: "owned" | "missing" | "changed" | "unsafe";
|
|
51
|
+
rotation: "clean" | "orphan-cleaned" | "recovery-required" | "unsafe";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function collectClientConnectionStatus(now = Date.now()): ClientConnectionStatus {
|
|
55
|
+
const state = readClientConnectionState();
|
|
56
|
+
const tokenState = readServiceApiTokenState();
|
|
57
|
+
const rotation = inspectClientRotationRecoveryGate(state).kind;
|
|
58
|
+
let catalog: ClientConnectionStatus["catalog"] = "missing";
|
|
59
|
+
if (existsSync(DEFAULT_CATALOG_PATH)) {
|
|
60
|
+
try {
|
|
61
|
+
const stat = lstatSync(DEFAULT_CATALOG_PATH);
|
|
62
|
+
catalog = !stat.isSymbolicLink() && stat.isFile() ? "present" : "unsafe";
|
|
63
|
+
} catch {
|
|
64
|
+
catalog = "unsafe";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (state.kind !== "connected") {
|
|
68
|
+
return {
|
|
69
|
+
state: state.kind,
|
|
70
|
+
...(state.kind === "invalid" || state.kind === "mismatched" ? { reason: state.reason } : {}),
|
|
71
|
+
catalog,
|
|
72
|
+
token: tokenState.kind === "absent" ? "missing" : tokenState.kind === "unsafe" ? "unsafe" : "changed",
|
|
73
|
+
rotation,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const catalogAgeSeconds = state.value.catalogSyncedAt
|
|
77
|
+
? Math.max(0, Math.floor((now - Date.parse(state.value.catalogSyncedAt)) / 1000))
|
|
78
|
+
: undefined;
|
|
79
|
+
const token = tokenState.kind === "absent"
|
|
80
|
+
? "missing"
|
|
81
|
+
: tokenState.kind === "unsafe"
|
|
82
|
+
? "unsafe"
|
|
83
|
+
: tokenState.fingerprint === state.value.tokenFingerprint ? "owned" : "changed";
|
|
84
|
+
return {
|
|
85
|
+
state: "connected",
|
|
86
|
+
serverUrl: state.value.serverUrl,
|
|
87
|
+
managementUrl: state.value.managementUrl,
|
|
88
|
+
managementTransport: state.value.managementTransport,
|
|
89
|
+
protocolVersion: state.value.protocolVersion,
|
|
90
|
+
apiKeyId: state.value.apiKeyId,
|
|
91
|
+
selectedClients: [...state.value.selectedClients],
|
|
92
|
+
connectedAt: state.value.connectedAt,
|
|
93
|
+
...(state.value.catalogSyncedAt ? { catalogSyncedAt: state.value.catalogSyncedAt } : {}),
|
|
94
|
+
...(catalogAgeSeconds !== undefined ? { catalogAgeSeconds } : {}),
|
|
95
|
+
catalog,
|
|
96
|
+
token,
|
|
97
|
+
rotation,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseClients(raw: string | undefined): OcxConnectedClientId[] {
|
|
102
|
+
const values = csv(raw) ?? ["codex", "claude"];
|
|
103
|
+
if (values.length < 1 || values.some(value => value !== "codex" && value !== "claude")) {
|
|
104
|
+
throw new CliUsageError("--clients must contain codex and/or claude", CONNECT_USAGE);
|
|
105
|
+
}
|
|
106
|
+
return values as OcxConnectedClientId[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function statusLines(status: ClientConnectionStatus): string[] {
|
|
110
|
+
if (status.state !== "connected") {
|
|
111
|
+
return [`Connection: ${status.state}${status.reason ? ` (${status.reason})` : ""}`];
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
"Connection: connected",
|
|
115
|
+
`Hub: ${status.serverUrl}`,
|
|
116
|
+
`Management: ${status.managementUrl} (${status.managementTransport})`,
|
|
117
|
+
`Protocol: ${status.protocolVersion}`,
|
|
118
|
+
`API key id: ${status.apiKeyId}`,
|
|
119
|
+
`Clients: ${status.selectedClients?.join(", ")}`,
|
|
120
|
+
`Token file: ${status.token}`,
|
|
121
|
+
`Key rotation: ${status.rotation}`,
|
|
122
|
+
`Catalog: ${status.catalog}${status.catalogAgeSeconds !== undefined ? ` (${status.catalogAgeSeconds}s old)` : ""}`,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runRotate(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
127
|
+
const args = [...argv];
|
|
128
|
+
const wantsJson = takeFlag(args, "--json");
|
|
129
|
+
const pairing = takeFlag(args, "--pairing-code-stdin");
|
|
130
|
+
const admin = takeFlag(args, "--admin-token-stdin");
|
|
131
|
+
if (Number(pairing) + Number(admin) !== 1) {
|
|
132
|
+
throw new CliUsageError("choose exactly one of --pairing-code-stdin or --admin-token-stdin", CONNECT_USAGE);
|
|
133
|
+
}
|
|
134
|
+
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
135
|
+
const value = new TextEncoder().encode(await readSecretLine(deps, pairing ? "pairing code" : "admin token"));
|
|
136
|
+
const connection = await rotateConnectedClientKey({
|
|
137
|
+
credential: { kind: pairing ? "pairing-grant" : "admin", value },
|
|
138
|
+
}, { fetchImpl: deps.fetchImpl });
|
|
139
|
+
printData({ apiKeyId: connection.apiKeyId, rotation: "committed" }, wantsJson, [
|
|
140
|
+
`Rotated connected API key ${connection.apiKeyId}; the previous key is no longer admitted.`,
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function runConnect(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
145
|
+
const args = [...argv];
|
|
146
|
+
const serverUrl = args.shift();
|
|
147
|
+
if (!serverUrl || serverUrl.startsWith("--")) throw new CliUsageError("hub URL is required", CONNECT_USAGE);
|
|
148
|
+
const managementUrl = takeOption(args, "--management-url");
|
|
149
|
+
const clients = parseClients(takeOption(args, "--clients"));
|
|
150
|
+
const managementTransport = takeOption(args, "--management-transport") ?? "direct";
|
|
151
|
+
if (managementTransport !== "direct" && managementTransport !== "relay") {
|
|
152
|
+
throw new CliUsageError("--management-transport must be direct or relay", CONNECT_USAGE);
|
|
153
|
+
}
|
|
154
|
+
const pairing = takeFlag(args, "--pairing-code-stdin");
|
|
155
|
+
const admin = takeFlag(args, "--admin-token-stdin");
|
|
156
|
+
const noSync = takeFlag(args, "--no-sync");
|
|
157
|
+
if (Number(pairing) + Number(admin) !== 1) {
|
|
158
|
+
throw new CliUsageError("choose exactly one of --pairing-code-stdin or --admin-token-stdin", CONNECT_USAGE);
|
|
159
|
+
}
|
|
160
|
+
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
161
|
+
const secret = await readSecretLine(deps, pairing ? "pairing code" : "admin token");
|
|
162
|
+
const value = new TextEncoder().encode(secret);
|
|
163
|
+
const connection = await connectClient({
|
|
164
|
+
serverUrl,
|
|
165
|
+
...(managementUrl ? { managementUrl } : {}),
|
|
166
|
+
credential: { kind: pairing ? "pairing-grant" : "admin", value },
|
|
167
|
+
selectedClients: clients,
|
|
168
|
+
managementTransport,
|
|
169
|
+
noSync,
|
|
170
|
+
}, { fetchImpl: deps.fetchImpl });
|
|
171
|
+
console.log(`Connected to ${connection.serverUrl} as key ${connection.apiKeyId}.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function runRevoke(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
175
|
+
const args = [...argv];
|
|
176
|
+
const wantsJson = takeFlag(args, "--json");
|
|
177
|
+
const admin = takeFlag(args, "--admin-token-stdin");
|
|
178
|
+
if (!admin) throw new CliUsageError("revoke requires --admin-token-stdin", CONNECT_USAGE);
|
|
179
|
+
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
180
|
+
const value = new TextEncoder().encode(await readSecretLine(deps, "admin token"));
|
|
181
|
+
const result = await revokeConnectedClientKey({ kind: "admin", value }, { fetchImpl: deps.fetchImpl });
|
|
182
|
+
printData(result, wantsJson, [`Revoked connected API key ${result.apiKeyId}. Disconnect this client next.`]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function handleConnectCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
186
|
+
return runCliAction(async () => {
|
|
187
|
+
if (argv[0] === "status") {
|
|
188
|
+
const args = argv.slice(1);
|
|
189
|
+
const wantsJson = takeFlag(args, "--json");
|
|
190
|
+
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
191
|
+
const status = collectClientConnectionStatus();
|
|
192
|
+
printData(status, wantsJson, statusLines(status));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (argv[0] === "revoke") {
|
|
196
|
+
await runRevoke(argv.slice(1), deps);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (argv[0] === "rotate") {
|
|
200
|
+
await runRotate(argv.slice(1), deps);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
await runConnect(argv, deps);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function handleDisconnectCommand(argv: string[]): Promise<number> {
|
|
208
|
+
return runCliAction(async () => {
|
|
209
|
+
const args = [...argv];
|
|
210
|
+
const keepCatalog = takeFlag(args, "--keep-catalog");
|
|
211
|
+
const wantsJson = takeFlag(args, "--json");
|
|
212
|
+
rejectArgs(args, DISCONNECT_USAGE, { redactValues: true });
|
|
213
|
+
const result = await disconnectClient({ keepCatalog });
|
|
214
|
+
const payload = {
|
|
215
|
+
...result,
|
|
216
|
+
revoke: {
|
|
217
|
+
apiKeyId: result.apiKeyId,
|
|
218
|
+
location: "Integrations → API Keys",
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
printData(payload, wantsJson, [
|
|
222
|
+
"Disconnected locally; native Codex state was restored.",
|
|
223
|
+
`The hub key ${result.apiKeyId} is still valid. Revoke it from Integrations → API Keys.`,
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { stripGrokConfig } from "../grok/inject";
|
|
|
23
23
|
import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
|
|
24
24
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
25
25
|
import { isJsonOption, takeFlag } from "./runtime-api";
|
|
26
|
+
import type { ClientConnectionState } from "../client/state";
|
|
26
27
|
|
|
27
28
|
export interface CliDispatchDeps {
|
|
28
29
|
args: string[];
|
|
@@ -59,6 +60,13 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
59
60
|
return Number(process.exitCode ?? 0);
|
|
60
61
|
},
|
|
61
62
|
start: async deps => {
|
|
63
|
+
const { readClientConnectionState } = await import("../client/state");
|
|
64
|
+
const clientState = readClientConnectionState();
|
|
65
|
+
await reconcileClientJournalBeforeLifecycle(clientState);
|
|
66
|
+
if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
|
|
67
|
+
console.error(`Client state is ${clientState.kind}: ${clientState.reason}`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
62
70
|
await deps.handleStart();
|
|
63
71
|
return Number(process.exitCode ?? 0);
|
|
64
72
|
},
|
|
@@ -245,6 +253,15 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
245
253
|
return 0;
|
|
246
254
|
},
|
|
247
255
|
ensure: async deps => {
|
|
256
|
+
const { readClientConnectionState } = await import("../client/state");
|
|
257
|
+
const clientState = readClientConnectionState();
|
|
258
|
+
await reconcileClientJournalBeforeLifecycle(clientState);
|
|
259
|
+
if (clientState.kind !== "disconnected") {
|
|
260
|
+
console.error(clientState.kind === "connected"
|
|
261
|
+
? "Client mode does not start a local provider proxy; use 'ocx sync'."
|
|
262
|
+
: `Client state is ${clientState.kind}: ${clientState.reason}`);
|
|
263
|
+
return 1;
|
|
264
|
+
}
|
|
248
265
|
await deps.handleEnsure();
|
|
249
266
|
return Number(process.exitCode ?? 0);
|
|
250
267
|
},
|
|
@@ -317,6 +334,31 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
317
334
|
// Separate flag on purpose: --restart-codex promises app-server-only scope,
|
|
318
335
|
// and quitting the desktop app ends live conversations.
|
|
319
336
|
const restartDesktopApp = syncArgs.includes("--restart-desktop-app");
|
|
337
|
+
const { readClientConnectionState } = await import("../client/state");
|
|
338
|
+
const clientState = readClientConnectionState();
|
|
339
|
+
if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
|
|
340
|
+
console.error(`Client state is ${clientState.kind}: ${clientState.reason}`);
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
if (clientState.kind === "connected") {
|
|
344
|
+
try {
|
|
345
|
+
const { syncConnectedClient } = await import("../client/connect");
|
|
346
|
+
const result = await syncConnectedClient({ restartCodex });
|
|
347
|
+
console.log(result.stale
|
|
348
|
+
? "Hub unavailable; retained and applied the last-known-good remote catalog (stale)."
|
|
349
|
+
: "Remote hub catalog synchronized.");
|
|
350
|
+
await handleConnectedSyncCatalogWrite(result, restartCodex, restartDesktopApp);
|
|
351
|
+
// `process.exitCode` rather than a literal 0, for the same reason every other
|
|
352
|
+
// runner does it (tests/cli-transport-honesty.test.ts): the catalog-write helper
|
|
353
|
+
// drives app-server restarts, and one of those recording a failure must not be
|
|
354
|
+
// erased by the value this runner returns. It reads 0 on the ordinary path. Node
|
|
355
|
+
// types it as `number | string`; only a numeric code means anything here.
|
|
356
|
+
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(`Connected sync failed without local fallback: ${error instanceof Error ? error.message : String(error)}`);
|
|
359
|
+
return 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
320
362
|
const live = await deps.findLiveProxy();
|
|
321
363
|
const synced = await syncModelsToCodex(
|
|
322
364
|
live?.port,
|
|
@@ -372,6 +414,14 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
372
414
|
const { cmdV2 } = await import("./v2");
|
|
373
415
|
return await cmdV2(deps.args.slice(1), {}, async () => (await deps.findLiveProxy())?.port);
|
|
374
416
|
},
|
|
417
|
+
connect: async deps => {
|
|
418
|
+
const { handleConnectCommand } = await import("./connect");
|
|
419
|
+
return await handleConnectCommand(deps.args.slice(1));
|
|
420
|
+
},
|
|
421
|
+
disconnect: async deps => {
|
|
422
|
+
const { handleDisconnectCommand } = await import("./connect");
|
|
423
|
+
return await handleDisconnectCommand(deps.args.slice(1));
|
|
424
|
+
},
|
|
375
425
|
"sync-cache": async deps => {
|
|
376
426
|
const cacheArgs = deps.args.slice(1);
|
|
377
427
|
const restartCodex = cacheArgs.includes("--restart-codex");
|
|
@@ -451,27 +501,34 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
451
501
|
return ok ? 0 : 1;
|
|
452
502
|
},
|
|
453
503
|
gui: async deps => {
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
504
|
+
const { runGuiCommand } = await import("./gui");
|
|
505
|
+
return runGuiCommand(deps.args.slice(1), {
|
|
506
|
+
loadConfig: deps.loadConfig,
|
|
507
|
+
findLiveProxy: deps.findLiveProxy,
|
|
508
|
+
openDefaultGui: async () => {
|
|
509
|
+
const config = deps.loadConfig();
|
|
510
|
+
// Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
|
|
511
|
+
// proxy and waits until the spawned one actually answers before opening the browser.
|
|
512
|
+
let live = await deps.findLiveProxy();
|
|
513
|
+
if (!live) {
|
|
514
|
+
console.log("Proxy not running. Starting...");
|
|
515
|
+
deps.spawnDetached(deps.startArgv((config.port ?? 10100) > 0 ? (config.port ?? 10100) : undefined));
|
|
516
|
+
live = await deps.waitForProxy();
|
|
517
|
+
if (!live) {
|
|
518
|
+
console.error("❌ Proxy did not become healthy after starting. Not opening the GUI.");
|
|
519
|
+
return 1;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// Open the host the proxy actually binds — `localhost` only answers for
|
|
523
|
+
// loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
|
|
524
|
+
const guiHost = deps.probeHostname(live?.hostname ?? config.hostname);
|
|
525
|
+
const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
|
|
526
|
+
console.log(`Opening ${guiUrl}`);
|
|
527
|
+
const { openUrl } = await import("../lib/open-url");
|
|
528
|
+
openUrl(guiUrl);
|
|
529
|
+
return 0;
|
|
530
|
+
},
|
|
531
|
+
});
|
|
475
532
|
},
|
|
476
533
|
service: async deps => {
|
|
477
534
|
process.exitCode = 0;
|
|
@@ -779,6 +836,34 @@ export const DISPATCH_ALIASES: ReadonlyMap<string, string> = aliasTargets;
|
|
|
779
836
|
|
|
780
837
|
/** Resolve the runner key for a command, following registry aliases to the
|
|
781
838
|
* canonical runner. Returns undefined when the command is unknown. */
|
|
839
|
+
/** What `handleStart` does about a live proxy it found before binding. */
|
|
840
|
+
export type StartOwnerDecision = "refuse" | "service-stay-out" | "sibling";
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Pure decision for `handleStart` when the pre-bind probe found a live proxy.
|
|
844
|
+
*
|
|
845
|
+
* The #3106 guard exists so a bare `start` cannot shadow a healthy configured-port
|
|
846
|
+
* proxy with an ephemeral-port copy. An interactive `--port X` naming a DIFFERENT
|
|
847
|
+
* port than the live proxy's is an explicit sibling request, not that shadow — and
|
|
848
|
+
* refusing it also broke every spawned-launcher test on a machine running a real
|
|
849
|
+
* proxy, because the probe reaches the machine-global port across sandbox homes.
|
|
850
|
+
* The service wrapper always passes the configured port and keeps its exact
|
|
851
|
+
* stay-out-of-the-way semantics: it never takes the sibling path.
|
|
852
|
+
*/
|
|
853
|
+
export function decideStartWithLiveOwner(input: {
|
|
854
|
+
livePort: number;
|
|
855
|
+
requestedPort: number | undefined;
|
|
856
|
+
ocxService: string | undefined;
|
|
857
|
+
}): StartOwnerDecision {
|
|
858
|
+
const sibling = input.requestedPort !== undefined
|
|
859
|
+
&& input.requestedPort !== input.livePort
|
|
860
|
+
// Only the exact "1" sentinel is service context — the same check syncCleanup
|
|
861
|
+
// uses — so an env value like "0" or "false" cannot reach the stay-out path.
|
|
862
|
+
&& input.ocxService !== "1";
|
|
863
|
+
if (sibling) return "sibling";
|
|
864
|
+
return input.ocxService === "1" ? "service-stay-out" : "refuse";
|
|
865
|
+
}
|
|
866
|
+
|
|
782
867
|
export function resolveDispatchCommand(command: string | undefined): string | undefined {
|
|
783
868
|
if (command === undefined) return undefined;
|
|
784
869
|
if (Object.prototype.hasOwnProperty.call(commandRunners, command)) return command;
|
|
@@ -847,3 +932,23 @@ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Pro
|
|
|
847
932
|
}
|
|
848
933
|
}
|
|
849
934
|
}
|
|
935
|
+
|
|
936
|
+
async function handleConnectedSyncCatalogWrite(
|
|
937
|
+
result: { catalogWritten: boolean; cacheSynced: boolean },
|
|
938
|
+
restartCodex: boolean,
|
|
939
|
+
restartDesktopApp: boolean,
|
|
940
|
+
): Promise<void> {
|
|
941
|
+
if (!result.catalogWritten && !result.cacheSynced) return;
|
|
942
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
943
|
+
if (restartDesktopApp) await handleDesktopAppRestart(console);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async function reconcileClientJournalBeforeLifecycle(
|
|
947
|
+
state: ClientConnectionState,
|
|
948
|
+
): Promise<void> {
|
|
949
|
+
if (state.kind === "disconnected") return;
|
|
950
|
+
const { reconcileJournal } = await import("../codex/journal");
|
|
951
|
+
reconcileJournal(state.kind === "connected"
|
|
952
|
+
? { activeClientApiKeyId: state.value.apiKeyId }
|
|
953
|
+
: undefined);
|
|
954
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
5
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
6
|
+
createLocalAttestationChallenge,
|
|
7
|
+
verifyLocalAttestationProof,
|
|
8
|
+
} from "../lib/local-management-attestation";
|
|
9
|
+
import {
|
|
10
|
+
GUI_PAIR_BROWSER_ORIGIN_HEADER,
|
|
11
|
+
GUI_PAIR_CAPABILITY_HEADER,
|
|
12
|
+
GUI_PAIR_CAPABILITY_TTL_MS,
|
|
13
|
+
GUI_PAIR_CAPABILITY_VERSION,
|
|
14
|
+
GUI_PAIR_EXPECTED_PID_HEADER,
|
|
15
|
+
GUI_PAIR_EXPIRES_AT_HEADER,
|
|
16
|
+
GUI_PAIR_METHOD,
|
|
17
|
+
GUI_PAIR_NONCE_HEADER,
|
|
18
|
+
GUI_PAIR_PATH,
|
|
19
|
+
canonicalGuiBrowserOrigin,
|
|
20
|
+
createGuiPairCapability,
|
|
21
|
+
} from "../lib/gui-pair-capability";
|
|
22
|
+
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
23
|
+
import {
|
|
24
|
+
isOpencodexHealthz,
|
|
25
|
+
probeHostname,
|
|
26
|
+
type HealthzIdentity,
|
|
27
|
+
type LiveProxy,
|
|
28
|
+
} from "../server/proxy-liveness";
|
|
29
|
+
|
|
30
|
+
export type GuiPairRequestResult =
|
|
31
|
+
| { kind: "created"; grant: string; browserOrigin: string; serverOrigin: string; expiresAt: number }
|
|
32
|
+
| { kind: "unavailable"; reason: "unattested-target" | "runtime-mismatch" | "attestation" | "capability" | "transport" | "rejected" };
|
|
33
|
+
|
|
34
|
+
export interface GuiPairClientDeps {
|
|
35
|
+
fetchImpl?: typeof fetch;
|
|
36
|
+
readRuntime?: (pid: number) => RuntimePortState | null;
|
|
37
|
+
createChallenge?: () => string;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const GUI_PAIR_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
|
+
|
|
44
|
+
function sameRuntime(left: RuntimePortState, right: RuntimePortState | null): boolean {
|
|
45
|
+
const leftSecret = Buffer.from(left.attestationSecret ?? "");
|
|
46
|
+
const rightSecret = Buffer.from(right?.attestationSecret ?? "");
|
|
47
|
+
return !!right?.attestationSecret
|
|
48
|
+
&& right.pid === left.pid
|
|
49
|
+
&& right.port === left.port
|
|
50
|
+
&& right.hostname === left.hostname
|
|
51
|
+
&& leftSecret.length === rightSecret.length
|
|
52
|
+
&& timingSafeEqual(leftSecret, rightSecret);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canonicalHttpOrigin(value: unknown): string | null {
|
|
56
|
+
if (typeof value !== "string") return null;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = new URL(value);
|
|
59
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
60
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) return null;
|
|
61
|
+
return parsed.origin;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseCreatedResult(value: unknown, browserOrigin: string): GuiPairRequestResult | null {
|
|
68
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
69
|
+
const record = value as Record<string, unknown>;
|
|
70
|
+
if (
|
|
71
|
+
typeof record.grant !== "string"
|
|
72
|
+
|| !/^ocx_pair_[A-Za-z0-9_-]{43}$/.test(record.grant)
|
|
73
|
+
|| canonicalGuiBrowserOrigin(record.browserOrigin) !== browserOrigin
|
|
74
|
+
|| typeof record.expiresAt !== "number"
|
|
75
|
+
|| !Number.isSafeInteger(record.expiresAt)
|
|
76
|
+
) return null;
|
|
77
|
+
const serverOrigin = canonicalHttpOrigin(record.serverOrigin);
|
|
78
|
+
if (!serverOrigin) return null;
|
|
79
|
+
return {
|
|
80
|
+
kind: "created",
|
|
81
|
+
grant: record.grant,
|
|
82
|
+
browserOrigin,
|
|
83
|
+
serverOrigin,
|
|
84
|
+
expiresAt: record.expiresAt,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function requestBoundGuiPairingGrant(
|
|
89
|
+
target: LiveProxy,
|
|
90
|
+
browserOrigin: string,
|
|
91
|
+
deps: GuiPairClientDeps = {},
|
|
92
|
+
): Promise<GuiPairRequestResult> {
|
|
93
|
+
if (target.source !== "runtime" || target.pid === null || target.pid <= 0) {
|
|
94
|
+
return { kind: "unavailable", reason: "unattested-target" };
|
|
95
|
+
}
|
|
96
|
+
const canonicalOrigin = canonicalGuiBrowserOrigin(browserOrigin);
|
|
97
|
+
if (!canonicalOrigin || canonicalOrigin !== browserOrigin) {
|
|
98
|
+
return { kind: "unavailable", reason: "capability" };
|
|
99
|
+
}
|
|
100
|
+
const readRuntime = deps.readRuntime ?? readRuntimePort;
|
|
101
|
+
const runtime = readRuntime(target.pid);
|
|
102
|
+
if (!runtime?.attestationSecret || runtime.pid !== target.pid || runtime.port !== target.port) {
|
|
103
|
+
return { kind: "unavailable", reason: "runtime-mismatch" };
|
|
104
|
+
}
|
|
105
|
+
const fetchImpl = deps.fetchImpl ?? directLocalHttpFetch;
|
|
106
|
+
const timeoutMs = deps.timeoutMs ?? GUI_PAIR_REQUEST_TIMEOUT_MS;
|
|
107
|
+
const challenge = (deps.createChallenge ?? createLocalAttestationChallenge)();
|
|
108
|
+
const baseUrl = `http://${probeHostname(target.hostname)}:${target.port}`;
|
|
109
|
+
let proofResponse: Response;
|
|
110
|
+
try {
|
|
111
|
+
proofResponse = await fetchImpl(`${baseUrl}/healthz`, {
|
|
112
|
+
headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
|
|
113
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
return { kind: "unavailable", reason: "transport" };
|
|
117
|
+
}
|
|
118
|
+
const body = await proofResponse.json().catch(() => null) as HealthzIdentity | null;
|
|
119
|
+
if (
|
|
120
|
+
!proofResponse.ok
|
|
121
|
+
|| !isOpencodexHealthz(body)
|
|
122
|
+
|| body?.pid !== target.pid
|
|
123
|
+
|| body?.port !== target.port
|
|
124
|
+
|| !verifyLocalAttestationProof(
|
|
125
|
+
runtime.attestationSecret,
|
|
126
|
+
challenge,
|
|
127
|
+
target.pid,
|
|
128
|
+
target.port,
|
|
129
|
+
proofResponse.headers.get(LOCAL_ATTESTATION_PROOF_HEADER),
|
|
130
|
+
)
|
|
131
|
+
) return { kind: "unavailable", reason: "attestation" };
|
|
132
|
+
if (body.guiPairCapability !== GUI_PAIR_CAPABILITY_VERSION) {
|
|
133
|
+
return { kind: "unavailable", reason: "capability" };
|
|
134
|
+
}
|
|
135
|
+
if (!sameRuntime(runtime, readRuntime(target.pid))) {
|
|
136
|
+
return { kind: "unavailable", reason: "runtime-mismatch" };
|
|
137
|
+
}
|
|
138
|
+
const expiresAt = (deps.now ?? Date.now)() + GUI_PAIR_CAPABILITY_TTL_MS;
|
|
139
|
+
const capability = createGuiPairCapability(
|
|
140
|
+
runtime.attestationSecret,
|
|
141
|
+
challenge,
|
|
142
|
+
GUI_PAIR_METHOD,
|
|
143
|
+
GUI_PAIR_PATH,
|
|
144
|
+
browserOrigin,
|
|
145
|
+
target.pid,
|
|
146
|
+
target.port,
|
|
147
|
+
expiresAt,
|
|
148
|
+
);
|
|
149
|
+
if (!capability) return { kind: "unavailable", reason: "capability" };
|
|
150
|
+
let response: Response;
|
|
151
|
+
try {
|
|
152
|
+
response = await fetchImpl(`${baseUrl}${GUI_PAIR_PATH}`, {
|
|
153
|
+
method: GUI_PAIR_METHOD,
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Length": "0",
|
|
156
|
+
[GUI_PAIR_EXPECTED_PID_HEADER]: String(target.pid),
|
|
157
|
+
[GUI_PAIR_NONCE_HEADER]: challenge,
|
|
158
|
+
[GUI_PAIR_EXPIRES_AT_HEADER]: String(expiresAt),
|
|
159
|
+
[GUI_PAIR_BROWSER_ORIGIN_HEADER]: browserOrigin,
|
|
160
|
+
[GUI_PAIR_CAPABILITY_HEADER]: capability,
|
|
161
|
+
},
|
|
162
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
return { kind: "unavailable", reason: "transport" };
|
|
166
|
+
}
|
|
167
|
+
if (!response.ok) return { kind: "unavailable", reason: "rejected" };
|
|
168
|
+
const result = parseCreatedResult(await response.json().catch(() => null), browserOrigin);
|
|
169
|
+
return result ?? { kind: "unavailable", reason: "rejected" };
|
|
170
|
+
}
|
package/src/cli/gui.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { canonicalGuiBrowserOrigin } from "../lib/gui-pair-capability";
|
|
3
|
+
import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
|
|
4
|
+
import {
|
|
5
|
+
requestBoundGuiPairingGrant,
|
|
6
|
+
type GuiPairClientDeps,
|
|
7
|
+
type GuiPairRequestResult,
|
|
8
|
+
} from "./gui-pair-client";
|
|
9
|
+
import type { RuntimeApiDeps } from "./runtime-api";
|
|
10
|
+
|
|
11
|
+
const GUI_USAGE = "ocx gui [pair --origin <browser-origin> [--json]]";
|
|
12
|
+
const PAIRING_WARNING = "Pairing grants are secret, single-use, and expire quickly. Do not save them.";
|
|
13
|
+
|
|
14
|
+
export interface GuiCommandDeps extends RuntimeApiDeps {
|
|
15
|
+
openDefaultGui: () => Promise<number>;
|
|
16
|
+
loadConfig: () => OcxConfig;
|
|
17
|
+
findLiveProxy?: () => Promise<LiveProxy | null>;
|
|
18
|
+
requestPairingGrant?: (
|
|
19
|
+
target: LiveProxy,
|
|
20
|
+
browserOrigin: string,
|
|
21
|
+
deps?: GuiPairClientDeps,
|
|
22
|
+
) => Promise<GuiPairRequestResult>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function allowedPairingOrigin(origin: string, config: OcxConfig): boolean {
|
|
26
|
+
if (config.runtimeRole !== "hub") return false;
|
|
27
|
+
if (canonicalGuiBrowserOrigin(config.hub?.managementPublicOrigin) === origin) return true;
|
|
28
|
+
return (config.corsAllowOrigins ?? []).some(value => canonicalGuiBrowserOrigin(value) === origin);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parsePairArgs(args: string[]): { origin: string; json: boolean } | null {
|
|
32
|
+
let origin: string | undefined;
|
|
33
|
+
let json = false;
|
|
34
|
+
for (let index = 0; index < args.length; index++) {
|
|
35
|
+
const arg = args[index]!;
|
|
36
|
+
if (arg === "--json" && !json) {
|
|
37
|
+
json = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === "--origin" && origin === undefined) {
|
|
41
|
+
const value = args[++index];
|
|
42
|
+
if (!value || value.startsWith("--")) return null;
|
|
43
|
+
origin = value;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return origin ? { origin, json } : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runGuiCommand(args: string[], deps: GuiCommandDeps): Promise<number> {
|
|
52
|
+
if (args.length === 0) return deps.openDefaultGui();
|
|
53
|
+
if (args[0] !== "pair") {
|
|
54
|
+
console.error(`Usage: ${GUI_USAGE}`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const parsed = parsePairArgs(args.slice(1));
|
|
58
|
+
const canonicalOrigin = parsed ? canonicalGuiBrowserOrigin(parsed.origin) : null;
|
|
59
|
+
if (!parsed || !canonicalOrigin || canonicalOrigin !== parsed.origin) {
|
|
60
|
+
console.error(`Usage: ${GUI_USAGE}`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const config = deps.loadConfig();
|
|
64
|
+
if (!allowedPairingOrigin(canonicalOrigin, config)) {
|
|
65
|
+
console.error("The pairing origin is not enabled by hub.managementPublicOrigin or corsAllowOrigins.");
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
const target = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
69
|
+
if (!target) {
|
|
70
|
+
console.error("No running attested OpenCodex proxy is available for GUI pairing.");
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
const result = await (deps.requestPairingGrant ?? requestBoundGuiPairingGrant)(target, canonicalOrigin, {
|
|
74
|
+
...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
|
|
75
|
+
});
|
|
76
|
+
if (result.kind !== "created") {
|
|
77
|
+
console.error(`GUI pairing failed (${result.reason}).`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (parsed.json) {
|
|
81
|
+
console.log(JSON.stringify({ ...result, warning: PAIRING_WARNING }));
|
|
82
|
+
} else {
|
|
83
|
+
console.log(result.grant);
|
|
84
|
+
console.error(PAIRING_WARNING);
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|