@bitkyc08/opencodex 2.89.0 → 2.91.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-DMps4OLk.css +1 -0
- package/gui/dist/assets/App-nZ34cR5G.js +58 -0
- package/gui/dist/assets/{Tray-7z1COnmz.js → Tray-BuLy7hL1.js} +1 -1
- package/gui/dist/assets/{index-uchD-ngf.js → index-DGXS51sp.js} +15 -15
- package/gui/dist/assets/quota-summary-DO1Sx5TM.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +28 -2
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google.ts +28 -9
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +7 -2
- package/src/adapters/openai-responses/request-strips.ts +32 -0
- package/src/adapters/openai-responses/transport.ts +19 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +36 -4
- package/src/chat/reasoning-details.ts +137 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +50 -0
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +84 -9
- package/src/cli/provider.ts +44 -2
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-relay-transport.ts +8 -1
- package/src/client/link-relay.ts +27 -3
- package/src/client/machine-listener.ts +83 -1
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +137 -25
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-store.ts +27 -8
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +10 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +58 -12
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/combos/failover.ts +8 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -2
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/generated/compatibility-version.json +360 -192
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +2 -2
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +31 -17
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +24 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +2 -1
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/claude-cli-usage.ts +20 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +10 -2
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +240 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota/anthropic-account-quota.ts +18 -6
- package/src/providers/quota/antigravity.ts +8 -10
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/vendor-probes-oauth.ts +12 -3
- package/src/providers/quota-routing-cache.ts +1 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry/entries-extended.ts +6 -1
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +11 -2
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +6 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +8 -1
- package/src/server/chat-native.ts +8 -7
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +11 -2
- package/src/server/index/optional-listeners.ts +7 -2
- package/src/server/index/serve-options.ts +27 -4
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/live.ts +5 -0
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +12 -1
- package/src/server/management/oauth-account-routes.ts +2 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/messages-native.ts +11 -0
- package/src/server/request-log.ts +16 -7
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +2 -2
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-stream-preflight.ts +7 -2
- package/src/server/responses/compact.ts +4 -2
- package/src/server/responses/core-auth.ts +31 -1
- package/src/server/responses/core-codex-account.ts +21 -14
- package/src/server/responses/core-combo.ts +75 -1
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +8 -0
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +30 -10
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/main-hard-lock-routing.ts +85 -8
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +2 -0
- package/src/server/responses/passthrough-dispatch.ts +13 -13
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-prepare.ts +19 -1
- package/src/server/responses/request-transport.ts +61 -3
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/diagnostics.ts +20 -6
- package/src/service/orchestration.ts +17 -0
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +7 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +16 -1
- package/src/types.ts +1 -0
- package/src/web-search/loop.ts +2 -2
- package/gui/dist/assets/App-BnRSujOq.js +0 -58
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/quota-summary-BY_10IEz.js +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx link cleanup`: find Home-initiated Remote Link Children that run Windows and explain how to
|
|
3
|
+
* move each one to Hub mode. It never revokes or changes anything.
|
|
4
|
+
*
|
|
5
|
+
* Windows Children are refused at enrollment, but a link enrolled before that refusal existed keeps
|
|
6
|
+
* its record, key and reverse tunnel. Its record carries no platform, so this command asks the
|
|
7
|
+
* Child over the same pinned SSH trust the tunnel uses (`uname -s` through `sh`) and classifies the
|
|
8
|
+
* answer with `detectRemotePlatform` (a localized cmd or PowerShell refusal gets the fixed %OS% probe).
|
|
9
|
+
*
|
|
10
|
+
* The recovery order matters. An ordinary `ocx link revoke` runs `ocx disconnect` on the Child
|
|
11
|
+
* through the POSIX ocx prelude, which exits 125 on Windows, so for these Children the revoke must
|
|
12
|
+
* be forced after the Child disconnected itself.
|
|
13
|
+
*/
|
|
14
|
+
import { linkKnownHostsPath, linkStorePath } from "../link/paths";
|
|
15
|
+
import { detectRemotePlatform, REMOTE_UNAME_ARGV, type RemotePlatform } from "../link/remote-platform";
|
|
16
|
+
import { buildExecArgv } from "../link/ssh-argv";
|
|
17
|
+
import { createSshRunner, sshFailureHint, sshRunnerErrorHint, type SshRunner } from "../link/ssh-runner";
|
|
18
|
+
import { readLinkStore, type LinkStore } from "../link/store";
|
|
19
|
+
import { CliUsageError, printData, rejectArgs, takeJsonFlag, takeOption } from "./runtime-api";
|
|
20
|
+
|
|
21
|
+
const LINK_ID = /^lnk_[0-9a-f]{16}$/;
|
|
22
|
+
const CHECK_TIMEOUT_MS = 30_000;
|
|
23
|
+
|
|
24
|
+
export interface LinkCleanupDeps {
|
|
25
|
+
readStore?: () => LinkStore;
|
|
26
|
+
sshRunner?: SshRunner;
|
|
27
|
+
knownHostsFile?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LinkCleanupEntry {
|
|
31
|
+
linkId: string;
|
|
32
|
+
alias: string;
|
|
33
|
+
direction: LinkStore["links"][number]["direction"];
|
|
34
|
+
platform: RemotePlatform | "not_checked";
|
|
35
|
+
reason: string | null;
|
|
36
|
+
recovery: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function windowsChildRecovery(linkId: string, alias: string): string[] {
|
|
40
|
+
return [
|
|
41
|
+
`On ${alias}, run 'ocx disconnect' and then 'ocx restart' so it stops using this link.`,
|
|
42
|
+
`On this Home, run 'ocx link revoke --link-id ${linkId} --force --yes'. A plain revoke cannot disconnect a Windows Child remotely.`,
|
|
43
|
+
`Then connect ${alias} in Hub mode: run 'ocx hub invite' on a Hub and the printed 'ocx connect' command on ${alias}.`,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function checkPlatform(runner: SshRunner, alias: string, knownHostsFile: string): Promise<{ platform: RemotePlatform; reason: string | null }> {
|
|
48
|
+
try {
|
|
49
|
+
const result = await runner.run(buildExecArgv({ alias, argv: REMOTE_UNAME_ARGV, knownHostsFile }), { timeoutMs: CHECK_TIMEOUT_MS });
|
|
50
|
+
const platform = await detectRemotePlatform(runner, alias, knownHostsFile, result);
|
|
51
|
+
return { platform, reason: platform === "unknown" ? (sshFailureHint(result.stderr) ?? `ssh exited ${result.code}`) : null };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return { platform: "unknown", reason: sshRunnerErrorHint(error) ?? "ssh could not run" };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function humanLines(entries: LinkCleanupEntry[]): string[] {
|
|
58
|
+
if (entries.length === 0) return ["No links are recorded on this Home.", "Nothing was revoked or changed."];
|
|
59
|
+
const lines: string[] = [];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const label = `${entry.linkId} (${entry.alias})`;
|
|
62
|
+
if (entry.platform === "windows") {
|
|
63
|
+
lines.push(`${label}: Windows Child. Remote Link does not support Windows Children; this link predates that refusal.`);
|
|
64
|
+
for (const [index, step] of entry.recovery.entries()) lines.push(` ${index + 1}. ${step}`);
|
|
65
|
+
} else if (entry.platform === "posix") lines.push(`${label}: macOS or Linux Child; nothing to clean up.`);
|
|
66
|
+
else if (entry.platform === "not_checked") lines.push(`${label}: Child-initiated; this Home has no SSH route to check it.`);
|
|
67
|
+
else lines.push(`${label}: platform could not be checked (${entry.reason ?? "unknown"}); retry when it is reachable.`);
|
|
68
|
+
}
|
|
69
|
+
lines.push("Nothing was revoked or changed.");
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runLinkCleanup(args: string[], deps: LinkCleanupDeps, usage: string): Promise<void> {
|
|
74
|
+
const wantsJson = takeJsonFlag(args);
|
|
75
|
+
const linkId = takeOption(args, "--link-id");
|
|
76
|
+
if (linkId !== undefined && !LINK_ID.test(linkId)) throw new CliUsageError("cleanup --link-id must be a valid link id", usage);
|
|
77
|
+
rejectArgs(args, usage);
|
|
78
|
+
const store = (deps.readStore ?? (() => readLinkStore(linkStorePath())))();
|
|
79
|
+
const records = linkId === undefined ? store.links : store.links.filter(link => link.id === linkId);
|
|
80
|
+
if (linkId !== undefined && records.length === 0) throw new CliUsageError(`No link ${linkId} is recorded on this Home.`, usage);
|
|
81
|
+
const runner = deps.sshRunner ?? createSshRunner();
|
|
82
|
+
const knownHostsFile = deps.knownHostsFile ?? linkKnownHostsPath();
|
|
83
|
+
const entries: LinkCleanupEntry[] = [];
|
|
84
|
+
// One at a time: each check is a bounded SSH command, and order keeps the report stable.
|
|
85
|
+
for (const record of records) {
|
|
86
|
+
const checked = record.direction === "hub-initiated"
|
|
87
|
+
? await checkPlatform(runner, record.alias, knownHostsFile)
|
|
88
|
+
: { platform: "not_checked" as const, reason: null };
|
|
89
|
+
entries.push({
|
|
90
|
+
linkId: record.id, alias: record.alias, direction: record.direction, ...checked,
|
|
91
|
+
recovery: checked.platform === "windows" ? windowsChildRecovery(record.id, record.alias) : [],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
printData({ links: entries, changed: false }, wantsJson, humanLines(entries));
|
|
95
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { assertSshAlias } from "../link/ssh-argv";
|
|
3
|
+
import { windowsChildRefusal } from "../link/remote-platform";
|
|
3
4
|
import { CliUsageError, RuntimeApiError, runtimeBaseUrl, rejectArgs, takeJsonFlag, takeOption, printData } from "./runtime-api";
|
|
4
5
|
import type { LinkCliDeps } from "./link";
|
|
5
6
|
|
|
@@ -83,6 +84,9 @@ export async function runLinkEnrollment(command: "probe" | "add", args: string[]
|
|
|
83
84
|
catch (error) {
|
|
84
85
|
if (error instanceof RuntimeApiError && record(error.body)) {
|
|
85
86
|
const code = record(error.body.error) ? error.body.error.code : undefined;
|
|
87
|
+
// Refused at probe or confirm, before any key or tunnel: point at Hub mode, not at a phase.
|
|
88
|
+
// A residual response comes after key issuance; it keeps its Child recovery below.
|
|
89
|
+
if (code === "remote_platform_unsupported" && error.body.residual !== true) throw new Error(windowsChildRefusal(alias));
|
|
86
90
|
const phase = typeof code === "string" && Object.hasOwn(PHASES, code) ? PHASES[code] : undefined;
|
|
87
91
|
const message = phase ? "Remote Link failed during " + phase : error.message;
|
|
88
92
|
if (phase || error.body.residual === true) throw new Error(message + (error.body.residual === true ? ". " + RECOVERY : ""));
|
package/src/cli/link.ts
CHANGED
|
@@ -2,7 +2,11 @@ import { clientLinkStatusIdentity } from "../client/link-status";
|
|
|
2
2
|
import { clientLinkTunnelStatus } from "../client/link-tunnel";
|
|
3
3
|
import { loadConfig } from "../config";
|
|
4
4
|
import { projectLinkStatus, type ClientLinkIdentity, type LinkStatusConfig, type LinkStatusDto } from "../link/status-projection";
|
|
5
|
+
import { isEpochMs, type LinkChildParity } from "../link/child-parity";
|
|
6
|
+
import { CODEX_SETTINGS_DIGEST_PATTERN } from "../link/codex-settings";
|
|
7
|
+
import type { LinkLocalPeer, LinkPeerStatus } from "../link/compatibility";
|
|
5
8
|
import { runLinkEnrollment } from "./link-enrollment";
|
|
9
|
+
import { runLinkCleanup, type LinkCleanupDeps } from "./link-cleanup";
|
|
6
10
|
import { assertSshAlias } from "../link/ssh-argv";
|
|
7
11
|
import { linkStorePath } from "../link/paths";
|
|
8
12
|
import { isLinkPort } from "../link/ports";
|
|
@@ -31,8 +35,10 @@ export const LINK_USAGE = `Usage:
|
|
|
31
35
|
ocx link status [--json]
|
|
32
36
|
ocx link revoke --link-id <id> [--force --yes] [--json]
|
|
33
37
|
ocx link reconnect --link-id <id> [--json]
|
|
38
|
+
ocx link cleanup [--link-id <id>] [--json]
|
|
34
39
|
|
|
35
|
-
Forced revoke skips remote cleanup. Run ocx disconnect on the remote client
|
|
40
|
+
Forced revoke skips remote cleanup. Run ocx disconnect on the remote client.
|
|
41
|
+
Cleanup finds Windows Children and explains how to move them to Hub mode; it changes nothing.`;
|
|
36
42
|
|
|
37
43
|
type LinkState = "connecting" | "connected" | "reconnecting" | "failed" | "idle";
|
|
38
44
|
type ListenerState = "off" | "listening" | "failed";
|
|
@@ -40,7 +46,7 @@ type ListenerState = "off" | "listening" | "failed";
|
|
|
40
46
|
export interface LinkStatusPayload {
|
|
41
47
|
role: "standalone" | "home" | "child";
|
|
42
48
|
listener: { state: ListenerState; port: number | null };
|
|
43
|
-
links: Array<{
|
|
49
|
+
links: Array<LinkChildParity & {
|
|
44
50
|
id: string;
|
|
45
51
|
alias: string;
|
|
46
52
|
direction: LinkDirection;
|
|
@@ -48,6 +54,7 @@ export interface LinkStatusPayload {
|
|
|
48
54
|
since: string;
|
|
49
55
|
reason: string | null;
|
|
50
56
|
tunnelPort: number;
|
|
57
|
+
peer?: LinkPeerStatus;
|
|
51
58
|
}>;
|
|
52
59
|
child: null | {
|
|
53
60
|
alias: string;
|
|
@@ -55,6 +62,7 @@ export interface LinkStatusPayload {
|
|
|
55
62
|
since: string;
|
|
56
63
|
reason: string | null;
|
|
57
64
|
};
|
|
65
|
+
local?: LinkLocalPeer;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
export interface LinkIssuePayload {
|
|
@@ -64,7 +72,7 @@ export interface LinkIssuePayload {
|
|
|
64
72
|
listenerPort: number;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
export interface LinkCliDeps extends RuntimeApiDeps {
|
|
75
|
+
export interface LinkCliDeps extends RuntimeApiDeps, Omit<LinkCleanupDeps, "readStore"> {
|
|
68
76
|
/** Test seam can shorten, never extend, the whole enrollment deadline. */
|
|
69
77
|
enrollmentTimeoutMs?: number;
|
|
70
78
|
choosePort?: () => Promise<number>;
|
|
@@ -85,9 +93,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
function assertExactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
|
|
88
|
-
|
|
96
|
+
assertKeys(value, keys, [], label);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Exact keys plus fields a newer proxy adds; an older proxy omits them. */
|
|
100
|
+
function assertKeys(value: Record<string, unknown>, keys: readonly string[], optional: readonly string[], label: string): void {
|
|
101
|
+
const allowed = new Set([...keys, ...optional]);
|
|
89
102
|
const actual = Object.keys(value);
|
|
90
|
-
if (
|
|
103
|
+
if (keys.some(key => !(key in value)) || actual.some(key => !allowed.has(key))) {
|
|
91
104
|
throw new Error(`invalid link API response: ${label} fields`);
|
|
92
105
|
}
|
|
93
106
|
}
|
|
@@ -104,9 +117,42 @@ function validNullableString(value: unknown): value is string | null {
|
|
|
104
117
|
return value === null || typeof value === "string";
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
function validStringList(value: unknown): value is string[] {
|
|
121
|
+
return Array.isArray(value) && value.every(validString);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateLocal(value: unknown): LinkLocalPeer {
|
|
125
|
+
if (!isRecord(value)) throw new Error("invalid link API response: local");
|
|
126
|
+
assertExactKeys(value, ["version", "relayAuth", "features"], "local");
|
|
127
|
+
if (!validString(value.version) || !validString(value.relayAuth) || !validStringList(value.features)) {
|
|
128
|
+
throw new Error("invalid link API response: local fields");
|
|
129
|
+
}
|
|
130
|
+
return { version: value.version, relayAuth: value.relayAuth, features: [...value.features] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validatePeer(value: unknown, index: number): LinkPeerStatus {
|
|
134
|
+
const fail = (): never => { throw new Error(`invalid link API response: link ${index} peer`); };
|
|
135
|
+
if (!isRecord(value)) return fail();
|
|
136
|
+
assertExactKeys(value, ["observedAt", "version", "versionRange", "relayAuth", "features", "compatibility", "reasons"], `link ${index} peer`);
|
|
137
|
+
const range = value.versionRange;
|
|
138
|
+
if (!validNullableString(value.observedAt) || !validNullableString(value.version) || !validNullableString(value.relayAuth)
|
|
139
|
+
|| (value.features !== null && !validStringList(value.features)) || !validStringList(value.reasons)
|
|
140
|
+
|| !["compatible", "degraded", "incompatible", "unknown"].includes(value.compatibility as string)
|
|
141
|
+
|| (range !== null && (!isRecord(range) || !validNullableString(range.min) || !validString(range.below)))) fail();
|
|
142
|
+
return {
|
|
143
|
+
observedAt: value.observedAt as string | null,
|
|
144
|
+
version: value.version as string | null,
|
|
145
|
+
versionRange: range === null ? null : { min: (range as Record<string, unknown>).min as string | null, below: (range as Record<string, unknown>).below as string },
|
|
146
|
+
relayAuth: value.relayAuth as string | null,
|
|
147
|
+
features: value.features === null ? null : [...value.features as string[]],
|
|
148
|
+
compatibility: value.compatibility as LinkPeerStatus["compatibility"],
|
|
149
|
+
reasons: [...value.reasons as string[]],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
107
153
|
function validateStatus(value: unknown): LinkStatusPayload {
|
|
108
154
|
if (!isRecord(value)) throw new Error("invalid link API response: status object");
|
|
109
|
-
|
|
155
|
+
assertKeys(value, ["role", "listener", "links", "child"], ["local"], "status");
|
|
110
156
|
if (value.role !== "standalone" && value.role !== "home" && value.role !== "child") {
|
|
111
157
|
throw new Error("invalid link API response: status role");
|
|
112
158
|
}
|
|
@@ -121,7 +167,10 @@ function validateStatus(value: unknown): LinkStatusPayload {
|
|
|
121
167
|
if (!Array.isArray(value.links)) throw new Error("invalid link API response: links");
|
|
122
168
|
const links = value.links.map((candidate, index) => {
|
|
123
169
|
if (!isRecord(candidate)) throw new Error(`invalid link API response: link ${index}`);
|
|
124
|
-
|
|
170
|
+
// The OCX-6 parity fields are optional on the wire (an older runtime omits them) and always
|
|
171
|
+
// present in this output, null when absent.
|
|
172
|
+
assertKeys(candidate, ["id", "alias", "direction", "state", "since", "reason", "tunnelPort"], [...PARITY_KEYS, "peer"], `link ${index}`);
|
|
173
|
+
const parity = validateParity(candidate, index);
|
|
125
174
|
if (typeof candidate.id !== "string" || !LINK_ID.test(candidate.id)
|
|
126
175
|
|| !validString(candidate.alias) || (candidate.direction !== "hub-initiated" && candidate.direction !== "client-initiated")
|
|
127
176
|
|| (candidate.state !== "connecting" && candidate.state !== "connected" && candidate.state !== "reconnecting" && candidate.state !== "failed" && candidate.state !== "idle")
|
|
@@ -136,6 +185,8 @@ function validateStatus(value: unknown): LinkStatusPayload {
|
|
|
136
185
|
since: candidate.since,
|
|
137
186
|
reason: candidate.reason,
|
|
138
187
|
tunnelPort: candidate.tunnelPort,
|
|
188
|
+
...parity,
|
|
189
|
+
...(candidate.peer !== undefined ? { peer: validatePeer(candidate.peer, index) } : {}),
|
|
139
190
|
} as LinkStatusPayload["links"][number];
|
|
140
191
|
});
|
|
141
192
|
let child: LinkStatusPayload["child"] = null;
|
|
@@ -159,6 +210,7 @@ function validateStatus(value: unknown): LinkStatusPayload {
|
|
|
159
210
|
listener: { state: value.listener.state, port: value.listener.port },
|
|
160
211
|
links,
|
|
161
212
|
child,
|
|
213
|
+
...(value.local !== undefined ? { local: validateLocal(value.local) } : {}),
|
|
162
214
|
};
|
|
163
215
|
}
|
|
164
216
|
|
|
@@ -258,7 +310,8 @@ async function runStatus(args: string[], deps: LinkCliDeps): Promise<void> {
|
|
|
258
310
|
}
|
|
259
311
|
throw error;
|
|
260
312
|
}
|
|
261
|
-
|
|
313
|
+
// A proxy older than the compatibility report ignores the query and answers the exact K16 shape.
|
|
314
|
+
const result = validateStatus(await linkRequest<unknown>("/api/link/status?compatibility=1", {}, { ...deps, baseUrl }));
|
|
262
315
|
console.log(JSON.stringify(result));
|
|
263
316
|
}
|
|
264
317
|
|
|
@@ -326,6 +379,28 @@ export async function runLinkCommand(rawArgs: string[], deps: LinkCliDeps = {}):
|
|
|
326
379
|
else if (command === "status") await runStatus(args, deps);
|
|
327
380
|
else if (command === "revoke") await runRevoke(args, deps);
|
|
328
381
|
else if (command === "reconnect") await runReconnect(args, deps);
|
|
329
|
-
else
|
|
382
|
+
else if (command === "cleanup") await runLinkCleanup(args, deps, LINK_USAGE);
|
|
383
|
+
else throw new CliUsageError("link requires one of: probe, add, port, issue, status, revoke, reconnect, cleanup", LINK_USAGE);
|
|
330
384
|
});
|
|
331
385
|
}
|
|
386
|
+
|
|
387
|
+
const PARITY_KEYS = ["lastApplyAt", "runtimeStartedAt", "appliedCodexSettingsDigest", "reportedAt"] as const;
|
|
388
|
+
|
|
389
|
+
function validateParity(candidate: Record<string, unknown>, index: number): LinkChildParity {
|
|
390
|
+
const time = (key: "lastApplyAt" | "runtimeStartedAt" | "reportedAt"): number | null => {
|
|
391
|
+
const value = candidate[key];
|
|
392
|
+
if (value === undefined || value === null) return null;
|
|
393
|
+
if (!isEpochMs(value)) throw new Error(`invalid link API response: link ${index} ${key}`);
|
|
394
|
+
return value;
|
|
395
|
+
};
|
|
396
|
+
const digest = candidate.appliedCodexSettingsDigest;
|
|
397
|
+
if (digest !== undefined && digest !== null && (typeof digest !== "string" || !CODEX_SETTINGS_DIGEST_PATTERN.test(digest))) {
|
|
398
|
+
throw new Error(`invalid link API response: link ${index} appliedCodexSettingsDigest`);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
lastApplyAt: time("lastApplyAt"),
|
|
402
|
+
runtimeStartedAt: time("runtimeStartedAt"),
|
|
403
|
+
appliedCodexSettingsDigest: typeof digest === "string" ? digest : null,
|
|
404
|
+
reportedAt: time("reportedAt"),
|
|
405
|
+
};
|
|
406
|
+
}
|
package/src/cli/provider.ts
CHANGED
|
@@ -28,6 +28,8 @@ import type { RuntimeApiDeps } from "./runtime-api";
|
|
|
28
28
|
import { redactSecretArgs } from "./secret-args";
|
|
29
29
|
import { projectLocalSyncResult, type LocalSyncResult } from "./local-sync-result";
|
|
30
30
|
import { providerManagementConfigError } from "../server/auth-cors";
|
|
31
|
+
import { readClientConnectionState, type ClientConnectionState } from "../client/state";
|
|
32
|
+
import { SENSITIVE_KEY_PATTERN } from "../lib/redact";
|
|
31
33
|
|
|
32
34
|
export interface ProviderCommandDeps extends RuntimeApiDeps {
|
|
33
35
|
syncModels?: typeof syncModelsToCodex;
|
|
@@ -102,7 +104,46 @@ function validateAndSave(config: ReturnType<typeof loadConfig>): void {
|
|
|
102
104
|
// provider list
|
|
103
105
|
// ---------------------------------------------------------------------------
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Does this device's own provider config serve requests? A device linked to a Home — a Remote
|
|
109
|
+
* Link Child or a Hub-mode client — routes through the Home, so its local rows are shadowed.
|
|
110
|
+
* A standalone device and a Hub serve their own rows. An unreadable or inconsistent client
|
|
111
|
+
* state is not a link, the same reading `readLinkChildContext` takes.
|
|
112
|
+
*/
|
|
113
|
+
export function localProvidersActive(read: () => ClientConnectionState = readClientConnectionState): boolean {
|
|
114
|
+
try {
|
|
115
|
+
return read().kind !== "connected";
|
|
116
|
+
} catch {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const SENSITIVE_QUERY_KEY = /key|token|secret|password|passwd|signature|^sig$|auth|credential/i;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The baseUrl as `provider list` prints it. Validation already rejects embedded credentials;
|
|
125
|
+
* this also covers a hand-edited config, dropping userinfo and masking credential-shaped query
|
|
126
|
+
* values while keeping ordinary parameters such as `api-version`.
|
|
127
|
+
*/
|
|
128
|
+
export function providerListBaseUrl(baseUrl: string): string {
|
|
129
|
+
let parsed: URL;
|
|
130
|
+
try { parsed = new URL(baseUrl); } catch { return baseUrl; }
|
|
131
|
+
let changed = false;
|
|
132
|
+
if (parsed.username || parsed.password) {
|
|
133
|
+
parsed.username = "";
|
|
134
|
+
parsed.password = "";
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
for (const key of [...new Set(parsed.searchParams.keys())]) {
|
|
138
|
+
if (SENSITIVE_KEY_PATTERN.test(key) || SENSITIVE_QUERY_KEY.test(key)) {
|
|
139
|
+
parsed.searchParams.set(key, "redacted");
|
|
140
|
+
changed = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return changed ? parsed.toString() : baseUrl;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleList(args: string[], active: boolean = localProvidersActive()): void {
|
|
106
147
|
const wantsJson = consumeFlag(args, "--json");
|
|
107
148
|
const wantsJsonl = consumeFlag(args, "--jsonl");
|
|
108
149
|
rejectUnknownArgs(args, "Usage: ocx provider list [--json|--jsonl]");
|
|
@@ -120,12 +161,13 @@ function handleList(args: string[]): void {
|
|
|
120
161
|
return {
|
|
121
162
|
name,
|
|
122
163
|
adapter: prov.adapter,
|
|
123
|
-
baseUrl: prov.baseUrl,
|
|
164
|
+
baseUrl: providerListBaseUrl(prov.baseUrl),
|
|
124
165
|
authMode: prov.authMode ?? "key",
|
|
125
166
|
defaultModel: prov.defaultModel ?? null,
|
|
126
167
|
isDefault: name === config.defaultProvider,
|
|
127
168
|
source: registryEntry ? "registry" : "custom",
|
|
128
169
|
models: prov.models ?? [],
|
|
170
|
+
active,
|
|
129
171
|
};
|
|
130
172
|
});
|
|
131
173
|
|
package/src/cli/registry.ts
CHANGED
|
@@ -150,7 +150,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
name: "link",
|
|
153
|
-
usage: "ocx link <port|issue|status|revoke|reconnect>",
|
|
153
|
+
usage: "ocx link <port|issue|status|revoke|reconnect|cleanup>",
|
|
154
154
|
summary: "Allocate and manage a loopback remote home link.",
|
|
155
155
|
details: [
|
|
156
156
|
"Port: ocx link port [--json]",
|
|
@@ -158,6 +158,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
158
158
|
"Status: ocx link status [--json]",
|
|
159
159
|
"Revoke: ocx link revoke --link-id <id> [--force --yes] [--json]",
|
|
160
160
|
"Reconnect: ocx link reconnect --link-id <id> [--json]",
|
|
161
|
+
"Cleanup: ocx link cleanup [--link-id <id>] [--json]",
|
|
161
162
|
"Issue, status, revoke, and reconnect use the running proxy's loopback management API and admin token.",
|
|
162
163
|
],
|
|
163
164
|
},
|
|
@@ -434,6 +435,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
434
435
|
"Works both online (via live proxy API) and offline (modifies persisted config safely with atomic writes).",
|
|
435
436
|
],
|
|
436
437
|
},
|
|
438
|
+
{
|
|
439
|
+
name: "fallback",
|
|
440
|
+
usage: "ocx fallback <status|off|on|clear|local> [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]",
|
|
441
|
+
summary: "Turn model and provider fallback off or on per scope, and record per-device local fallback.",
|
|
442
|
+
details: [
|
|
443
|
+
"A refused fallback answers fallback_refused with the status of the failure that triggered it, and names the command that allows it.",
|
|
444
|
+
"Precedence: a device set to off always refuses; otherwise route, then provider, then global; unset allows.",
|
|
445
|
+
"Rotation among the accounts or keys of one provider pool is not a fallback and is not governed here.",
|
|
446
|
+
"Live proxy only. On a Remote Link Child, status reads the Home; writes are refused. Run write commands on the Home.",
|
|
447
|
+
],
|
|
448
|
+
},
|
|
437
449
|
{
|
|
438
450
|
name: "agent",
|
|
439
451
|
usage: "ocx agent <status|injection|effort|subagents|fallback|sidecar> ...",
|
|
@@ -16,6 +16,10 @@ export function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: f
|
|
|
16
16
|
console.error("The proxy home or lifecycle ownership could not be verified. Run `ocx status`; if a service owns the proxy, use its installation's `ocx service restart`, otherwise use the owning app or supervisor. No changes were made.");
|
|
17
17
|
} else if (code === "restart_package_tree_unsettled") {
|
|
18
18
|
console.error(`The proxy's package files are unsettled. Wait for the install to finish, then run \`ocx restart\`. ${INCOMPLETE_INSTALL_RECOVERY}`);
|
|
19
|
+
} else if (code === "restart_supervised_service") {
|
|
20
|
+
console.error("Restart not performed; the running client runtime was kept. Its background service owns it: run `ocx service restart`.");
|
|
21
|
+
} else if (code === "restart_supervised_desktop") {
|
|
22
|
+
console.error("Restart not performed; the running client runtime was kept. The OpenCodex desktop app owns it: restart it from the app.");
|
|
19
23
|
} else {
|
|
20
24
|
console.error("Proxy restart request could not be confirmed; no fallback stop/start was attempted. Run `ocx status` to inspect ownership and runtime state.");
|
|
21
25
|
}
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -202,7 +202,7 @@ export async function runtimeRequest<T = unknown>(
|
|
|
202
202
|
if (!linkChildReadable(method, path)) {
|
|
203
203
|
throw new RuntimeApiError(linkChildRefusal(link, `${method} ${pathname}`), 503, null, "link_child_management_unavailable");
|
|
204
204
|
}
|
|
205
|
-
const { status, body } = await readLinkHomeJson(link, path, deps.linkHome);
|
|
205
|
+
const { status, body } = await readLinkHomeJson(link, path, deps.linkHome, init.signal ? { signal: init.signal } : {});
|
|
206
206
|
if (status < 200 || status >= 300) throw new RuntimeApiError(responseMessage(body, status), status, body);
|
|
207
207
|
return body as T;
|
|
208
208
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OCX-6 fields of `ocx status --json`: which Codex settings this machine applied, when, and
|
|
3
|
+
* when its client runtime started.
|
|
4
|
+
*
|
|
5
|
+
* - `appliedCodexSettingsDigest` (any device): the digest of the values this machine applies. On a
|
|
6
|
+
* Remote Link Child that is its apply record (the Home's values its last sync wrote); on a Home or
|
|
7
|
+
* standalone install, its own projection, which is also exactly what it serves its Children. Null
|
|
8
|
+
* on a hub client and on a Child that has no apply record yet.
|
|
9
|
+
* - `lastApplyAt` (Child): epoch ms of the last successful apply of a different Home settings
|
|
10
|
+
* document; null elsewhere.
|
|
11
|
+
* - `runtimeStartedAt` (Child): epoch ms the live client runtime reports as its start; null when no
|
|
12
|
+
* client runtime answers.
|
|
13
|
+
*/
|
|
14
|
+
import type { LiveProxy } from "../server/proxy-liveness";
|
|
15
|
+
import type { OcxConfig } from "../types";
|
|
16
|
+
|
|
17
|
+
export interface CliAppliedCodexSettingsStatus {
|
|
18
|
+
appliedCodexSettingsDigest: string | null;
|
|
19
|
+
lastApplyAt: number | null;
|
|
20
|
+
runtimeStartedAt: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AppliedCodexSettingsStatusDeps {
|
|
24
|
+
readStartedAt?: (live: LiveProxy) => Promise<number | null>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function collectAppliedCodexSettingsStatus(
|
|
28
|
+
config: OcxConfig,
|
|
29
|
+
live: LiveProxy | null,
|
|
30
|
+
deps: AppliedCodexSettingsStatusDeps = {},
|
|
31
|
+
): Promise<CliAppliedCodexSettingsStatus> {
|
|
32
|
+
const { codexSettingsDigest, projectHomeCodexSettings } = await import("../link/codex-settings");
|
|
33
|
+
if (config.runtimeRole !== "client") {
|
|
34
|
+
return {
|
|
35
|
+
appliedCodexSettingsDigest: codexSettingsDigest(projectHomeCodexSettings(config, "0.0.0").settings),
|
|
36
|
+
lastApplyAt: null,
|
|
37
|
+
runtimeStartedAt: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const linkId = config.client?.transport === "link" ? config.client.link?.linkId : undefined;
|
|
41
|
+
if (!linkId) return { appliedCodexSettingsDigest: null, lastApplyAt: null, runtimeStartedAt: null };
|
|
42
|
+
const { readHomeCodexSettingsRecord } = await import("../client/link-home");
|
|
43
|
+
const record = readHomeCodexSettingsRecord(linkId);
|
|
44
|
+
const appliedAt = record ? Date.parse(record.appliedAt) : Number.NaN;
|
|
45
|
+
let runtimeStartedAt: number | null = null;
|
|
46
|
+
if (live?.role === "client") {
|
|
47
|
+
const read = deps.readStartedAt ?? (async (target: LiveProxy) =>
|
|
48
|
+
(await import("./link-apply-restart")).readClientRuntimeStartedAt(target));
|
|
49
|
+
runtimeStartedAt = await read(live);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
appliedCodexSettingsDigest: record ? codexSettingsDigest(record.document.settings) : null,
|
|
53
|
+
lastApplyAt: Number.isFinite(appliedAt) ? appliedAt : null,
|
|
54
|
+
runtimeStartedAt,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ocx status --json` `fallback` (OCX-11). The live proxy's `GET /api/fallback` when it answers
|
|
3
|
+
* (on a Remote Link Child that read is the Home's, over the link); otherwise the policy from this
|
|
4
|
+
* machine's config with no events, marked `source: "local"`. Never fails the status report.
|
|
5
|
+
*/
|
|
6
|
+
import type { OcxConfig } from "../types";
|
|
7
|
+
import { buildFallbackState, type FallbackStateView } from "../routing/fallback-state";
|
|
8
|
+
import { runtimeRequest, type RuntimeApiDeps } from "./runtime-api";
|
|
9
|
+
|
|
10
|
+
export type StatusFallback = (FallbackStateView | ReturnType<typeof buildFallbackState>) & { source: "live" | "home" | "local" };
|
|
11
|
+
|
|
12
|
+
const STATUS_FALLBACK_TIMEOUT_MS = 2_000;
|
|
13
|
+
|
|
14
|
+
export async function collectStatusFallback(
|
|
15
|
+
config: Pick<OcxConfig, "fallback">,
|
|
16
|
+
options: { live: boolean; linkChild: boolean; deps?: RuntimeApiDeps },
|
|
17
|
+
): Promise<StatusFallback> {
|
|
18
|
+
if (options.live) {
|
|
19
|
+
try {
|
|
20
|
+
const state = await runtimeRequest<FallbackStateView>("/api/fallback", {
|
|
21
|
+
signal: AbortSignal.timeout(STATUS_FALLBACK_TIMEOUT_MS),
|
|
22
|
+
}, options.deps ?? {});
|
|
23
|
+
if (state && typeof state === "object" && state.schema === "ocx.fallback/1") {
|
|
24
|
+
return { ...state, source: options.linkChild ? "home" : "live" };
|
|
25
|
+
}
|
|
26
|
+
} catch { /* an older or unreachable proxy: report the local policy instead */ }
|
|
27
|
+
}
|
|
28
|
+
return { ...buildFallbackState(options.linkChild ? {} : config), source: "local" };
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { detectInstallOwnership, type InstallOwnership } from "../update";
|
|
2
|
+
|
|
3
|
+
/** How this ocx is installed, from the updater's own ownership detector. */
|
|
4
|
+
export type InstallMethod = "bun" | "npm" | "pnpm" | "mise" | "source" | "standalone" | "unknown";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ocx status --json` `install`. Additive within `schemaVersion: 1`.
|
|
8
|
+
*
|
|
9
|
+
* The verdict is the one `ocx update` acts on (`src/update/install-detection.mjs`), read
|
|
10
|
+
* from the running package's own location — PATH is never consulted, because a machine can
|
|
11
|
+
* carry several package managers at once. `evidence` names what decided it without adding
|
|
12
|
+
* a path: the package layout, mise's backend record (tool alias and backend only), the
|
|
13
|
+
* compiled standalone binary, or nothing when detection itself failed.
|
|
14
|
+
*/
|
|
15
|
+
export type InstallDetectionStatus = {
|
|
16
|
+
method: InstallMethod;
|
|
17
|
+
evidence: {
|
|
18
|
+
basis: "package-layout" | "mise-backend-record" | "standalone-binary" | "unavailable";
|
|
19
|
+
mise?: { tool: string; backend: string };
|
|
20
|
+
error?: "metadata_unreadable" | "metadata_inconsistent" | "detection_failed";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function installDetectionStatus(
|
|
25
|
+
detect: () => InstallOwnership = detectInstallOwnership,
|
|
26
|
+
): InstallDetectionStatus {
|
|
27
|
+
let ownership: InstallOwnership;
|
|
28
|
+
try {
|
|
29
|
+
ownership = detect();
|
|
30
|
+
} catch {
|
|
31
|
+
return { method: "unknown", evidence: { basis: "unavailable", error: "detection_failed" } };
|
|
32
|
+
}
|
|
33
|
+
switch (ownership.installer) {
|
|
34
|
+
case "standalone":
|
|
35
|
+
return { method: "standalone", evidence: { basis: "standalone-binary" } };
|
|
36
|
+
case "mise":
|
|
37
|
+
if (ownership.owner) {
|
|
38
|
+
return {
|
|
39
|
+
method: "mise",
|
|
40
|
+
evidence: { basis: "mise-backend-record", mise: { tool: ownership.owner.tool, backend: ownership.owner.backend } },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { method: "mise", evidence: { basis: "mise-backend-record", error: ownership.error ?? "metadata_unreadable" } };
|
|
44
|
+
case "bun":
|
|
45
|
+
case "npm":
|
|
46
|
+
case "pnpm":
|
|
47
|
+
case "source":
|
|
48
|
+
return { method: ownership.installer, evidence: { basis: "package-layout" } };
|
|
49
|
+
default:
|
|
50
|
+
return { method: "unknown", evidence: { basis: "unavailable", error: "detection_failed" } };
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -36,6 +36,8 @@ export type { ListenTarget } from "./status-probes";
|
|
|
36
36
|
import { checkProxyHealth, probeUncleanExitState, type ListenTarget } from "./status-probes";
|
|
37
37
|
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
38
38
|
import { fetchBoundLocalManagementRead } from "../server/local-management-read-client";
|
|
39
|
+
import { collectAppliedCodexSettingsStatus, type CliAppliedCodexSettingsStatus } from "./status-codex-settings";
|
|
40
|
+
import { installDetectionStatus, type InstallDetectionStatus } from "./status-install";
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* The state of the data-plane admission secret the SERVICE will use. State only -- never the value.
|
|
@@ -118,6 +120,8 @@ export type CliRemoteHubStatus = {
|
|
|
118
120
|
|
|
119
121
|
export type CliStatusJson = {
|
|
120
122
|
mainAccountHardLock?: CodexMainAccountPolicyHealth;
|
|
123
|
+
/** OCX-11 fallback state; see src/cli/status-fallback.ts. */
|
|
124
|
+
fallback?: import("./status-fallback").StatusFallback;
|
|
121
125
|
schemaVersion: 1;
|
|
122
126
|
/**
|
|
123
127
|
* This machine's topology role, named rather than inferred (#4236).
|
|
@@ -229,8 +233,10 @@ export type CliStatusJson = {
|
|
|
229
233
|
*/
|
|
230
234
|
versionSkew: VersionSkew;
|
|
231
235
|
cliCommand: CliPathDiagnostics;
|
|
236
|
+
/** Installer ownership from the updater's detector (OCX-10); additive, `schemaVersion` stays 1. */
|
|
237
|
+
install: InstallDetectionStatus;
|
|
232
238
|
startupSource: "live" | "local" | "local-supervision-override";
|
|
233
|
-
};
|
|
239
|
+
} & CliAppliedCodexSettingsStatus;
|
|
234
240
|
|
|
235
241
|
export type CliStatusView = {
|
|
236
242
|
json: CliStatusJson;
|
|
@@ -853,12 +859,22 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
853
859
|
const mainAccountHardLock = options.mainAccountPolicy && live
|
|
854
860
|
? (await (await import("../oauth/health")).fetchCodexHealthFromLiveProxy(undefined, async () => live)).mainAccountHardLock
|
|
855
861
|
: undefined;
|
|
862
|
+
// Same `--json`-only gate as the hard-lock read above: the human report stays one probe.
|
|
863
|
+
const fallback = options.mainAccountPolicy
|
|
864
|
+
? await (await import("./status-fallback")).collectStatusFallback(config, {
|
|
865
|
+
live: Boolean(live), linkChild: config.client?.transport === "link",
|
|
866
|
+
deps: { findLiveProxy: async () => live },
|
|
867
|
+
})
|
|
868
|
+
: undefined;
|
|
856
869
|
const pidFile = readPid();
|
|
857
870
|
// Preserve an authoritative null from orphan/legacy liveness — do not restore pidFile.
|
|
858
871
|
const pid = resolveStatusPid(live, pidFile);
|
|
859
872
|
// No extra request: findLiveProxy's identity probe already parsed and validated the
|
|
860
873
|
// healthz body, so the version came back with the liveness result.
|
|
861
874
|
const versionSkew = computeVersionSkew(packageVersion(), live?.version);
|
|
875
|
+
// OCX-6: what this machine applied and, on a Child, when its runtime started. Additive and
|
|
876
|
+
// always present (null when unknown), so `schemaVersion` stays 1.
|
|
877
|
+
const appliedCodexSettings = await collectAppliedCodexSettingsStatus(config, live);
|
|
862
878
|
const listen = live
|
|
863
879
|
? {
|
|
864
880
|
port: live.port,
|
|
@@ -1005,6 +1021,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
1005
1021
|
healthLabel: formatCliStatusHealthLabel(health.label, cliCommand, remoteHub.connected),
|
|
1006
1022
|
json: {
|
|
1007
1023
|
...(mainAccountHardLock ? { mainAccountHardLock } : {}),
|
|
1024
|
+
...(fallback ? { fallback } : {}),
|
|
1008
1025
|
schemaVersion: 1,
|
|
1009
1026
|
runtimeRole: config.runtimeRole ?? "standalone",
|
|
1010
1027
|
proxy: {
|
|
@@ -1067,7 +1084,9 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
|
|
|
1067
1084
|
// print it under the wrong heading (#2701).
|
|
1068
1085
|
versionSkew,
|
|
1069
1086
|
cliCommand,
|
|
1087
|
+
install: installDetectionStatus(),
|
|
1070
1088
|
startupSource,
|
|
1089
|
+
...appliedCodexSettings,
|
|
1071
1090
|
},
|
|
1072
1091
|
};
|
|
1073
1092
|
}
|