@bitkyc08/opencodex 2.82.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-DTBC8n7-.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/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- 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 +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -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 +44 -12
- 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/resolve.ts +1 -1
- 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/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- 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/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- 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 +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- 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 +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- 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/errors.ts +25 -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/redact.ts +2 -2
- 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 +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- 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-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- 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 +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- 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/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- 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 +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- 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 +333 -57
- 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 +92 -390
- 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 +7 -5
- 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 +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- 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 +129 -50
- 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-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- 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/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
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/resolve.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ocx resolve` — the machine-readable runtime resolution surface for an embedding shell.
|
|
3
3
|
*
|
|
4
|
-
* D5 of devlog/
|
|
4
|
+
* D5 of devlog/_fin/260921_app_runtime_ownership/: the desktop shell must stop resolving
|
|
5
5
|
* the config home, the port and liveness itself. The tuned probe budgets in
|
|
6
6
|
* src/server/proxy-liveness.ts exist because a shell-side reimplementation answered
|
|
7
7
|
* "nobody listening" twice and started duplicate proxies; this verb exposes that module's
|
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 } : {}),
|
package/src/client/connect.ts
CHANGED
|
@@ -74,6 +74,16 @@ import {
|
|
|
74
74
|
assertNoClientDisconnectPending, assertClientConnectionUnchanged, sameClientConnectionOwner,
|
|
75
75
|
} from "./state";
|
|
76
76
|
import { assertClientCatalogCompatible, type CatalogCompatibilityDeps } from "./catalog-compatibility";
|
|
77
|
+
import { overlayHomeCodexSettings } from "../link/codex-settings";
|
|
78
|
+
import {
|
|
79
|
+
clearHomeCodexSettingsRecord,
|
|
80
|
+
fetchHomeCodexSettings,
|
|
81
|
+
resolveHomeCodexSettings,
|
|
82
|
+
recordHomeCodexSettings,
|
|
83
|
+
writeHomeCodexSettingsRecord,
|
|
84
|
+
type HomeCodexSettingsSource,
|
|
85
|
+
type LinkHomeDeps,
|
|
86
|
+
} from "./link-home";
|
|
77
87
|
import { hubStateCachePath } from "./hub-state";
|
|
78
88
|
import { ClientLinkStateError, clearClientLinkState, readClientLinkState } from "./link-state";
|
|
79
89
|
import { isLinkPort } from "../link/ports";
|
|
@@ -110,6 +120,8 @@ export interface ClientConnectDeps {
|
|
|
110
120
|
now?: () => Date;
|
|
111
121
|
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
112
122
|
catalogCompatibility?: CatalogCompatibilityDeps;
|
|
123
|
+
/** Link mode: how the Home's Codex settings are read (#6957). */
|
|
124
|
+
linkHome?: LinkHomeDeps;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
export interface RotateClientOptions {
|
|
@@ -687,7 +699,16 @@ export async function connectClient(
|
|
|
687
699
|
|
|
688
700
|
const config = earlyConfig ?? loadConfig();
|
|
689
701
|
const target = routingTarget(serverUrl, linkMode ? config.port : undefined, config);
|
|
690
|
-
|
|
702
|
+
// A link Child writes the Home's OpenCodex-owned Codex settings, not its own (#6957). A read
|
|
703
|
+
// that fails here keeps the local values; the next `ocx sync` applies the Home's.
|
|
704
|
+
// Read only: the record is written after the connection commits, so a rollback leaves none.
|
|
705
|
+
const homeSettings = linkMode && linkMetadata && options.selectedClients.includes("codex")
|
|
706
|
+
? await fetchHomeCodexSettings({ transport: "link", link: linkMetadata, apiKeyId }, admissionToken, deps.linkHome)
|
|
707
|
+
: undefined;
|
|
708
|
+
const injectConfig = {
|
|
709
|
+
...(homeSettings?.kind === "fresh" ? overlayHomeCodexSettings(config, homeSettings.document.settings) : config),
|
|
710
|
+
syncResumeHistory: false,
|
|
711
|
+
};
|
|
691
712
|
const preflight = await injectCodexConfig(config.port, injectConfig, {
|
|
692
713
|
validateOnly: true,
|
|
693
714
|
routingTarget: target,
|
|
@@ -734,6 +755,12 @@ export async function connectClient(
|
|
|
734
755
|
commitClientConnection(connection);
|
|
735
756
|
committed = true;
|
|
736
757
|
}), deps.lifecycleLockDeps);
|
|
758
|
+
// Recorded only when the injection that applied it ran (not with --no-sync).
|
|
759
|
+
if (homeSettings?.kind === "fresh" && linkMetadata && injectionCommitted) {
|
|
760
|
+
try {
|
|
761
|
+
writeHomeCodexSettingsRecord({ linkId: linkMetadata.linkId, fetchedAt: now, document: homeSettings.document });
|
|
762
|
+
} catch { /* the next sync records it; the applied config is already committed */ }
|
|
763
|
+
}
|
|
737
764
|
return connection;
|
|
738
765
|
} catch (error) {
|
|
739
766
|
const rollbackFailures: string[] = [];
|
|
@@ -780,7 +807,7 @@ export async function connectClient(
|
|
|
780
807
|
export async function syncConnectedClient(
|
|
781
808
|
_options: { restartCodex?: boolean } = {},
|
|
782
809
|
deps: ClientConnectDeps = {},
|
|
783
|
-
): Promise<{ catalogWritten: boolean; cacheSynced: boolean; injected: boolean; stale: boolean }> {
|
|
810
|
+
): Promise<{ catalogWritten: boolean; cacheSynced: boolean; injected: boolean; stale: boolean; homeSettings?: HomeCodexSettingsSource }> {
|
|
784
811
|
if (siblingOfLivePort() !== null) throw new Error(siblingSkipMessage());
|
|
785
812
|
const initial = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
786
813
|
assertNoClientDisconnectPending();
|
|
@@ -825,21 +852,28 @@ export async function syncConnectedClient(
|
|
|
825
852
|
if (next.pendingOperation || token.kind !== "present" || token.fingerprint !== next.tokenFingerprint) throw new Error("client_token_changed");
|
|
826
853
|
};
|
|
827
854
|
let injected = false;
|
|
855
|
+
let homeSettings: HomeCodexSettingsSource | undefined;
|
|
828
856
|
if (next.selectedClients.includes("codex")) {
|
|
829
|
-
|
|
857
|
+
let config = loadConfig();
|
|
858
|
+
// #6957: a link Child applies the Home's OpenCodex-owned Codex settings with the catalog.
|
|
859
|
+
if (isLinkConnection(next)) {
|
|
860
|
+
homeSettings = await resolveHomeCodexSettings(next, initial.token.token, deps.linkHome);
|
|
861
|
+
if (homeSettings.source !== "local") config = overlayHomeCodexSettings(config, homeSettings.document.settings);
|
|
862
|
+
}
|
|
830
863
|
const result = await injectCodexConfig(config.port, { ...config, syncResumeHistory: false }, {
|
|
831
864
|
routingTarget: routingTarget(next.serverUrl, isLinkConnection(next) ? config.port : undefined, config), catalogPath: DEFAULT_CATALOG_PATH,
|
|
832
865
|
journalOwner: { kind: "client", apiKeyId: next.apiKeyId }, beforeClientWrite,
|
|
833
866
|
});
|
|
834
867
|
if (!result.success || result.status === "skipped") throw new Error(result.message);
|
|
835
868
|
injected = true;
|
|
869
|
+
if (homeSettings && next.link) recordHomeCodexSettings(next.link.linkId, homeSettings);
|
|
836
870
|
}
|
|
837
871
|
const cacheSynced = withClientLifecycleSync(() => {
|
|
838
872
|
beforeClientWrite();
|
|
839
873
|
// Cache invalidation acquires K itself (N -> K -> C); never call it while C is held.
|
|
840
874
|
return invalidateCodexModelsCache({ allowWhenDesiredDisabled: true });
|
|
841
875
|
}, deps.lifecycleLockDeps);
|
|
842
|
-
return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale };
|
|
876
|
+
return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale, ...(homeSettings ? { homeSettings } : {}) };
|
|
843
877
|
}
|
|
844
878
|
|
|
845
879
|
/**
|
|
@@ -1035,6 +1069,7 @@ export async function disconnectClient(
|
|
|
1035
1069
|
if (!(error instanceof ClientLinkStateError)) throw error;
|
|
1036
1070
|
}
|
|
1037
1071
|
if (sidecarLinkId === connection.link.linkId) clearClientLinkState(connection.link.linkId);
|
|
1072
|
+
try { clearHomeCodexSettingsRecord(); } catch { /* derived data; never fails a disconnect */ }
|
|
1038
1073
|
}
|
|
1039
1074
|
removeHubStateCache();
|
|
1040
1075
|
requireDesktopResult(finishRemoteDesktopCleanup(held, receipt.owner));
|