@bitkyc08/opencodex 2.65.0 → 2.66.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 +14 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
package/src/cli/connect.ts
CHANGED
|
@@ -15,14 +15,21 @@ import {
|
|
|
15
15
|
connectClient,
|
|
16
16
|
} from "../client/connect";
|
|
17
17
|
import { inspectClientRotationRecoveryGate, readClientConnectionState } from "../client/state";
|
|
18
|
+
import { readClientLinkState } from "../client/link-state";
|
|
19
|
+
import { teardownClientLink } from "../client/link-teardown";
|
|
20
|
+
import { reapOrphanTunnel } from "../client/link-tunnel";
|
|
18
21
|
import { readServiceApiTokenState } from "../lib/service-secrets";
|
|
19
22
|
import type { ClientLifecycleLockDeps } from "../client/lifecycle-lock";
|
|
23
|
+
import { linkKnownHostsPath } from "../link/paths";
|
|
24
|
+
import { createSshRunner, type SshRunner } from "../link/ssh-runner";
|
|
25
|
+
import type { OrphanTunnelResult } from "../client/link-tunnel";
|
|
20
26
|
import { inspectRemoteDesktopStore } from "../claude/desktop-remote-store";
|
|
21
27
|
import type { OcxConnectedClientId } from "../types";
|
|
22
28
|
import {
|
|
23
29
|
CliUsageError,
|
|
24
30
|
csv,
|
|
25
31
|
printData,
|
|
32
|
+
readSecretBytes,
|
|
26
33
|
readSecretLine,
|
|
27
34
|
rejectArgs,
|
|
28
35
|
runCliAction,
|
|
@@ -37,6 +44,12 @@ import {
|
|
|
37
44
|
export interface ClientCommandDeps extends RuntimeApiDeps {
|
|
38
45
|
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
39
46
|
catalogProbeDeps?: ClientCatalogProbeDeps;
|
|
47
|
+
linkTeardownDeps?: {
|
|
48
|
+
runner?: Pick<SshRunner, "run">;
|
|
49
|
+
reapOrphanTunnel?: () => Promise<OrphanTunnelResult>;
|
|
50
|
+
knownHostsFile?: string;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
};
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
export interface ClientCatalogProbeDeps extends CatalogCompatibilityDeps {
|
|
@@ -54,6 +67,8 @@ export interface ClientCatalogProbeDeps extends CatalogCompatibilityDeps {
|
|
|
54
67
|
}
|
|
55
68
|
|
|
56
69
|
export const CONNECT_USAGE = `Usage:
|
|
70
|
+
ocx connect --link --key-stdin --tunnel-port <port> --link-id <id>
|
|
71
|
+
[--clients codex,claude] [--catalog-timeout <seconds>] [--no-sync]
|
|
57
72
|
ocx connect <url> [--management-url <url>]
|
|
58
73
|
(--pairing-code-stdin | --admin-token-stdin)
|
|
59
74
|
[--clients codex,claude] [--management-transport direct|relay]
|
|
@@ -72,6 +87,8 @@ export type ClientConnectionStatus = {
|
|
|
72
87
|
serverUrl?: string;
|
|
73
88
|
managementUrl?: string;
|
|
74
89
|
managementTransport?: "direct" | "relay";
|
|
90
|
+
transport?: "hub" | "link";
|
|
91
|
+
link?: { tunnelPort: number; linkId: string };
|
|
75
92
|
protocolVersion?: number;
|
|
76
93
|
apiKeyId?: string;
|
|
77
94
|
selectedClients?: OcxConnectedClientId[];
|
|
@@ -199,6 +216,8 @@ export function collectClientConnectionStatus(
|
|
|
199
216
|
serverUrl: state.value.serverUrl,
|
|
200
217
|
managementUrl: state.value.managementUrl,
|
|
201
218
|
managementTransport: state.value.managementTransport,
|
|
219
|
+
...(state.value.transport ? { transport: state.value.transport } : {}),
|
|
220
|
+
...(state.value.link ? { link: { ...state.value.link } } : {}),
|
|
202
221
|
protocolVersion: state.value.protocolVersion,
|
|
203
222
|
apiKeyId: state.value.apiKeyId,
|
|
204
223
|
selectedClients: [...state.value.selectedClients],
|
|
@@ -221,6 +240,27 @@ function parseClients(raw: string | undefined): OcxConnectedClientId[] {
|
|
|
221
240
|
return values as OcxConnectedClientId[];
|
|
222
241
|
}
|
|
223
242
|
|
|
243
|
+
async function readLinkCredential(deps: ClientCommandDeps): Promise<{ apiKeyId: string; key: string }> {
|
|
244
|
+
const raw = await readSecretBytes(deps, "link credential");
|
|
245
|
+
try {
|
|
246
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(raw).trim();
|
|
247
|
+
const parsed: unknown = JSON.parse(text);
|
|
248
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("shape");
|
|
249
|
+
const record = parsed as Record<string, unknown>;
|
|
250
|
+
if (Object.keys(record).some(key => key !== "apiKeyId" && key !== "key")
|
|
251
|
+
|| typeof record.apiKeyId !== "string" || !record.apiKeyId.trim()
|
|
252
|
+
|| record.apiKeyId.length > 256 || typeof record.key !== "string"
|
|
253
|
+
|| !/^ocx_data_[0-9a-f]{40}$/.test(record.key)) {
|
|
254
|
+
throw new Error("shape");
|
|
255
|
+
}
|
|
256
|
+
return { apiKeyId: record.apiKeyId, key: record.key };
|
|
257
|
+
} catch {
|
|
258
|
+
throw new CliUsageError("invalid link credential", CONNECT_USAGE);
|
|
259
|
+
} finally {
|
|
260
|
+
raw.fill(0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
224
264
|
/** Reads as a verdict, not a field dump: "ready" is the only word that means the client works. */
|
|
225
265
|
function readinessLine(status: ClientConnectionStatus): string {
|
|
226
266
|
const label = status.readiness === "ready"
|
|
@@ -290,6 +330,8 @@ function statusLines(status: ClientConnectionStatus): string[] {
|
|
|
290
330
|
// Second line on purpose. The whole of #4207 is that a reader stopped at "connected" and
|
|
291
331
|
// believed the client was usable, so the local verdict has to arrive before the hub detail.
|
|
292
332
|
readinessLine(status),
|
|
333
|
+
`Transport: ${status.transport ?? "hub"}`,
|
|
334
|
+
...(status.link ? [`Link: ${status.link.linkId} via tunnel port ${status.link.tunnelPort}`] : []),
|
|
293
335
|
`Hub: ${status.serverUrl}`,
|
|
294
336
|
`Management: ${status.managementUrl} (${status.managementTransport})`,
|
|
295
337
|
`Protocol: ${status.protocolVersion}`,
|
|
@@ -323,7 +365,7 @@ async function runRotate(argv: string[], deps: ClientCommandDeps): Promise<void>
|
|
|
323
365
|
]);
|
|
324
366
|
}
|
|
325
367
|
|
|
326
|
-
async function
|
|
368
|
+
async function runHubConnect(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
327
369
|
const args = [...argv];
|
|
328
370
|
const serverUrl = args.shift();
|
|
329
371
|
if (!serverUrl || serverUrl.startsWith("--")) throw new CliUsageError("hub URL is required", CONNECT_USAGE);
|
|
@@ -375,6 +417,64 @@ async function runConnect(argv: string[], deps: ClientCommandDeps): Promise<void
|
|
|
375
417
|
if (report.failure) throw new Error(report.failure);
|
|
376
418
|
}
|
|
377
419
|
|
|
420
|
+
async function runLinkConnect(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
421
|
+
const args = [...argv];
|
|
422
|
+
const keyStdin = takeFlag(args, "--key-stdin");
|
|
423
|
+
const rawPort = takeIntegerOption(args, "--tunnel-port", { min: 1024 });
|
|
424
|
+
const linkId = takeOption(args, "--link-id");
|
|
425
|
+
const managementUrl = takeOption(args, "--management-url");
|
|
426
|
+
const managementTransport = takeOption(args, "--management-transport");
|
|
427
|
+
const pairing = takeFlag(args, "--pairing-code-stdin");
|
|
428
|
+
const admin = takeFlag(args, "--admin-token-stdin");
|
|
429
|
+
const clients = parseClients(takeOption(args, "--clients"));
|
|
430
|
+
const catalogTimeoutSeconds = takeIntegerOption(args, "--catalog-timeout", { min: 1 });
|
|
431
|
+
if (catalogTimeoutSeconds !== undefined && catalogTimeoutSeconds > 120) {
|
|
432
|
+
throw new CliUsageError("--catalog-timeout must be an integer between 1 and 120", CONNECT_USAGE);
|
|
433
|
+
}
|
|
434
|
+
const noSync = takeFlag(args, "--no-sync");
|
|
435
|
+
if (!keyStdin || rawPort === undefined || !linkId) {
|
|
436
|
+
throw new CliUsageError("link connect requires --key-stdin, --tunnel-port, and --link-id", CONNECT_USAGE);
|
|
437
|
+
}
|
|
438
|
+
if (managementUrl || managementTransport !== undefined || pairing || admin || args.some(value => !value.startsWith("--"))) {
|
|
439
|
+
throw new CliUsageError("link connect accepts no hub URL, management URL, management transport, pairing code, or admin token", CONNECT_USAGE);
|
|
440
|
+
}
|
|
441
|
+
if (rawPort > 65535 || !/^lnk_[0-9a-f]{16}$/.test(linkId)) {
|
|
442
|
+
throw new CliUsageError("invalid link transport", CONNECT_USAGE);
|
|
443
|
+
}
|
|
444
|
+
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
445
|
+
const credential = await readLinkCredential(deps);
|
|
446
|
+
const connection = await connectClient({
|
|
447
|
+
serverUrl: `http://127.0.0.1:${rawPort}`,
|
|
448
|
+
managementUrl: `http://127.0.0.1:${rawPort}`,
|
|
449
|
+
credential: { kind: "link", ...credential },
|
|
450
|
+
selectedClients: clients,
|
|
451
|
+
managementTransport: "direct",
|
|
452
|
+
transport: "link",
|
|
453
|
+
link: { tunnelPort: rawPort, linkId },
|
|
454
|
+
noSync,
|
|
455
|
+
...(catalogTimeoutSeconds === undefined ? {} : { catalogTimeoutMs: catalogTimeoutSeconds * 1_000 }),
|
|
456
|
+
}, {
|
|
457
|
+
fetchImpl: deps.fetchImpl,
|
|
458
|
+
lifecycleLockDeps: deps.lifecycleLockDeps,
|
|
459
|
+
catalogCompatibility: catalogObserver(deps.catalogProbeDeps),
|
|
460
|
+
}).catch((error: unknown) => {
|
|
461
|
+
throw terminalSafeError(error);
|
|
462
|
+
});
|
|
463
|
+
const readiness = inspectInstalledCatalogReadiness(installedCatalogFileState(), deps.catalogProbeDeps ?? {});
|
|
464
|
+
const report = connectCompletionReport(connection, clients, readiness);
|
|
465
|
+
for (const line of report.lines) console.log(line);
|
|
466
|
+
if (report.failure) throw new Error(report.failure);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function runConnect(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
470
|
+
const args = [...argv];
|
|
471
|
+
if (takeFlag(args, "--link")) {
|
|
472
|
+
await runLinkConnect(args, deps);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
await runHubConnect(argv, deps);
|
|
476
|
+
}
|
|
477
|
+
|
|
378
478
|
async function runRevoke(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
379
479
|
const args = [...argv];
|
|
380
480
|
const wantsJson = takeFlag(args, "--json");
|
|
@@ -408,15 +508,33 @@ export async function handleConnectCommand(argv: string[], deps: ClientCommandDe
|
|
|
408
508
|
});
|
|
409
509
|
}
|
|
410
510
|
|
|
411
|
-
export async function handleDisconnectCommand(
|
|
511
|
+
export async function handleDisconnectCommand(
|
|
512
|
+
argv: string[],
|
|
513
|
+
deps: Pick<ClientCommandDeps, "lifecycleLockDeps" | "linkTeardownDeps"> = {},
|
|
514
|
+
): Promise<number> {
|
|
412
515
|
return runCliAction(async () => {
|
|
413
516
|
const args = [...argv];
|
|
414
517
|
const keepCatalog = takeFlag(args, "--keep-catalog");
|
|
415
518
|
const wantsJson = takeFlag(args, "--json");
|
|
416
519
|
rejectArgs(args, DISCONNECT_USAGE, { redactValues: true });
|
|
520
|
+
const teardown = await teardownClientLink({
|
|
521
|
+
readSidecar: readClientLinkState,
|
|
522
|
+
connectedLinkId: () => {
|
|
523
|
+
const state = readClientConnectionState();
|
|
524
|
+
return state.kind === "connected" && state.value.transport === "link"
|
|
525
|
+
? state.value.link?.linkId ?? null
|
|
526
|
+
: null;
|
|
527
|
+
},
|
|
528
|
+
reapOrphanTunnel: deps.linkTeardownDeps?.reapOrphanTunnel ?? (() => reapOrphanTunnel()),
|
|
529
|
+
runner: deps.linkTeardownDeps?.runner ?? createSshRunner(),
|
|
530
|
+
knownHostsFile: deps.linkTeardownDeps?.knownHostsFile ?? linkKnownHostsPath(),
|
|
531
|
+
timeoutMs: deps.linkTeardownDeps?.timeoutMs,
|
|
532
|
+
});
|
|
417
533
|
const result = await disconnectClient({ keepCatalog }, deps);
|
|
418
534
|
const payload = {
|
|
419
535
|
...result,
|
|
536
|
+
homeRevoke: teardown.homeRevoke,
|
|
537
|
+
tunnel: teardown.tunnel,
|
|
420
538
|
revoke: {
|
|
421
539
|
apiKeyId: result.apiKeyId,
|
|
422
540
|
location: "Integrations → API Keys",
|
|
@@ -427,7 +545,13 @@ export async function handleDisconnectCommand(argv: string[], deps: Pick<ClientC
|
|
|
427
545
|
...(result.desktopRestoration === "standard_fallback"
|
|
428
546
|
? ["Previous Desktop settings were not recorded; the managed profile was switched to standard mode."] : []),
|
|
429
547
|
...(result.restartRequired ? ["Fully quit and reopen Claude Desktop; local cleanup cannot revoke an in-memory credential."] : []),
|
|
430
|
-
|
|
548
|
+
...(teardown.homeRevoke === "failed" && teardown.linkId
|
|
549
|
+
? [`Home revoke failed; run ocx link revoke --link-id ${teardown.linkId} on the home.`] : []),
|
|
550
|
+
...(teardown.tunnel?.tunnel === "unresolved"
|
|
551
|
+
? [`A link tunnel (pid ${teardown.tunnel.pid}) may still be running; stop it if it is.`] : []),
|
|
552
|
+
teardown.homeRevoke === "revoked"
|
|
553
|
+
? `The home revoked link ${teardown.linkId} and its key ${result.apiKeyId}.`
|
|
554
|
+
: `The hub key ${result.apiKeyId} is still valid. Revoke it from Integrations → API Keys.`,
|
|
431
555
|
]);
|
|
432
556
|
});
|
|
433
557
|
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -539,6 +539,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
539
539
|
const { handleConnectCommand } = await import("./connect");
|
|
540
540
|
return await handleConnectCommand(deps.args.slice(1));
|
|
541
541
|
},
|
|
542
|
+
link: async deps => {
|
|
543
|
+
const { runLinkCommand } = await import("./link");
|
|
544
|
+
return await runLinkCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
|
|
545
|
+
},
|
|
542
546
|
"remote-workspace": async deps => {
|
|
543
547
|
const { runRemoteWorkspaceCommand } = await import("./remote-workspace");
|
|
544
548
|
return await runRemoteWorkspaceCommand(deps.args.slice(1));
|
|
@@ -892,6 +896,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
892
896
|
const { handleAccessCommand } = await import("./access");
|
|
893
897
|
return await handleAccessCommand(["key", ...deps.args.slice(1)]);
|
|
894
898
|
},
|
|
899
|
+
api: async deps => {
|
|
900
|
+
const { handleApiCommand } = await import("./api-protocols");
|
|
901
|
+
return await handleApiCommand(deps.args.slice(1));
|
|
902
|
+
},
|
|
895
903
|
export: async deps => {
|
|
896
904
|
const { handleExportCommand } = await import("./export-command");
|
|
897
905
|
return await handleExportCommand(deps.args.slice(1));
|
package/src/cli/doctor.ts
CHANGED
|
@@ -42,7 +42,9 @@ import {
|
|
|
42
42
|
} from "../codex/coordinator-doctor";
|
|
43
43
|
import {
|
|
44
44
|
inspectAbandonedResponseStateTemps,
|
|
45
|
+
inspectResponseSpillStorage,
|
|
45
46
|
reclaimAbandonedResponseStateTemps,
|
|
47
|
+
type ResponseSpillDirInspection,
|
|
46
48
|
type ResponseStateTempRecoveryResult,
|
|
47
49
|
} from "../responses/state";
|
|
48
50
|
import {
|
|
@@ -861,6 +863,34 @@ export function formatResponseTempLines(
|
|
|
861
863
|
return lines;
|
|
862
864
|
}
|
|
863
865
|
|
|
866
|
+
/**
|
|
867
|
+
* Render the response-state spill section (testable without console capture).
|
|
868
|
+
*
|
|
869
|
+
* Always dry-run: doctor reports what the disk looks like, it never unlinks
|
|
870
|
+
* spill files. "Owned" counts union the live store and the persisted snapshot,
|
|
871
|
+
* so a file a restart would re-own is never reported as garbage.
|
|
872
|
+
*/
|
|
873
|
+
export function formatResponseSpillLines(result: ResponseSpillDirInspection): string[] {
|
|
874
|
+
// A truncated scan saw only a prefix of the directory, so zero orphans there is
|
|
875
|
+
// not a clean bill of health: the unscanned tail may still hold them.
|
|
876
|
+
if (result.orphanFiles === 0 && result.truncated) {
|
|
877
|
+
return [
|
|
878
|
+
` !! No orphaned response-state spill files in the first ${result.scanned} entries (${result.files} file(s), ${mb(result.bytes)} scanned).`,
|
|
879
|
+
" Scan stopped at its entry budget; the rest of the directory was not checked.",
|
|
880
|
+
];
|
|
881
|
+
}
|
|
882
|
+
if (result.orphanFiles === 0) {
|
|
883
|
+
return [` ok No orphaned response-state spill files (${result.files} file(s), ${mb(result.bytes)} on disk).`];
|
|
884
|
+
}
|
|
885
|
+
const lines = [
|
|
886
|
+
` !! ${result.orphanFiles} unreferenced response-state spill file(s), ${mb(result.orphanBytes)} reclaimable.`,
|
|
887
|
+
` ${result.ownedFiles} file(s), ${mb(result.ownedBytes)} still owned by the store or the persisted snapshot.`,
|
|
888
|
+
" The running proxy reclaims orphans on its periodic sweep; do not delete spill files manually.",
|
|
889
|
+
];
|
|
890
|
+
if (result.truncated) lines.push(" Scan stopped at its entry budget; the real total is higher.");
|
|
891
|
+
return lines;
|
|
892
|
+
}
|
|
893
|
+
|
|
864
894
|
export function formatCoordinatorDoctorLines(diagnostic: CodexCoordinatorDiagnostic): string[] {
|
|
865
895
|
const pathLine = diagnostic.path ? [` path: ${diagnostic.path}`] : [];
|
|
866
896
|
const evidenceLines = "evidence" in diagnostic && diagnostic.evidence
|
|
@@ -1299,6 +1329,9 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1299
1329
|
reclaimTemps,
|
|
1300
1330
|
)) console.log(line);
|
|
1301
1331
|
|
|
1332
|
+
console.log("\nResponse-state spill files");
|
|
1333
|
+
for (const line of formatResponseSpillLines(inspectResponseSpillStorage())) console.log(line);
|
|
1334
|
+
|
|
1302
1335
|
const orcaHome = collectOrcaCodexHomeDiagnostic();
|
|
1303
1336
|
console.log("\nCodex app home targeting");
|
|
1304
1337
|
console.log(` ${orcaHome.mismatch ? "!! " : "ok "} Effective Codex home: ${orcaHome.effectiveCodexHome}`);
|
package/src/cli/help.ts
CHANGED
|
@@ -57,6 +57,7 @@ Usage:
|
|
|
57
57
|
Open the dashboard or create a single-use remote pairing grant
|
|
58
58
|
ocx hub invite [--json] Print a ready-to-run \`ocx connect\` line for one more machine
|
|
59
59
|
(hub only; see \`ocx help hub\` for the one-port topology)
|
|
60
|
+
ocx link <sub> Machine links over SSH (port|issue|revoke|status)
|
|
60
61
|
ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
|
|
61
62
|
ocx restart Stop and restart the proxy
|
|
62
63
|
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)
|
|
@@ -81,6 +82,7 @@ Usage:
|
|
|
81
82
|
ocx memory [--json] Alias of ocx observe memory
|
|
82
83
|
ocx api-key <sub> Alias of ocx access key
|
|
83
84
|
ocx access <sub> External API keys and endpoint information
|
|
85
|
+
ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
|
|
84
86
|
ocx export --client <id> Print a client config wired to the running proxy (15 clients)
|
|
85
87
|
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
86
88
|
ocx grok <sub> Grok Build model selection and apply
|
package/src/cli/link.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { assertSshAlias } from "../link/ssh-argv";
|
|
2
|
+
import { linkStorePath } from "../link/paths";
|
|
3
|
+
import { isLinkPort } from "../link/ports";
|
|
4
|
+
import { readLinkStore, type LinkDirection, type LinkStore } from "../link/store";
|
|
5
|
+
import { findAvailablePort } from "../server/ports";
|
|
6
|
+
import {
|
|
7
|
+
CliUsageError,
|
|
8
|
+
RuntimeApiError,
|
|
9
|
+
rejectArgs,
|
|
10
|
+
runCliAction,
|
|
11
|
+
runtimeBaseUrl,
|
|
12
|
+
runtimeRequest,
|
|
13
|
+
takeIntegerOption,
|
|
14
|
+
takeJsonFlag,
|
|
15
|
+
takeOption,
|
|
16
|
+
type RuntimeApiDeps,
|
|
17
|
+
} from "./runtime-api";
|
|
18
|
+
|
|
19
|
+
export const LINK_USAGE = `Usage:
|
|
20
|
+
ocx link port [--json]
|
|
21
|
+
ocx link issue --alias <alias> --tunnel-port <port> [--json]
|
|
22
|
+
ocx link status [--json]
|
|
23
|
+
ocx link revoke --link-id <id> [--json]`;
|
|
24
|
+
|
|
25
|
+
type LinkState = "connecting" | "connected" | "reconnecting" | "failed" | "idle";
|
|
26
|
+
type ListenerState = "off" | "listening" | "failed";
|
|
27
|
+
|
|
28
|
+
export interface LinkStatusPayload {
|
|
29
|
+
role: "standalone" | "home" | "child";
|
|
30
|
+
listener: { state: ListenerState; port: number | null };
|
|
31
|
+
links: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
alias: string;
|
|
34
|
+
direction: LinkDirection;
|
|
35
|
+
state: LinkState;
|
|
36
|
+
since: string;
|
|
37
|
+
reason: string | null;
|
|
38
|
+
tunnelPort: number;
|
|
39
|
+
}>;
|
|
40
|
+
child: null | {
|
|
41
|
+
alias: string;
|
|
42
|
+
state: LinkState;
|
|
43
|
+
since: string;
|
|
44
|
+
reason: string | null;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface LinkIssuePayload {
|
|
49
|
+
linkId: string;
|
|
50
|
+
apiKeyId: string;
|
|
51
|
+
key: string;
|
|
52
|
+
listenerPort: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LinkCliDeps extends RuntimeApiDeps {
|
|
56
|
+
choosePort?: () => Promise<number>;
|
|
57
|
+
readStore?: () => LinkStore;
|
|
58
|
+
readAdminToken?: () => string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const LINK_ID = /^lnk_[0-9a-f]{16}$/;
|
|
62
|
+
const API_KEY_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
63
|
+
const DATA_KEY = /^ocx_data_[0-9a-f]{40}$/;
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
|
|
70
|
+
const expected = new Set(keys);
|
|
71
|
+
const actual = Object.keys(value);
|
|
72
|
+
if (actual.length !== expected.size || actual.some(key => !expected.has(key))) {
|
|
73
|
+
throw new Error(`invalid link API response: ${label} fields`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validListenerPort(value: unknown): value is number {
|
|
78
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function validString(value: unknown): value is string {
|
|
82
|
+
return typeof value === "string" && value.length > 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validNullableString(value: unknown): value is string | null {
|
|
86
|
+
return value === null || typeof value === "string";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validateStatus(value: unknown): LinkStatusPayload {
|
|
90
|
+
if (!isRecord(value)) throw new Error("invalid link API response: status object");
|
|
91
|
+
assertExactKeys(value, ["role", "listener", "links", "child"], "status");
|
|
92
|
+
if (value.role !== "standalone" && value.role !== "home" && value.role !== "child") {
|
|
93
|
+
throw new Error("invalid link API response: status role");
|
|
94
|
+
}
|
|
95
|
+
if (!isRecord(value.listener)) throw new Error("invalid link API response: listener");
|
|
96
|
+
assertExactKeys(value.listener, ["state", "port"], "listener");
|
|
97
|
+
if (value.listener.state !== "off" && value.listener.state !== "listening" && value.listener.state !== "failed") {
|
|
98
|
+
throw new Error("invalid link API response: listener state");
|
|
99
|
+
}
|
|
100
|
+
if (value.listener.port !== null && !validListenerPort(value.listener.port)) {
|
|
101
|
+
throw new Error("invalid link API response: listener port");
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(value.links)) throw new Error("invalid link API response: links");
|
|
104
|
+
const links = value.links.map((candidate, index) => {
|
|
105
|
+
if (!isRecord(candidate)) throw new Error(`invalid link API response: link ${index}`);
|
|
106
|
+
assertExactKeys(candidate, ["id", "alias", "direction", "state", "since", "reason", "tunnelPort"], `link ${index}`);
|
|
107
|
+
if (typeof candidate.id !== "string" || !LINK_ID.test(candidate.id)
|
|
108
|
+
|| !validString(candidate.alias) || (candidate.direction !== "hub-initiated" && candidate.direction !== "client-initiated")
|
|
109
|
+
|| (candidate.state !== "connecting" && candidate.state !== "connected" && candidate.state !== "reconnecting" && candidate.state !== "failed" && candidate.state !== "idle")
|
|
110
|
+
|| !validString(candidate.since) || !validNullableString(candidate.reason) || !isLinkPort(candidate.tunnelPort)) {
|
|
111
|
+
throw new Error(`invalid link API response: link ${index} fields`);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
id: candidate.id,
|
|
115
|
+
alias: candidate.alias,
|
|
116
|
+
direction: candidate.direction,
|
|
117
|
+
state: candidate.state,
|
|
118
|
+
since: candidate.since,
|
|
119
|
+
reason: candidate.reason,
|
|
120
|
+
tunnelPort: candidate.tunnelPort,
|
|
121
|
+
} as LinkStatusPayload["links"][number];
|
|
122
|
+
});
|
|
123
|
+
let child: LinkStatusPayload["child"] = null;
|
|
124
|
+
if (value.child !== null) {
|
|
125
|
+
if (!isRecord(value.child)) throw new Error("invalid link API response: child");
|
|
126
|
+
assertExactKeys(value.child, ["alias", "state", "since", "reason"], "child");
|
|
127
|
+
if (!validString(value.child.alias)
|
|
128
|
+
|| (value.child.state !== "connecting" && value.child.state !== "connected" && value.child.state !== "reconnecting" && value.child.state !== "failed" && value.child.state !== "idle")
|
|
129
|
+
|| !validString(value.child.since) || !validNullableString(value.child.reason)) {
|
|
130
|
+
throw new Error("invalid link API response: child fields");
|
|
131
|
+
}
|
|
132
|
+
child = {
|
|
133
|
+
alias: value.child.alias,
|
|
134
|
+
state: value.child.state,
|
|
135
|
+
since: value.child.since,
|
|
136
|
+
reason: value.child.reason,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
role: value.role,
|
|
141
|
+
listener: { state: value.listener.state, port: value.listener.port },
|
|
142
|
+
links,
|
|
143
|
+
child,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateIssue(value: unknown): LinkIssuePayload {
|
|
148
|
+
if (!isRecord(value)) throw new Error("invalid link API response: issue object");
|
|
149
|
+
assertExactKeys(value, ["linkId", "apiKeyId", "key", "listenerPort"], "issue");
|
|
150
|
+
if (typeof value.linkId !== "string" || !LINK_ID.test(value.linkId)
|
|
151
|
+
|| typeof value.apiKeyId !== "string" || !API_KEY_ID.test(value.apiKeyId)
|
|
152
|
+
|| typeof value.key !== "string" || !DATA_KEY.test(value.key)
|
|
153
|
+
|| !validListenerPort(value.listenerPort)) {
|
|
154
|
+
throw new Error("invalid link API response: issue fields");
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
linkId: value.linkId,
|
|
158
|
+
apiKeyId: value.apiKeyId,
|
|
159
|
+
key: value.key,
|
|
160
|
+
listenerPort: value.listenerPort,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validateRevoke(value: unknown, expectedLinkId: string): void {
|
|
165
|
+
if (value === null) return;
|
|
166
|
+
if (!isRecord(value)) throw new Error("invalid link API response: revoke object");
|
|
167
|
+
assertExactKeys(value, ["linkId"], "revoke");
|
|
168
|
+
if (value.linkId !== expectedLinkId) throw new Error("invalid link API response: revoke link id");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function localStatus(store: LinkStore): LinkStatusPayload {
|
|
172
|
+
const hasLinks = store.links.length > 0;
|
|
173
|
+
return {
|
|
174
|
+
role: hasLinks ? "home" : "standalone",
|
|
175
|
+
listener: {
|
|
176
|
+
state: hasLinks && store.listenerPort !== null ? "listening" : "off",
|
|
177
|
+
port: hasLinks ? store.listenerPort : null,
|
|
178
|
+
},
|
|
179
|
+
links: store.links.map(link => ({
|
|
180
|
+
id: link.id,
|
|
181
|
+
alias: link.alias,
|
|
182
|
+
direction: link.direction,
|
|
183
|
+
state: "idle",
|
|
184
|
+
since: link.createdAt,
|
|
185
|
+
reason: null,
|
|
186
|
+
tunnelPort: link.tunnelPort,
|
|
187
|
+
})),
|
|
188
|
+
child: null,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function linkRequest<T>(path: string, init: RequestInit, deps: LinkCliDeps): Promise<T> {
|
|
193
|
+
const hasTokenOverride = deps.readAdminToken !== undefined;
|
|
194
|
+
const token = deps.readAdminToken?.() ?? null;
|
|
195
|
+
const headers = new Headers(init.headers);
|
|
196
|
+
if (hasTokenOverride) headers.set("x-opencodex-api-key", token ?? "");
|
|
197
|
+
const requestInit: RequestInit = { ...init, headers };
|
|
198
|
+
try {
|
|
199
|
+
return await runtimeRequest<T>(path, requestInit, deps);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error instanceof RuntimeApiError) {
|
|
202
|
+
// Never echo an API error body: issue responses contain a one-time data key. Only the error
|
|
203
|
+
// code survives, because callers branch on it and a code is never secret.
|
|
204
|
+
throw new RuntimeApiError(`Link management request failed (${error.status})`, error.status, errorCodeBody(error.body));
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function errorCodeBody(body: unknown): { error: { code: string } } | null {
|
|
211
|
+
if (!isRecord(body) || !isRecord(body.error)) return null;
|
|
212
|
+
const code = body.error.code;
|
|
213
|
+
return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? { error: { code } } : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function runPort(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
217
|
+
takeJsonFlag(args);
|
|
218
|
+
rejectArgs(args, LINK_USAGE);
|
|
219
|
+
const port = await (deps.choosePort ?? (() => findAvailablePort(0, "127.0.0.1")))();
|
|
220
|
+
if (!isLinkPort(port)) throw new Error("port allocator returned an invalid link port");
|
|
221
|
+
console.log(JSON.stringify({ port }));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function runIssue(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
225
|
+
takeJsonFlag(args);
|
|
226
|
+
const alias = takeOption(args, "--alias");
|
|
227
|
+
const tunnelPort = takeIntegerOption(args, "--tunnel-port", { min: 1024 });
|
|
228
|
+
if (!alias || tunnelPort === undefined || !isLinkPort(tunnelPort)) {
|
|
229
|
+
throw new CliUsageError("issue requires --alias and --tunnel-port", LINK_USAGE);
|
|
230
|
+
}
|
|
231
|
+
try { assertSshAlias(alias); }
|
|
232
|
+
catch { throw new CliUsageError("--alias must be a valid SSH host alias", LINK_USAGE); }
|
|
233
|
+
rejectArgs(args, LINK_USAGE);
|
|
234
|
+
const result = validateIssue(await linkRequest<unknown>("/api/link/issue", {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { "content-type": "application/json" },
|
|
237
|
+
body: JSON.stringify({ alias, tunnelPort }),
|
|
238
|
+
}, deps));
|
|
239
|
+
console.log(JSON.stringify(result));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function runStatus(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
243
|
+
takeJsonFlag(args);
|
|
244
|
+
rejectArgs(args, LINK_USAGE);
|
|
245
|
+
let baseUrl: string;
|
|
246
|
+
try {
|
|
247
|
+
baseUrl = await runtimeBaseUrl(deps);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (!deps.baseUrl && error instanceof RuntimeApiError && error.status === 503) {
|
|
250
|
+
const store = (deps.readStore ?? (() => readLinkStore(linkStorePath())))();
|
|
251
|
+
console.log(JSON.stringify(localStatus(store)));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
const result = validateStatus(await linkRequest<unknown>("/api/link/status", {}, { ...deps, baseUrl }));
|
|
257
|
+
console.log(JSON.stringify(result));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function runRevoke(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
261
|
+
takeJsonFlag(args);
|
|
262
|
+
const linkId = takeOption(args, "--link-id");
|
|
263
|
+
if (!linkId || !LINK_ID.test(linkId)) throw new CliUsageError("revoke requires a valid --link-id", LINK_USAGE);
|
|
264
|
+
rejectArgs(args, LINK_USAGE);
|
|
265
|
+
try {
|
|
266
|
+
const response = await linkRequest<unknown>(`/api/link/${encodeURIComponent(linkId)}`, { method: "DELETE" }, deps);
|
|
267
|
+
validateRevoke(response, linkId);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Revoke is idempotent: a link the Home no longer has is already revoked, and a Child retrying
|
|
270
|
+
// a join rollback depends on that answer being success. A 404 without this code comes from a
|
|
271
|
+
// listener that does not serve the management API and stays a failure.
|
|
272
|
+
const code = error instanceof RuntimeApiError && error.status === 404 ? errorCodeBody(error.body)?.error.code : undefined;
|
|
273
|
+
if (code !== "link_not_found") throw error;
|
|
274
|
+
}
|
|
275
|
+
console.log(JSON.stringify({ linkId }));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function runLinkCommand(rawArgs: string[], deps: LinkCliDeps = {}): Promise<number> {
|
|
279
|
+
return runCliAction(async () => {
|
|
280
|
+
const args = [...rawArgs];
|
|
281
|
+
const command = args.shift();
|
|
282
|
+
if (command === "port") await runPort(args, deps);
|
|
283
|
+
else if (command === "issue") await runIssue(args, deps);
|
|
284
|
+
else if (command === "status") await runStatus(args, deps);
|
|
285
|
+
else if (command === "revoke") await runRevoke(args, deps);
|
|
286
|
+
else throw new CliUsageError("link requires one of: port, issue, status, revoke", LINK_USAGE);
|
|
287
|
+
});
|
|
288
|
+
}
|
package/src/cli/registry.ts
CHANGED
|
@@ -130,6 +130,18 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
130
130
|
"Credentials are accepted only through stdin; argv and environment credential forms are not supported.",
|
|
131
131
|
],
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
name: "link",
|
|
135
|
+
usage: "ocx link <port|issue|status|revoke>",
|
|
136
|
+
summary: "Allocate and manage a loopback remote home link.",
|
|
137
|
+
details: [
|
|
138
|
+
"Port: ocx link port [--json]",
|
|
139
|
+
"Issue: ocx link issue --alias <alias> --tunnel-port <port> [--json]",
|
|
140
|
+
"Status: ocx link status [--json]",
|
|
141
|
+
"Revoke: ocx link revoke --link-id <id> [--json]",
|
|
142
|
+
"Issue, status, and revoke use the running proxy's loopback management API and admin token.",
|
|
143
|
+
],
|
|
144
|
+
},
|
|
133
145
|
{
|
|
134
146
|
name: "remote-workspace",
|
|
135
147
|
usage: "ocx remote-workspace <pair|agent|status>",
|
|
@@ -391,6 +403,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
391
403
|
],
|
|
392
404
|
},
|
|
393
405
|
{ name: "api-key", usage: "ocx api-key <list|create|rotate|remove> ...", summary: "Alias of ocx access key." },
|
|
406
|
+
{
|
|
407
|
+
name: "api",
|
|
408
|
+
usage: "ocx api <protocols|explain|policy> ...",
|
|
409
|
+
summary: "Inspect protocol paths, preview a request path, and read or change the protocol policy.",
|
|
410
|
+
details: [
|
|
411
|
+
"protocols [--provider <name>] Contract version, API surfaces, protocol settings and feature vocabulary.",
|
|
412
|
+
"explain --model <id> --inbound <responses|chat|messages> [--feature <key>]... Preview the request path; sends nothing upstream.",
|
|
413
|
+
"policy Read the protocol policy; with --messages, --unrepresentable or --rollout <switch>=<on|off> it changes config.",
|
|
414
|
+
"Every rollout switch defaults off. `ocx api policy` writes only when a setting flag is given.",
|
|
415
|
+
],
|
|
416
|
+
},
|
|
394
417
|
{
|
|
395
418
|
name: "export",
|
|
396
419
|
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|aside|raycast|omo|cline> [--json] [--out <path>] [--force]",
|