@bitkyc08/opencodex 2.83.0 → 2.84.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/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { assertSshAlias } from "../link/ssh-argv";
|
|
3
|
+
import { CliUsageError, RuntimeApiError, runtimeBaseUrl, rejectArgs, takeJsonFlag, takeOption, printData } from "./runtime-api";
|
|
4
|
+
import type { LinkCliDeps } from "./link";
|
|
5
|
+
|
|
6
|
+
const FINGERPRINT = /^SHA256:[A-Za-z0-9+/]{16,88}={0,2}$/;
|
|
7
|
+
const ENROLLMENT_DEADLINE_MS = 360_000;
|
|
8
|
+
const PHASES: Record<string, string> = {
|
|
9
|
+
remote_port_failed: "remote port selection", remote_port_timeout: "remote port selection",
|
|
10
|
+
child_preflight_failed: "Child preflight", child_preflight_timeout: "Child preflight",
|
|
11
|
+
child_restart_unsupported: "Child restart preflight", child_curl_missing: "Child curl preflight",
|
|
12
|
+
home_setup_failed: "Home setup", home_setup_timeout: "Home setup",
|
|
13
|
+
remote_connect_failed: "Child enrollment", remote_connect_timeout: "Child enrollment",
|
|
14
|
+
child_restart_failed: "Child restart", child_restart_timeout: "Child restart",
|
|
15
|
+
child_relay_failed: "Child relay verification", child_relay_timeout: "Child relay verification",
|
|
16
|
+
child_runtime_not_client: "Child runtime role verification",
|
|
17
|
+
remote_platform_unsupported: "remote platform verification", remote_ocx_missing: "remote OpenCodex discovery",
|
|
18
|
+
};
|
|
19
|
+
const RECOVERY = "Run ocx disconnect and ocx restart on the Child before retrying.";
|
|
20
|
+
type LinkRequest = <T>(path: string, init: RequestInit, deps: LinkCliDeps) => Promise<T>;
|
|
21
|
+
|
|
22
|
+
function record(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
23
|
+
function offered(value: unknown): { alias: string; fingerprint: string; keyType: string; ocxVersion: string } {
|
|
24
|
+
if (!record(value) || Object.keys(value).sort().join(",") !== "alias,fingerprint,keyType,ocxVersion"
|
|
25
|
+
|| typeof value.alias !== "string" || typeof value.fingerprint !== "string" || !FINGERPRINT.test(value.fingerprint)
|
|
26
|
+
|| typeof value.keyType !== "string" || !/^[A-Za-z0-9@._+-]{1,64}$/.test(value.keyType)) throw new Error("Invalid host probe response");
|
|
27
|
+
if (typeof value.ocxVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]{1,128})?$/.test(value.ocxVersion)) throw new Error("Invalid host version response");
|
|
28
|
+
return { alias: value.alias, fingerprint: value.fingerprint, keyType: value.keyType, ocxVersion: value.ocxVersion };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runLinkEnrollment(command: "probe" | "add", args: string[], deps: LinkCliDeps, request: LinkRequest, usage: string): Promise<void> {
|
|
32
|
+
const wantsJson = takeJsonFlag(args);
|
|
33
|
+
const expected = takeOption(args, "--expect-fingerprint");
|
|
34
|
+
const alias = args.shift();
|
|
35
|
+
if (!alias) throw new CliUsageError(command + " requires an SSH alias", usage);
|
|
36
|
+
try { assertSshAlias(alias); } catch { throw new CliUsageError("Invalid SSH alias", usage); }
|
|
37
|
+
rejectArgs(args, usage);
|
|
38
|
+
if (command === "probe" && expected !== undefined) throw new CliUsageError("--expect-fingerprint is only for link add", usage);
|
|
39
|
+
if (expected !== undefined && !FINGERPRINT.test(expected)) throw new CliUsageError("Expected a SHA256 SSH fingerprint", usage);
|
|
40
|
+
if (command === "add" && !expected && !(deps.stdinImpl ?? process.stdin).isTTY) {
|
|
41
|
+
throw new CliUsageError("Non-interactive link add requires --expect-fingerprint", usage);
|
|
42
|
+
}
|
|
43
|
+
const timeout = deps.enrollmentTimeoutMs ?? ENROLLMENT_DEADLINE_MS;
|
|
44
|
+
if (!Number.isFinite(timeout) || timeout < 1 || timeout > ENROLLMENT_DEADLINE_MS) throw new Error("Invalid link enrollment deadline");
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
const expiry = new Promise<never>((_, reject) => {
|
|
48
|
+
timer = setTimeout(() => { controller.abort(); reject(new Error("Link enrollment timed out; its outcome is uncertain. Run ocx link status on the Home and ocx connect status on the Child before retrying.")); }, timeout);
|
|
49
|
+
});
|
|
50
|
+
let boundDeps = deps;
|
|
51
|
+
const post = async <T>(path: string, body: unknown): Promise<T> => {
|
|
52
|
+
controller.signal.throwIfAborted();
|
|
53
|
+
const result = await request<T>(path, { method: "POST", redirect: "error", signal: controller.signal, headers: { "content-type": "application/json" }, body: JSON.stringify(body) }, boundDeps);
|
|
54
|
+
controller.signal.throwIfAborted();
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
let rl: ReturnType<typeof createInterface> | undefined;
|
|
58
|
+
const work = async () => {
|
|
59
|
+
boundDeps = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
60
|
+
controller.signal.throwIfAborted();
|
|
61
|
+
const probe = offered(await post<unknown>("/api/link/probe", { alias, includeVersion: true }));
|
|
62
|
+
if (probe.alias !== alias) throw new Error("Host probe alias did not match");
|
|
63
|
+
if (command === "probe") { printData(probe, wantsJson, [probe.alias + " " + probe.keyType + " " + probe.fingerprint + " opencodex " + probe.ocxVersion]); return; }
|
|
64
|
+
if (expected && expected !== probe.fingerprint) throw new CliUsageError("The offered host fingerprint does not match --expect-fingerprint");
|
|
65
|
+
if (!expected) {
|
|
66
|
+
let approved: boolean;
|
|
67
|
+
if (deps.confirmFingerprint) approved = await deps.confirmFingerprint(probe);
|
|
68
|
+
else {
|
|
69
|
+
rl = createInterface({ input: deps.stdinImpl ?? process.stdin, output: process.stderr });
|
|
70
|
+
approved = (await rl.question("Confirm " + alias + " " + probe.keyType + " " + probe.fingerprint + " opencodex " + probe.ocxVersion + " (type yes): ", { signal: controller.signal })).trim() === "yes";
|
|
71
|
+
}
|
|
72
|
+
controller.signal.throwIfAborted();
|
|
73
|
+
if (!approved) throw new CliUsageError("Host fingerprint was not confirmed");
|
|
74
|
+
}
|
|
75
|
+
const confirm = await post<unknown>("/api/link/confirm-host", { alias, fingerprint: probe.fingerprint });
|
|
76
|
+
if (!record(confirm) || Object.keys(confirm).sort().join(",") !== "alias,fingerprint,ocxVersion" || confirm.alias !== alias || confirm.fingerprint !== probe.fingerprint
|
|
77
|
+
|| typeof confirm.ocxVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]{1,128})?$/.test(confirm.ocxVersion)) throw new Error("Invalid host confirmation response");
|
|
78
|
+
const result = await post<unknown>("/api/link/apply", { alias, fingerprint: probe.fingerprint });
|
|
79
|
+
if (!record(result) || Object.keys(result).join(",") !== "linkId" || typeof result.linkId !== "string" || !/^lnk_[0-9a-f]{16}$/.test(result.linkId)) throw new Error("Invalid link apply response");
|
|
80
|
+
printData({ linkId: result.linkId, alias, state: "connected", childRole: "client" }, wantsJson, ["Connected " + alias + " as a Child (" + result.linkId + ")."]);
|
|
81
|
+
};
|
|
82
|
+
try { await Promise.race([work(), expiry]); }
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof RuntimeApiError && record(error.body)) {
|
|
85
|
+
const code = record(error.body.error) ? error.body.error.code : undefined;
|
|
86
|
+
const phase = typeof code === "string" && Object.hasOwn(PHASES, code) ? PHASES[code] : undefined;
|
|
87
|
+
const message = phase ? "Remote Link failed during " + phase : error.message;
|
|
88
|
+
if (phase || error.body.residual === true) throw new Error(message + (error.body.residual === true ? ". " + RECOVERY : ""));
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
} finally {
|
|
92
|
+
if (timer) clearTimeout(timer);
|
|
93
|
+
controller.abort();
|
|
94
|
+
rl?.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/cli/link.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { clientLinkStatusIdentity } from "../client/link-status";
|
|
2
|
+
import { clientLinkTunnelStatus } from "../client/link-tunnel";
|
|
3
|
+
import { loadConfig } from "../config";
|
|
4
|
+
import { projectLinkStatus, type ClientLinkIdentity, type LinkStatusConfig, type LinkStatusDto } from "../link/status-projection";
|
|
5
|
+
import { runLinkEnrollment } from "./link-enrollment";
|
|
1
6
|
import { assertSshAlias } from "../link/ssh-argv";
|
|
2
7
|
import { linkStorePath } from "../link/paths";
|
|
3
8
|
import { isLinkPort } from "../link/ports";
|
|
@@ -19,10 +24,13 @@ import {
|
|
|
19
24
|
} from "./runtime-api";
|
|
20
25
|
|
|
21
26
|
export const LINK_USAGE = `Usage:
|
|
27
|
+
ocx link probe <alias> [--json]
|
|
28
|
+
ocx link add <alias> [--expect-fingerprint SHA256:…] [--json]
|
|
22
29
|
ocx link port [--json]
|
|
23
30
|
ocx link issue --alias <alias> --tunnel-port <port> [--json]
|
|
24
31
|
ocx link status [--json]
|
|
25
32
|
ocx link revoke --link-id <id> [--force --yes] [--json]
|
|
33
|
+
ocx link reconnect --link-id <id> [--json]
|
|
26
34
|
|
|
27
35
|
Forced revoke skips remote cleanup. Run ocx disconnect on the remote client.`;
|
|
28
36
|
|
|
@@ -57,7 +65,13 @@ export interface LinkIssuePayload {
|
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
export interface LinkCliDeps extends RuntimeApiDeps {
|
|
68
|
+
/** Test seam can shorten, never extend, the whole enrollment deadline. */
|
|
69
|
+
enrollmentTimeoutMs?: number;
|
|
60
70
|
choosePort?: () => Promise<number>;
|
|
71
|
+
readConfig?: () => LinkStatusConfig;
|
|
72
|
+
readClientLinkIdentity?: (linkId: string) => ClientLinkIdentity | null;
|
|
73
|
+
readChildStatus?: () => LinkStatusDto["child"];
|
|
74
|
+
confirmFingerprint?: (host: { alias: string; fingerprint: string; keyType: string }) => Promise<boolean>;
|
|
61
75
|
readStore?: () => LinkStore;
|
|
62
76
|
readAdminToken?: () => string | null;
|
|
63
77
|
}
|
|
@@ -172,25 +186,12 @@ function validateRevoke(value: unknown, expectedLinkId: string): void {
|
|
|
172
186
|
if (value.linkId !== expectedLinkId) throw new Error("invalid link API response: revoke link id");
|
|
173
187
|
}
|
|
174
188
|
|
|
175
|
-
function localStatus(store: LinkStore): LinkStatusPayload {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
port: hasLinks ? store.listenerPort : null,
|
|
182
|
-
},
|
|
183
|
-
links: store.links.map(link => ({
|
|
184
|
-
id: link.id,
|
|
185
|
-
alias: link.alias,
|
|
186
|
-
direction: link.direction,
|
|
187
|
-
state: "idle",
|
|
188
|
-
since: link.createdAt,
|
|
189
|
-
reason: null,
|
|
190
|
-
tunnelPort: link.tunnelPort,
|
|
191
|
-
})),
|
|
192
|
-
child: null,
|
|
193
|
-
};
|
|
189
|
+
function localStatus(store: LinkStore, deps: LinkCliDeps): LinkStatusPayload {
|
|
190
|
+
const config = (deps.readConfig ?? loadConfig)();
|
|
191
|
+
const identity = config.client?.transport === "link" && config.client.link
|
|
192
|
+
? (deps.readClientLinkIdentity ?? clientLinkStatusIdentity)(config.client.link.linkId) : null;
|
|
193
|
+
return projectLinkStatus(store, [], { state: store.links.length > 0 && store.listenerPort !== null ? "listening" : "off", port: store.links.length > 0 ? store.listenerPort : null },
|
|
194
|
+
config, { version: 1, entries: {} }, (deps.readChildStatus ?? clientLinkTunnelStatus)(), identity);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
async function linkRequest<T>(path: string, init: RequestInit, deps: LinkCliDeps): Promise<T> {
|
|
@@ -211,10 +212,10 @@ async function linkRequest<T>(path: string, init: RequestInit, deps: LinkCliDeps
|
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
function errorCodeBody(body: unknown): { error: { code: string } } | null {
|
|
215
|
+
function errorCodeBody(body: unknown): { error: { code: string }; residual?: true } | null {
|
|
215
216
|
if (!isRecord(body) || !isRecord(body.error)) return null;
|
|
216
217
|
const code = body.error.code;
|
|
217
|
-
return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? { error: { code } } : null;
|
|
218
|
+
return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? { error: { code }, ...(body.residual === true ? { residual: true as const } : {}) } : null;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
async function runPort(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
@@ -252,7 +253,7 @@ async function runStatus(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
|
252
253
|
} catch (error) {
|
|
253
254
|
if (!deps.baseUrl && error instanceof RuntimeApiError && error.status === 503) {
|
|
254
255
|
const store = (deps.readStore ?? (() => readLinkStore(linkStorePath())))();
|
|
255
|
-
console.log(JSON.stringify(localStatus(store)));
|
|
256
|
+
console.log(JSON.stringify(localStatus(store, deps)));
|
|
256
257
|
return;
|
|
257
258
|
}
|
|
258
259
|
throw error;
|
|
@@ -303,14 +304,28 @@ async function runRevoke(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
|
303
304
|
console.log(JSON.stringify({ linkId }));
|
|
304
305
|
}
|
|
305
306
|
|
|
307
|
+
async function runReconnect(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
308
|
+
takeJsonFlag(args);
|
|
309
|
+
const linkId = takeOption(args, "--link-id");
|
|
310
|
+
if (!linkId || !LINK_ID.test(linkId)) throw new CliUsageError("reconnect requires a valid --link-id", LINK_USAGE);
|
|
311
|
+
rejectArgs(args, LINK_USAGE);
|
|
312
|
+
const response = await linkRequest<unknown>(`/api/link/${encodeURIComponent(linkId)}/reconnect`, { method: "POST" }, deps);
|
|
313
|
+
if (!isRecord(response)) throw new Error("invalid link API response: reconnect object");
|
|
314
|
+
assertExactKeys(response, ["linkId"], "reconnect");
|
|
315
|
+
if (response.linkId !== linkId) throw new Error("invalid link API response: reconnect link id");
|
|
316
|
+
console.log(JSON.stringify({ linkId }));
|
|
317
|
+
}
|
|
318
|
+
|
|
306
319
|
export async function runLinkCommand(rawArgs: string[], deps: LinkCliDeps = {}): Promise<number> {
|
|
307
320
|
return runCliAction(async () => {
|
|
308
321
|
const args = [...rawArgs];
|
|
309
322
|
const command = args.shift();
|
|
310
|
-
if (command === "
|
|
323
|
+
if (command === "probe" || command === "add") await runLinkEnrollment(command, args, deps, linkRequest, LINK_USAGE);
|
|
324
|
+
else if (command === "port") await runPort(args, deps);
|
|
311
325
|
else if (command === "issue") await runIssue(args, deps);
|
|
312
326
|
else if (command === "status") await runStatus(args, deps);
|
|
313
327
|
else if (command === "revoke") await runRevoke(args, deps);
|
|
314
|
-
else
|
|
328
|
+
else if (command === "reconnect") await runReconnect(args, deps);
|
|
329
|
+
else throw new CliUsageError("link requires one of: probe, add, port, issue, status, revoke, reconnect", LINK_USAGE);
|
|
315
330
|
});
|
|
316
331
|
}
|
|
@@ -89,7 +89,12 @@ export async function withObserveStream(
|
|
|
89
89
|
// Snapshot recovery comes from our observed target, never arbitrary transport text.
|
|
90
90
|
if (context.kind === "snapshot") {
|
|
91
91
|
if (!found) throw new ObservationError("Proxy is not running. Start it with: ocx start");
|
|
92
|
-
if (found.role === "client")
|
|
92
|
+
if (found.role === "client") {
|
|
93
|
+
const { readLinkChildContext, linkChildRefusal } = await import("./link-child");
|
|
94
|
+
const link = readLinkChildContext();
|
|
95
|
+
throw new ObservationError(link ? linkChildRefusal(link)
|
|
96
|
+
: "This listener runs in the client role and has no management API. Run this command on its serving hub, or open the hub dashboard.");
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
return found;
|
|
95
100
|
} }), signal);
|
package/src/cli/observe.ts
CHANGED
|
@@ -113,8 +113,12 @@ async function logs(argv: string[], deps: ObserveStreamDeps): Promise<number> {
|
|
|
113
113
|
return followLogs({ query: new URLSearchParams(query(params)), limit, jsonl: wantsJsonl, events: wantsEvents, formatRow: formatLog }, deps);
|
|
114
114
|
}
|
|
115
115
|
const data = await runtimeRequest(`/api/logs${query(params)}`, {}, deps);
|
|
116
|
+
const rows = logRows(data);
|
|
117
|
+
if (!wantsJson && !wantsJsonl && rows.length === 0 && [provider, model, status, conversationId, account].some(Boolean)) {
|
|
118
|
+
console.log("No log entry matched the filters.");
|
|
119
|
+
}
|
|
116
120
|
if (wantsJson) printData(data, true);
|
|
117
|
-
else for (const row of
|
|
121
|
+
else for (const row of rows) {
|
|
118
122
|
if (wantsJsonl) console.log(JSON.stringify(row));
|
|
119
123
|
else console.log(formatLog(row));
|
|
120
124
|
}
|
|
@@ -206,8 +210,12 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
206
210
|
if (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint) {
|
|
207
211
|
throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
|
|
208
212
|
}
|
|
213
|
+
// A Remote Link Child sends this read through the connection-bound link relay (#6957).
|
|
214
|
+
const link = connection.value.transport === "link" && connection.value.link;
|
|
215
|
+
const peerFetch = deps.fetchImpl
|
|
216
|
+
?? (link ? (await import("../client/link-home")).linkBoundFetch(connection.value, token.token) : undefined);
|
|
209
217
|
result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix), {
|
|
210
|
-
fetchImpl:
|
|
218
|
+
fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
|
|
211
219
|
});
|
|
212
220
|
const current = readClientConnectionState();
|
|
213
221
|
const currentToken = readServiceApiTokenState();
|
package/src/cli/provider.ts
CHANGED
|
@@ -584,8 +584,19 @@ export async function handleProviderCommand(args: string[], deps: ProviderComman
|
|
|
584
584
|
return;
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
+
// #6957: a link Child's local providers are inactive; changing them is refused like any other
|
|
588
|
+
// provider change, which belongs to the Home.
|
|
589
|
+
if (sub === "add" || sub === "remove" || sub === "set-default") {
|
|
590
|
+
const refusal = (await import("./link-child")).linkChildProviderChangeRefusal(`ocx provider ${sub}`);
|
|
591
|
+
if (refusal) {
|
|
592
|
+
console.error(refusal);
|
|
593
|
+
process.exitCode = 1;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
587
597
|
switch (sub) {
|
|
588
598
|
case "list":
|
|
599
|
+
await (await import("./link-child")).printLinkChildLocalNotice("The providers");
|
|
589
600
|
handleList(subArgs);
|
|
590
601
|
break;
|
|
591
602
|
case "add":
|
|
@@ -595,6 +606,7 @@ export async function handleProviderCommand(args: string[], deps: ProviderComman
|
|
|
595
606
|
handleRemove(subArgs);
|
|
596
607
|
break;
|
|
597
608
|
case "show":
|
|
609
|
+
await (await import("./link-child")).printLinkChildLocalNotice("The provider settings");
|
|
598
610
|
handleShow(subArgs);
|
|
599
611
|
break;
|
|
600
612
|
case "set-default":
|
package/src/cli/registry.ts
CHANGED
|
@@ -144,14 +144,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
name: "link",
|
|
147
|
-
usage: "ocx link <port|issue|status|revoke>",
|
|
147
|
+
usage: "ocx link <port|issue|status|revoke|reconnect>",
|
|
148
148
|
summary: "Allocate and manage a loopback remote home link.",
|
|
149
149
|
details: [
|
|
150
150
|
"Port: ocx link port [--json]",
|
|
151
151
|
"Issue: ocx link issue --alias <alias> --tunnel-port <port> [--json]",
|
|
152
152
|
"Status: ocx link status [--json]",
|
|
153
153
|
"Revoke: ocx link revoke --link-id <id> [--force --yes] [--json]",
|
|
154
|
-
"
|
|
154
|
+
"Reconnect: ocx link reconnect --link-id <id> [--json]",
|
|
155
|
+
"Issue, status, revoke, and reconnect use the running proxy's loopback management API and admin token.",
|
|
155
156
|
],
|
|
156
157
|
},
|
|
157
158
|
{
|
|
@@ -245,8 +246,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
245
246
|
},
|
|
246
247
|
{
|
|
247
248
|
name: "hub",
|
|
248
|
-
usage: "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]",
|
|
249
|
-
summary: "
|
|
249
|
+
usage: "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]\n ocx hub disable [--revoke-clients --client-key <id> ...] [--json]",
|
|
250
|
+
summary: "Invite another machine, or disable hub mode in one validated config write.",
|
|
250
251
|
details: [
|
|
251
252
|
"Topology: a hub serves ONE port. Remote machines dial `hostname:port` with their own per-client",
|
|
252
253
|
"key; the hub's own local processes dial `127.0.0.1:<same port>` with no credential, through the",
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { findLiveProxy, probeHostname, type LivenessIo, type LiveProxy } from "../server/proxy-liveness";
|
|
13
13
|
import { runningProxyUpdateHeaders } from "../oauth/login-cli";
|
|
14
14
|
import { redactSecretArgs } from "./secret-args";
|
|
15
|
+
import type { LinkHomeDeps } from "../client/link-home";
|
|
15
16
|
export { redactSecretArgs };
|
|
16
17
|
|
|
17
18
|
export type CliStdin = NodeJS.ReadableStream & { isTTY?: boolean; readableEnded?: boolean };
|
|
@@ -28,6 +29,8 @@ export interface RuntimeApiDeps {
|
|
|
28
29
|
stdinSignal?: AbortSignal;
|
|
29
30
|
/** Optional proxy liveness probe injection for commands that check or fall back around live runtime state. */
|
|
30
31
|
findLiveProxy?: (io?: LivenessIo) => Promise<LiveProxy | null>;
|
|
32
|
+
/** Remote Link Child: how allowlisted reads reach the Home (#6957). */
|
|
33
|
+
linkHome?: LinkHomeDeps;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export class CliUsageError extends Error {
|
|
@@ -45,7 +48,7 @@ export class RuntimeApiError extends Error {
|
|
|
45
48
|
readonly status: number,
|
|
46
49
|
readonly body: unknown,
|
|
47
50
|
/** Local discovery refusals occur before the management request is sent. */
|
|
48
|
-
readonly code?: "proxy_not_running" | "client_role_management_unavailable",
|
|
51
|
+
readonly code?: "proxy_not_running" | "client_role_management_unavailable" | "link_child_management_unavailable",
|
|
49
52
|
) {
|
|
50
53
|
super(message);
|
|
51
54
|
this.name = "RuntimeApiError";
|
|
@@ -83,6 +86,10 @@ export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string>
|
|
|
83
86
|
// this costs no extra request. Only the client role is refused: an absent role is a
|
|
84
87
|
// standalone or hub proxy (or a legacy body that predates the field), and both serve /api/*.
|
|
85
88
|
if (live.role === "client") {
|
|
89
|
+
// A Remote Link Child has no relay transport to point at; it names its Home instead (#6957).
|
|
90
|
+
const { readLinkChildContext, linkChildRefusal } = await import("./link-child");
|
|
91
|
+
const link = readLinkChildContext();
|
|
92
|
+
if (link) throw new RuntimeApiError(linkChildRefusal(link), 503, null, "link_child_management_unavailable");
|
|
86
93
|
throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null, "client_role_management_unavailable");
|
|
87
94
|
}
|
|
88
95
|
return `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
@@ -180,7 +187,25 @@ export async function runtimeRequest<T = unknown>(
|
|
|
180
187
|
init: RequestInit = {},
|
|
181
188
|
deps: RuntimeApiDeps = {},
|
|
182
189
|
): Promise<T> {
|
|
183
|
-
|
|
190
|
+
let baseUrl: string;
|
|
191
|
+
try {
|
|
192
|
+
baseUrl = await runtimeBaseUrl(deps);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (!(error instanceof RuntimeApiError) || error.code !== "link_child_management_unavailable") throw error;
|
|
195
|
+
// #6957: on a Remote Link Child an allowlisted read is answered by the Home over the link;
|
|
196
|
+
// every other request is refused with the operation named, before anything is sent.
|
|
197
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
198
|
+
const { readLinkChildContext, linkChildReadable, linkChildRefusal, readLinkHomeJson } = await import("./link-child");
|
|
199
|
+
const link = readLinkChildContext();
|
|
200
|
+
if (!link) throw error;
|
|
201
|
+
const pathname = new URL(path.startsWith("/") ? path : `/${path}`, "http://cli.invalid").pathname;
|
|
202
|
+
if (!linkChildReadable(method, path)) {
|
|
203
|
+
throw new RuntimeApiError(linkChildRefusal(link, `${method} ${pathname}`), 503, null, "link_child_management_unavailable");
|
|
204
|
+
}
|
|
205
|
+
const { status, body } = await readLinkHomeJson(link, path, deps.linkHome);
|
|
206
|
+
if (status < 200 || status >= 300) throw new RuntimeApiError(responseMessage(body, status), status, body);
|
|
207
|
+
return body as T;
|
|
208
|
+
}
|
|
184
209
|
const headers = runningProxyUpdateHeaders();
|
|
185
210
|
for (const [key, value] of new Headers(init.headers).entries()) headers.set(key, value);
|
|
186
211
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
package/src/cli/status.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
5
5
|
import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
|
|
6
6
|
import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
7
7
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
8
|
-
import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
|
|
8
|
+
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
9
9
|
import type { OcxConfig } from "../types";
|
|
10
10
|
import { diagnoseService, serviceLogPath } from "../service";
|
|
11
11
|
import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
|
|
@@ -104,6 +104,16 @@ export type CliRemoteHubStatus = {
|
|
|
104
104
|
truncated: boolean;
|
|
105
105
|
/** The hub's own Claude Code toggle; a client cannot infer it from local config. */
|
|
106
106
|
claudeCodeEnabled: boolean | null;
|
|
107
|
+
/**
|
|
108
|
+
* Present on a Remote Link Child (#6957): the peer is a Home reached over the link, not a hub,
|
|
109
|
+
* and every sentence names it that way. `codexSettings` lists the Home's Codex settings the
|
|
110
|
+
* last sync applied, or why the Child's local values are in effect.
|
|
111
|
+
*/
|
|
112
|
+
link?: {
|
|
113
|
+
linkId: string;
|
|
114
|
+
homePhrase: string;
|
|
115
|
+
codexSettings: { source: "home"; fetchedAt: string; lines: string[] } | { source: "local"; reason: string };
|
|
116
|
+
};
|
|
107
117
|
};
|
|
108
118
|
|
|
109
119
|
export type CliStatusJson = {
|
|
@@ -152,6 +162,7 @@ export type CliStatusJson = {
|
|
|
152
162
|
};
|
|
153
163
|
connection: {
|
|
154
164
|
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
165
|
+
transport?: "hub" | "link";
|
|
155
166
|
reason?: string;
|
|
156
167
|
serverUrl?: string;
|
|
157
168
|
managementUrl?: string;
|
|
@@ -321,8 +332,12 @@ export function selectStatusStartupHealth(
|
|
|
321
332
|
if (evidence?.kind === "desktop" && evidence.runtimePid === livePid) {
|
|
322
333
|
const local = fallback();
|
|
323
334
|
const supervisor = local.desktop?.supervisor;
|
|
324
|
-
// The local probe runs later; it must still describe the same
|
|
325
|
-
|
|
335
|
+
// The local probe runs later; it must still describe the same process pair. macOS and Linux
|
|
336
|
+
// accept only an unowned pair. On Windows an older runtime never published a Desktop
|
|
337
|
+
// diagnostic, so a durable Desktop claim beside proven live parentage qualifies too.
|
|
338
|
+
const ownershipEligible = local.desktop?.owned === false
|
|
339
|
+
|| (local.desktop?.owned === true && local.platform === "win32");
|
|
340
|
+
if (ownershipEligible && supervisor
|
|
326
341
|
&& supervisor.runtimePid === evidence.runtimePid && supervisor.supervisorPid === evidence.supervisorPid) {
|
|
327
342
|
return { startup: local, startupSource: "local-supervision-override" };
|
|
328
343
|
}
|
|
@@ -499,8 +514,8 @@ function boundHubStateToken(
|
|
|
499
514
|
* degradation.
|
|
500
515
|
*/
|
|
501
516
|
export async function collectRemoteHubStatus(
|
|
502
|
-
connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt"
|
|
503
|
-
options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number } = {},
|
|
517
|
+
connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt"> & { transport?: "hub" | "link" },
|
|
518
|
+
options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number; findChild?: () => Promise<LiveProxy | null> } = {},
|
|
504
519
|
): Promise<CliRemoteHubStatus> {
|
|
505
520
|
if (connection.state !== "connected" || !connection.serverUrl || !connection.apiKeyId || !connection.connectedAt) {
|
|
506
521
|
return disconnectedRemoteHubStatus();
|
|
@@ -512,14 +527,50 @@ export async function collectRemoteHubStatus(
|
|
|
512
527
|
connectedAt: connection.connectedAt,
|
|
513
528
|
};
|
|
514
529
|
const bound = boundHubStateToken(readClientConnectionState(), readServiceApiTokenState(), owner);
|
|
530
|
+
// Link credentials never go to the persisted tunnel URL from this CLI. Ask the local Child
|
|
531
|
+
// without a credential; its connection-bound relay authenticates the actual Home connection.
|
|
532
|
+
let fetchImpl = options.fetchImpl;
|
|
533
|
+
let allowNetwork = true;
|
|
534
|
+
let localRelayFetchImpl: typeof fetch | undefined;
|
|
535
|
+
if (connection.transport === "link") {
|
|
536
|
+
const live = await (options.findChild ?? findLiveProxy)();
|
|
537
|
+
const current = readClientConnectionState();
|
|
538
|
+
allowNetwork = live?.role === "client" && current.kind === "connected" && sameClientConnectionOwner(current.value, owner);
|
|
539
|
+
if (allowNetwork && live) {
|
|
540
|
+
const localOrigin = `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
541
|
+
localRelayFetchImpl = ((_, init) => (options.fetchImpl ?? fetch)(localOrigin + "/v1/hub-state", {
|
|
542
|
+
method: "GET", redirect: "manual", signal: init?.signal, headers: { Accept: "application/json" },
|
|
543
|
+
})) as typeof fetch;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// #6957: the Home's name, the link id and the Codex settings the last sync applied.
|
|
547
|
+
const linkState = readClientConnectionState();
|
|
548
|
+
const linkConnection = connection.transport === "link" && linkState.kind === "connected" && linkState.value.transport === "link" && linkState.value.link
|
|
549
|
+
? linkState.value : null;
|
|
550
|
+
const linkHome = linkConnection ? await import("../client/link-home") : null;
|
|
515
551
|
const resolved = await resolveHubState({
|
|
516
552
|
owner,
|
|
517
|
-
token: bound.token,
|
|
553
|
+
token: connection.transport === "link" ? null : bound.token,
|
|
554
|
+
...(localRelayFetchImpl ? { localRelayFetchImpl } : {}),
|
|
518
555
|
...(bound.withheldReason ? { withheldTokenReason: bound.withheldReason } : {}),
|
|
519
|
-
...(
|
|
556
|
+
...(fetchImpl ? { fetchImpl } : {}),
|
|
557
|
+
allowNetwork,
|
|
558
|
+
...(connection.transport === "link" ? { peer: "Home" as const } : {}),
|
|
520
559
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
521
560
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
522
561
|
});
|
|
562
|
+
let link: CliRemoteHubStatus["link"];
|
|
563
|
+
if (linkConnection && linkHome) {
|
|
564
|
+
const { describeHomeCodexSettings } = await import("../link/codex-settings");
|
|
565
|
+
const record = linkHome.readHomeCodexSettingsRecord(linkConnection.link!.linkId);
|
|
566
|
+
link = {
|
|
567
|
+
linkId: linkConnection.link!.linkId,
|
|
568
|
+
homePhrase: linkHome.linkHomePhrase(linkConnection),
|
|
569
|
+
codexSettings: record
|
|
570
|
+
? { source: "home", fetchedAt: record.fetchedAt, lines: describeHomeCodexSettings(record.document.settings) }
|
|
571
|
+
: { source: "local", reason: "no Home settings recorded yet; run: ocx sync" },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
523
574
|
return {
|
|
524
575
|
connected: true,
|
|
525
576
|
// The hub's own advertised origin when it publishes one; otherwise the URL this client
|
|
@@ -535,6 +586,7 @@ export async function collectRemoteHubStatus(
|
|
|
535
586
|
subagentModels: resolved.state?.subagentModels ?? [],
|
|
536
587
|
truncated: resolved.state?.truncated === true,
|
|
537
588
|
claudeCodeEnabled: resolved.state ? resolved.state.claudeCode.enabled : null,
|
|
589
|
+
...(link ? { link } : {}),
|
|
538
590
|
};
|
|
539
591
|
}
|
|
540
592
|
|
|
@@ -546,8 +598,24 @@ export async function collectRemoteHubStatus(
|
|
|
546
598
|
* A buried `Remote hub: connected (<url>)` line — which is all this report had — does not stop
|
|
547
599
|
* that, as #4236 demonstrated.
|
|
548
600
|
*/
|
|
549
|
-
export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | null {
|
|
601
|
+
export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string | null {
|
|
550
602
|
if (!remoteHub.connected) return null;
|
|
603
|
+
if (remoteHub.link) {
|
|
604
|
+
const peer = `${remoteHub.link.homePhrase} over Remote Link (${remoteHub.link.linkId})`;
|
|
605
|
+
if (remoteHub.stateSource === "unavailable") {
|
|
606
|
+
return `⚠️ ${peer}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the Home.`;
|
|
607
|
+
}
|
|
608
|
+
const cached = remoteHub.stateSource === "cache"
|
|
609
|
+
? ` — cached ${remoteHub.ageSeconds ?? "?"}s ago (${remoteHub.reason ?? "live read failed"})`
|
|
610
|
+
: "";
|
|
611
|
+
return `🔗 State from ${peer}${cached}: provider credentials, logins and delegable models below are the Home's, not this machine's.`;
|
|
612
|
+
}
|
|
613
|
+
if (transport === "link") {
|
|
614
|
+
const origin = remoteHub.origin ?? "unknown";
|
|
615
|
+
return remoteHub.stateSource === "unavailable"
|
|
616
|
+
? `🔗 Remote Link Home ${origin}: provider credentials and routing come from Home. State unavailable (${remoteHub.reason ?? "unknown reason"}).`
|
|
617
|
+
: `🔗 State from Remote Link Home ${origin}${remoteHub.stateSource === "cache" ? " (cached)" : ""}: provider credentials, logins and models below come from Home.`;
|
|
618
|
+
}
|
|
551
619
|
const origin = remoteHub.origin ?? "the hub";
|
|
552
620
|
if (remoteHub.stateSource === "unavailable") {
|
|
553
621
|
return `⚠️ Hub ${origin}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the hub.`;
|
|
@@ -562,32 +630,47 @@ export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | nul
|
|
|
562
630
|
* The hub-sourced provider/login/model block, owned here so the sentences are testable without
|
|
563
631
|
* spawning the CLI. Indentation is the caller's. Empty when nothing true is known.
|
|
564
632
|
*/
|
|
565
|
-
export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus): string[] {
|
|
566
|
-
if (!remoteHub.connected
|
|
567
|
-
const
|
|
633
|
+
export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string[] {
|
|
634
|
+
if (!remoteHub.connected) return [];
|
|
635
|
+
const settings = remoteHub.link ? linkCodexSettingsLines(remoteHub.link) : [];
|
|
636
|
+
if (remoteHub.stateSource === "unavailable") return settings;
|
|
637
|
+
const isLink = Boolean(remoteHub.link) || transport === "link";
|
|
638
|
+
const peer = remoteHub.link?.homePhrase ?? `${isLink ? "Home" : "hub"} ${remoteHub.origin ?? (isLink ? "Home" : "hub")}`;
|
|
639
|
+
const subject = isLink ? "the Home" : "the hub";
|
|
568
640
|
const lines = [
|
|
569
|
-
`OAuth logins (
|
|
641
|
+
`OAuth logins (${peer}):`,
|
|
570
642
|
...(remoteHub.oauth.length === 0
|
|
571
|
-
? [
|
|
643
|
+
? [` (${subject} reported no OAuth providers)`]
|
|
572
644
|
: remoteHub.oauth.map(entry => ` ${entry.provider.padEnd(10)} ${entry.loggedIn ? "✓ logged in" : "✗ not logged in"}`)),
|
|
573
|
-
`Providers (
|
|
645
|
+
`Providers (${peer}):`,
|
|
574
646
|
...(remoteHub.providers.length === 0
|
|
575
|
-
? [
|
|
647
|
+
? [` (${subject} reported no providers)`]
|
|
576
648
|
: remoteHub.providers.map(provider => {
|
|
577
649
|
// `authMode` is what keeps "no API key" from reading as "not configured": an `oauth`
|
|
578
650
|
// provider legitimately has no key and is still fully usable.
|
|
579
651
|
const credential = provider.hasCredential ? "credential stored" : `no API key (authMode ${provider.authMode ?? "key"})`;
|
|
580
652
|
return ` ${provider.name.padEnd(10)} ${provider.adapter} — ${credential}${provider.disabled ? ", disabled" : ""}`;
|
|
581
653
|
})),
|
|
582
|
-
`Delegable models (
|
|
654
|
+
`Delegable models (${peer}): ${remoteHub.subagentModels.length === 0 ? "none" : remoteHub.subagentModels.join(", ")}`,
|
|
583
655
|
];
|
|
584
|
-
if (remoteHub.hubVersion) lines.push(
|
|
656
|
+
if (remoteHub.hubVersion) lines.push(`${remoteHub.link ? "Home" : "Hub"} version: ${remoteHub.hubVersion}`);
|
|
585
657
|
// The hub told us its own lists are a prefix. Saying nothing here would make this report claim
|
|
586
658
|
// completeness it does not have — the same shape of confident-and-wrong answer as #4236.
|
|
587
659
|
if (remoteHub.truncated) {
|
|
588
|
-
lines.push(
|
|
660
|
+
lines.push(` (${subject} truncated this state to fit its response caps; some rows are not listed)`);
|
|
589
661
|
}
|
|
590
|
-
return lines;
|
|
662
|
+
return [...lines, ...settings];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Which Codex settings the last sync took from the Home (#6957). */
|
|
666
|
+
export function linkCodexSettingsLines(link: NonNullable<CliRemoteHubStatus["link"]>): string[] {
|
|
667
|
+
if (link.codexSettings.source === "local") {
|
|
668
|
+
return [`Codex settings: this machine's local values (${link.codexSettings.reason})`];
|
|
669
|
+
}
|
|
670
|
+
return [
|
|
671
|
+
`Codex settings from ${link.homePhrase} (applied ${link.codexSettings.fetchedAt}):`,
|
|
672
|
+
...link.codexSettings.lines.map(line => ` ${line}`),
|
|
673
|
+
];
|
|
591
674
|
}
|
|
592
675
|
|
|
593
676
|
export function selectListenTarget(
|
|
@@ -760,7 +843,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
760
843
|
// Asked before the local probes below so a connected client's report is hub-sourced from its
|
|
761
844
|
// first line. Bounded and failure-tolerant: an offline hub degrades the remoteHub block, it
|
|
762
845
|
// does not fail `ocx status`.
|
|
763
|
-
const remoteHub = await collectRemoteHubStatus(clientConnection);
|
|
846
|
+
const remoteHub = await collectRemoteHubStatus({ ...clientConnection, transport: config.client?.transport });
|
|
764
847
|
// Prefer identity-verified liveness (runtime-port + /healthz) over ocx.pid alone (#618).
|
|
765
848
|
// Pass the already-resolved diagnostics config so findLiveProxy does not re-load and
|
|
766
849
|
// warn on malformed config.json (status --json must stay stderr-clean).
|
|
@@ -960,6 +1043,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
960
1043
|
},
|
|
961
1044
|
connection: {
|
|
962
1045
|
state: clientConnection.state,
|
|
1046
|
+
...(config.client?.transport ? { transport: config.client.transport } : {}),
|
|
963
1047
|
...(clientConnection.reason ? { reason: clientConnection.reason } : {}),
|
|
964
1048
|
...(clientConnection.serverUrl ? { serverUrl: clientConnection.serverUrl } : {}),
|
|
965
1049
|
...(clientConnection.managementUrl ? { managementUrl: clientConnection.managementUrl } : {}),
|