@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
export const REMOTE_WORKSPACE_USAGE = `Usage:
|
|
23
23
|
ocx remote-workspace pair <hub-url> --pairing-code-stdin --root <absolute-path> [--root <absolute-path> ...] [--toolchain-root <absolute-directory> ...] [--executor-helper <absolute-file>] [--name <device-name>] [--json]
|
|
24
24
|
ocx remote-workspace agent
|
|
25
|
-
ocx remote-workspace status [--json]
|
|
25
|
+
ocx remote-workspace status [--json]
|
|
26
|
+
ocx remote-workspace hub [status|runtimes|sessions] [--json]`;
|
|
26
27
|
|
|
27
28
|
export interface RemoteWorkspaceCliDeps extends RuntimeApiDeps {
|
|
28
29
|
store?: RemoteWorkspaceDeviceStateStore;
|
|
@@ -76,6 +77,10 @@ function publicStatus(state: RemoteWorkspaceDeviceState | null): Record<string,
|
|
|
76
77
|
export async function runRemoteWorkspaceCommand(rawArgs: string[], deps: RemoteWorkspaceCliDeps = {}): Promise<number> {
|
|
77
78
|
const args = [...rawArgs];
|
|
78
79
|
const command = args.shift();
|
|
80
|
+
if (command === "hub") {
|
|
81
|
+
const { handleRemoteWorkspaceHubCommand } = await import("./remote-workspace-hub");
|
|
82
|
+
return handleRemoteWorkspaceHubCommand(args, deps);
|
|
83
|
+
}
|
|
79
84
|
const store = deps.store ?? new RemoteWorkspaceDeviceFileStore();
|
|
80
85
|
if (command === "status") {
|
|
81
86
|
const wantsJson = takeJsonFlag(args);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProxyRestartResult } from "./tray-proxy";
|
|
2
|
+
|
|
3
|
+
export const INCOMPLETE_INSTALL_RECOVERY = "If the installer already exited or failed, run `ocx status`, stop any running proxy through its owner (`ocx stop`, or its service or desktop app), then reinstall with the same package manager and start it again (see https://opencodex.me/troubleshooting/update-failed/).";
|
|
4
|
+
|
|
5
|
+
export function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
|
|
6
|
+
if (result.phase === "identity") {
|
|
7
|
+
console.error("Refusing to restart because the running proxy identity could not be attested. Run `ocx status` to inspect the intended proxy.");
|
|
8
|
+
} else if (result.phase === "request") {
|
|
9
|
+
const code = result.error instanceof Error ? result.error.message : "";
|
|
10
|
+
if (code === "restart_capability_unsupported") {
|
|
11
|
+
console.error("The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
|
|
12
|
+
console.error(" After confirming this home owns the proxy, run `ocx stop` and then `ocx start` once.");
|
|
13
|
+
} else if (code === "restart_version_skew") {
|
|
14
|
+
console.error("This CLI is older than the running proxy (or versions cannot be compared). Use the newer installation's ocx to restart; check `which -a ocx` and run `ocx status`. No changes were made.");
|
|
15
|
+
} else if (code === "update_restart_home_unverified") {
|
|
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
|
+
} else if (code === "restart_package_tree_unsettled") {
|
|
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 {
|
|
20
|
+
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
|
+
}
|
|
22
|
+
} else if (result.phase === "replacement") {
|
|
23
|
+
console.error("Proxy restart was accepted, but no identity-verified replacement became healthy in time. Run `ocx status` to inspect the replacement.");
|
|
24
|
+
} else {
|
|
25
|
+
console.error("Proxy was not running and the fallback start did not become healthy. Run `ocx status` to inspect startup state.");
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cli/root.ts
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
* the exit paths and the shim preflight, then returns the dispatchable head
|
|
9
9
|
* for the command switch in src/cli/index.ts.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { printFullUsage, printSubcommandUsage, printUsage, printVersion } from "./help";
|
|
12
12
|
import { parseReadyArgs, type ReadyArgs } from "./ready";
|
|
13
13
|
import { parseResolveArgs, type ResolveArgs } from "./resolve";
|
|
14
14
|
import { parseStopApproval } from "./stop-approval";
|
|
15
15
|
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
|
|
16
|
+
import { findCommand } from "./registry";
|
|
17
|
+
import { printUnknownCommand } from "./help-recovery";
|
|
18
|
+
import { noteCredentialArgv, redactSecretArgs } from "./secret-args";
|
|
16
19
|
|
|
17
20
|
export interface CliHead {
|
|
18
21
|
kind: "version" | "help" | "ready" | "resolve" | "command";
|
|
@@ -20,6 +23,10 @@ export interface CliHead {
|
|
|
20
23
|
args: string[];
|
|
21
24
|
/** For kind "help": the subcommand whose usage should print, if any. */
|
|
22
25
|
helpTarget?: string;
|
|
26
|
+
/** Explicit multi-token help topic; never passed to command execution. */
|
|
27
|
+
helpPath?: string[];
|
|
28
|
+
/** Complete static reference requested from root help. */
|
|
29
|
+
helpAll?: boolean;
|
|
23
30
|
/** Present only for `ready`; undefined when the ready args failed to parse. */
|
|
24
31
|
readyArgs?: ReadyArgs;
|
|
25
32
|
/** Present only for `resolve`; undefined when the resolve args failed to parse. */
|
|
@@ -32,19 +39,26 @@ export function parseCliHead(argv: string[]): CliHead {
|
|
|
32
39
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
33
40
|
return { kind: "version", command, args };
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const boundary = argv.indexOf("--");
|
|
43
|
+
const prefix = boundary < 0 ? argv : argv.slice(0, boundary);
|
|
44
|
+
const rootHelp = command === undefined || command === "help" || command === "--help" || command === "-h";
|
|
45
|
+
const commandHelp = prefix[1] === "help";
|
|
46
|
+
if (rootHelp || commandHelp || prefix.slice(1).some(value => value === "--help" || value === "-h")) {
|
|
47
|
+
const topic = rootHelp ? prefix.slice(1)
|
|
48
|
+
: commandHelp ? [command!, ...prefix.slice(2)] : prefix;
|
|
49
|
+
// Options and their operands are not part of a help topic. Keep argv intact.
|
|
50
|
+
const option = topic.findIndex((value, index) => value.startsWith("-")
|
|
51
|
+
&& (index > 0 || (rootHelp && ["--all", "--help", "-h"].includes(value))));
|
|
52
|
+
const path = option < 0 ? topic : topic.slice(0, option);
|
|
37
53
|
return {
|
|
38
54
|
kind: "help",
|
|
39
55
|
command,
|
|
40
56
|
args,
|
|
41
|
-
...(
|
|
57
|
+
...(path.length ? { helpTarget: path[0] } : {}),
|
|
58
|
+
...(path.length > 1 ? { helpPath: path } : {}),
|
|
59
|
+
...(rootHelp && path.length === 0 && prefix.includes("--all") ? { helpAll: true } : {}),
|
|
42
60
|
};
|
|
43
61
|
}
|
|
44
|
-
if (command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
45
|
-
// `ocx <cmd> --help|-h|help` prints that command's usage, not the full list.
|
|
46
|
-
return { kind: "help", command, args, helpTarget: command };
|
|
47
|
-
}
|
|
48
62
|
// P1: pre-parse `ocx ready` and reject invalid arguments with exit 64 BEFORE
|
|
49
63
|
// maybeAutoRestoreCodexShim (or any discovery/probe/filesystem-capable global
|
|
50
64
|
// preflight) runs. `ready --help` / `help ready` already exited above, so this
|
|
@@ -65,14 +79,24 @@ export function parseCliHead(argv: string[]): CliHead {
|
|
|
65
79
|
return { kind: "command", command, args };
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
export function uninstallArgsError(command: string | undefined, args: string[]): string | undefined {
|
|
83
|
+
if ((command !== "uninstall" && command !== "remove") || args.length <= 1) return undefined;
|
|
84
|
+
return `ocx ${command} does not accept arguments (got: ${redactSecretArgs(args.slice(1)).join(" ")}). No changes were made. See: ocx help ${command}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export async function runCli(argv: string[]): Promise<CliHead> {
|
|
88
|
+
noteCredentialArgv(argv);
|
|
69
89
|
const head = parseCliHead(argv);
|
|
70
90
|
switch (head.kind) {
|
|
71
91
|
case "version":
|
|
72
92
|
printVersion();
|
|
73
93
|
process.exit(0);
|
|
74
94
|
case "help": {
|
|
75
|
-
if (head.
|
|
95
|
+
if (head.helpAll) printFullUsage();
|
|
96
|
+
else if (head.helpTarget) printSubcommandUsage(head.helpTarget, head.helpPath, {
|
|
97
|
+
fallbackToParent: head.command !== "help" && head.command !== "--help"
|
|
98
|
+
&& head.command !== "-h" && head.args[1] !== "help",
|
|
99
|
+
});
|
|
76
100
|
else printUsage();
|
|
77
101
|
process.exit(0);
|
|
78
102
|
}
|
|
@@ -104,10 +128,19 @@ export async function runCli(argv: string[]): Promise<CliHead> {
|
|
|
104
128
|
return head;
|
|
105
129
|
}
|
|
106
130
|
case "command":
|
|
131
|
+
if (head.command !== "internal" && !findCommand(head.command ?? "")) {
|
|
132
|
+
printUnknownCommand(head.command);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
107
135
|
if (head.command === "stop" && !parseStopApproval(head.args.slice(1)).ok) {
|
|
108
136
|
console.error("Usage: ocx stop [--json [--expect-pid <pid> --expect-port <port> --expect-hostname <host> --expect-config-home <home> --expect-cli-version <version> --expect-compatibility-token <hex>]]");
|
|
109
137
|
process.exit(64);
|
|
110
138
|
}
|
|
139
|
+
const uninstallError = uninstallArgsError(head.command, head.args);
|
|
140
|
+
if (uninstallError) {
|
|
141
|
+
console.error(uninstallError);
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
111
144
|
maybeAutoRestoreCodexShim(head.command, head.args);
|
|
112
145
|
return head;
|
|
113
146
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Explicit create/replace/delete for routing profiles; contextual validation stays server-owned. */
|
|
2
|
+
import { isValidPolicyId } from "../routing/profile";
|
|
3
|
+
import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
|
|
4
|
+
import { readJsonInput, serializeManagementJson } from "./json-input";
|
|
5
|
+
import { CliUsageError, runtimeBaseUrl, runtimeRequest, takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
|
|
6
|
+
|
|
7
|
+
const USAGE = `Usage:
|
|
8
|
+
ocx route policy create <id> --file <profile.json|-> [--json]
|
|
9
|
+
ocx route policy update <id> --file <profile.json|-> --expected-revision <revision> [--json]
|
|
10
|
+
ocx route policy remove <id> --yes [--json]`;
|
|
11
|
+
const ERRORS: Readonly<Record<string, string>> = {
|
|
12
|
+
profile_exists: "Routing profile already exists. Use show and review an explicit update.",
|
|
13
|
+
profile_revision_conflict: "Routing profile changed. Read show again and review the changes before updating with its revision.",
|
|
14
|
+
unknown_profile: "Routing profile was not found on the selected target.",
|
|
15
|
+
invalid_profile: "The target refused the profile. Check its candidates, alias and policy settings against the target configuration.",
|
|
16
|
+
missing_profile_id: "A routing profile id is required.",
|
|
17
|
+
invalid_profile_mode: "The target refused the routing profile operation.",
|
|
18
|
+
alias_reference_conflict: "The alias change conflicts with existing model references. Resolve the conflicting mappings first.",
|
|
19
|
+
invalid_shadow_call_target: "The alias change would create an invalid shadow-call target. Inspect its routing references first.",
|
|
20
|
+
};
|
|
21
|
+
const PROFILE_KEYS = ["alias", "candidates", "require", "optimize", "limits", "unknownEvidence", "compatibility"];
|
|
22
|
+
const REQUIRE_KEYS = ["minContextWindow", "minQuotaHeadroom", "tools", "imageInput", "structuredOutput", "reasoningEffort", "serviceTier", "localOnly", "remoteAllowed", "encryptedCodexTasks"];
|
|
23
|
+
const WEIGHT_KEYS = ["latency", "health", "cost", "quota"];
|
|
24
|
+
const UNKNOWN_KEYS = ["capability", "health", "quota", "cost"];
|
|
25
|
+
const COMPATIBILITY_KEYS = ["requiredSuites", "minStatus", "maxEvidenceAgeMs", "unknownEvidence", "degradedEvidence"];
|
|
26
|
+
|
|
27
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
28
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
29
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
30
|
+
}
|
|
31
|
+
function only(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
32
|
+
return record(value) && Object.keys(value).every(key => keys.includes(key));
|
|
33
|
+
}
|
|
34
|
+
function text(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; }
|
|
35
|
+
function nonnegative(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
|
|
36
|
+
function evidence(value: unknown): boolean { return value === "allow" || value === "penalize" || value === "exclude"; }
|
|
37
|
+
function optionalFields(value: Record<string, unknown>, validators: Record<string, (value: unknown) => boolean>): boolean {
|
|
38
|
+
return Object.entries(validators).every(([key, accepts]) => !Object.hasOwn(value, key) || accepts(value[key]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Shape/ownership only: do not invent a provider configuration to run contextual validators. */
|
|
42
|
+
function editableProfile(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
if (!only(value, PROFILE_KEYS) || !Array.isArray(value.candidates) || !value.candidates.length
|
|
44
|
+
|| !value.candidates.every(candidate => only(candidate, ["provider", "model"]) && text(candidate.provider) && text(candidate.model))) return false;
|
|
45
|
+
if (Object.hasOwn(value, "alias") && typeof value.alias !== "string") return false;
|
|
46
|
+
if (Object.hasOwn(value, "require")) {
|
|
47
|
+
const required = value.require;
|
|
48
|
+
if (!only(required, REQUIRE_KEYS) || !optionalFields(required, {
|
|
49
|
+
minContextWindow: n => Number.isSafeInteger(n) && (n as number) > 0,
|
|
50
|
+
minQuotaHeadroom: n => nonnegative(n) && n <= 1,
|
|
51
|
+
reasoningEffort: n => typeof n === "string", serviceTier: n => typeof n === "string",
|
|
52
|
+
})) return false;
|
|
53
|
+
for (const key of ["tools", "imageInput", "structuredOutput", "localOnly", "remoteAllowed", "encryptedCodexTasks"]) {
|
|
54
|
+
if (Object.hasOwn(required, key) && typeof required[key] !== "boolean") return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Object.hasOwn(value, "optimize") && (!only(value.optimize, WEIGHT_KEYS) || !Object.values(value.optimize).every(nonnegative))) return false;
|
|
58
|
+
if (Object.hasOwn(value, "limits") && (!only(value.limits, ["maxEstimatedCostUsd", "onUnknownCost"])
|
|
59
|
+
|| !optionalFields(value.limits, { maxEstimatedCostUsd: nonnegative, onUnknownCost: n => n === "allow" || n === "exclude" }))) return false;
|
|
60
|
+
if (Object.hasOwn(value, "unknownEvidence") && (!only(value.unknownEvidence, UNKNOWN_KEYS) || !Object.values(value.unknownEvidence).every(evidence))) return false;
|
|
61
|
+
if (Object.hasOwn(value, "compatibility")) {
|
|
62
|
+
const compatibility = value.compatibility;
|
|
63
|
+
if (!only(compatibility, COMPATIBILITY_KEYS) || !optionalFields(compatibility, {
|
|
64
|
+
requiredSuites: n => Array.isArray(n) && n.every(suite => only(suite, ["suiteId", "evidenceLayer"])
|
|
65
|
+
&& text(suite.suiteId) && ["protocol_conformance", "live_route_compatibility"].includes(suite.evidenceLayer as string)),
|
|
66
|
+
minStatus: n => n === "PROBED" || n === "VERIFIED",
|
|
67
|
+
maxEvidenceAgeMs: n => Number.isSafeInteger(n) && (n as number) >= 0,
|
|
68
|
+
unknownEvidence: evidence, degradedEvidence: evidence,
|
|
69
|
+
})) return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function invalidResult(): never { throw new Error("Invalid routing profile result"); }
|
|
75
|
+
|
|
76
|
+
function projectProfile(value: unknown, id: string, model: unknown): Record<string, unknown> {
|
|
77
|
+
if (!only(value, [...PROFILE_KEYS, "id", "model", "revision"]) || value.id !== id
|
|
78
|
+
|| !text(value.revision) || !text(model) || value.model !== model
|
|
79
|
+
|| (value.alias !== null && !text(value.alias))) invalidResult();
|
|
80
|
+
const { id: _id, model: _model, revision: _revision, alias, ...editable } = value;
|
|
81
|
+
const input = { ...editable, ...(alias === null ? {} : { alias }) };
|
|
82
|
+
if (!editableProfile(input) || model !== (alias ?? `policy/${id}`)
|
|
83
|
+
|| !record(value.require) || !record(value.limits)
|
|
84
|
+
|| !record(value.optimize) || !WEIGHT_KEYS.every(key => Object.hasOwn(value.optimize as object, key))
|
|
85
|
+
|| !record(value.unknownEvidence) || !UNKNOWN_KEYS.every(key => Object.hasOwn(value.unknownEvidence as object, key))) invalidResult();
|
|
86
|
+
if (value.compatibility !== undefined && (!record(value.compatibility)
|
|
87
|
+
|| !Array.isArray(value.compatibility.requiredSuites) || !evidence(value.compatibility.unknownEvidence)
|
|
88
|
+
|| !evidence(value.compatibility.degradedEvidence))) invalidResult();
|
|
89
|
+
// Every nested field was admitted above; rebuild the envelope rather than echoing server metadata.
|
|
90
|
+
return { id, model, revision: value.revision, alias, ...editable };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function printResult(value: unknown, id: string, remove: boolean, wantsJson: boolean): number {
|
|
94
|
+
if (!only(value, remove ? ["success", "id", "catalogRefresh"] : ["success", "id", "model", "profile", "catalogRefresh"])
|
|
95
|
+
|| value.success !== true || value.id !== id) invalidResult();
|
|
96
|
+
const data: Record<string, unknown> = { success: true, id };
|
|
97
|
+
const lines = [remove ? `Removed routing profile: ${id}` : `Saved routing profile: ${id}`];
|
|
98
|
+
if (!remove) {
|
|
99
|
+
data.profile = projectProfile(value.profile, id, value.model);
|
|
100
|
+
data.model = value.model;
|
|
101
|
+
lines.push(`Model: ${value.model}`, `Revision: ${(data.profile as Record<string, unknown>).revision}`);
|
|
102
|
+
}
|
|
103
|
+
return printCatalogResult(data, value.catalogRefresh, wantsJson, lines);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function handleRoutePolicyWriteCommand(
|
|
107
|
+
sub: "create" | "update" | "remove", argv: string[], deps: RuntimeApiDeps = {},
|
|
108
|
+
): Promise<number> {
|
|
109
|
+
return runCatalogAction(async () => {
|
|
110
|
+
const args = [...argv];
|
|
111
|
+
const id = args.shift();
|
|
112
|
+
if (!id || !isValidPolicyId(id)) throw new CliUsageError("A valid exact routing profile id is required (letters, numbers, dot, underscore or hyphen; max 64).", USAGE);
|
|
113
|
+
const wantsJson = takeFlag(args, "--json");
|
|
114
|
+
const yes = takeFlag(args, "--yes");
|
|
115
|
+
const file = takeOptionWithSyntax(args, "--file")?.value;
|
|
116
|
+
const revision = takeOptionWithSyntax(args, "--expected-revision")?.value;
|
|
117
|
+
if (args.length) throw new CliUsageError("Unknown, repeated or unexpected routing profile arguments.", USAGE);
|
|
118
|
+
if (sub === "remove") {
|
|
119
|
+
if (!yes || file !== undefined || revision !== undefined) throw new CliUsageError("Removal requires --yes and does not accept a file or revision.", USAGE);
|
|
120
|
+
} else {
|
|
121
|
+
if (yes || !file?.trim()) throw new CliUsageError("Creation and update require --file and do not accept --yes.", USAGE);
|
|
122
|
+
if (sub === "update" ? !revision?.trim() : revision !== undefined) throw new CliUsageError("Update requires an explicit nonblank --expected-revision; create does not accept it.", USAGE);
|
|
123
|
+
}
|
|
124
|
+
let body: string | undefined;
|
|
125
|
+
if (sub !== "remove") {
|
|
126
|
+
const profile = await readJsonInput(file!, deps);
|
|
127
|
+
if (!editableProfile(profile)) throw new CliUsageError("Profile file must contain only editable routing fields with valid nested types; omit id, model and revision.", USAGE);
|
|
128
|
+
body = serializeManagementJson({ id, mode: sub, profile, ...(sub === "update" ? { expectedRevision: revision } : {}) });
|
|
129
|
+
}
|
|
130
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
131
|
+
const result = await runtimeRequest(sub === "remove" ? `/api/routing-profiles?id=${encodeURIComponent(id)}` : "/api/routing-profiles", {
|
|
132
|
+
method: sub === "remove" ? "DELETE" : "PUT", redirect: "error",
|
|
133
|
+
...(body !== undefined ? { headers: { "content-type": "application/json" }, body } : {}),
|
|
134
|
+
}, pinned);
|
|
135
|
+
return printResult(result, id, sub === "remove", wantsJson);
|
|
136
|
+
}, ERRORS);
|
|
137
|
+
}
|
package/src/cli/route-policy.ts
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
} from "./runtime-api";
|
|
12
12
|
|
|
13
13
|
const USAGE = `Usage:
|
|
14
|
+
ocx route policy create <id> --file <FILE|-> [--json]
|
|
15
|
+
ocx route policy update <id> --file <FILE|-> --expected-revision <revision> [--json]
|
|
16
|
+
ocx route policy remove <id> --yes [--json]
|
|
14
17
|
ocx route policy list [--json]
|
|
15
18
|
ocx route policy show <id> [--json]
|
|
16
19
|
ocx route policy dry-run <id> [--model-context <tokens>] [--tools]
|
|
@@ -83,6 +86,11 @@ async function dryRun(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
export async function handleRoutePolicyCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
89
|
+
const [write, ...rest] = argv;
|
|
90
|
+
if (write === "create" || write === "update" || write === "remove") {
|
|
91
|
+
const { handleRoutePolicyWriteCommand } = await import("./route-policy-write");
|
|
92
|
+
return handleRoutePolicyWriteCommand(write, rest, deps);
|
|
93
|
+
}
|
|
86
94
|
return runCliAction(async () => {
|
|
87
95
|
const [sub, ...rest] = argv;
|
|
88
96
|
if (!sub) throw new CliUsageError("route policy requires a subcommand (list, show, dry-run, evaluate)", USAGE);
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { findLiveProxy, probeHostname, type LivenessIo, type LiveProxy } from "../server/proxy-liveness";
|
|
13
13
|
import { runningProxyUpdateHeaders } from "../oauth/login-cli";
|
|
14
|
+
import { redactSecretArgs } from "./secret-args";
|
|
15
|
+
export { redactSecretArgs };
|
|
14
16
|
|
|
15
17
|
export type CliStdin = NodeJS.ReadableStream & { isTTY?: boolean; readableEnded?: boolean };
|
|
16
18
|
|
|
@@ -22,6 +24,8 @@ export interface RuntimeApiDeps {
|
|
|
22
24
|
/** Test injection for commands that read a secret from stdin instead of argv. */
|
|
23
25
|
stdinImpl?: CliStdin;
|
|
24
26
|
stdinTimeoutMs?: number;
|
|
27
|
+
/** Cancellation for bounded secret-byte reads; line readers retain their contract. */
|
|
28
|
+
stdinSignal?: AbortSignal;
|
|
25
29
|
/** Optional proxy liveness probe injection for commands that check or fall back around live runtime state. */
|
|
26
30
|
findLiveProxy?: (io?: LivenessIo) => Promise<LiveProxy | null>;
|
|
27
31
|
}
|
|
@@ -33,11 +37,15 @@ export class CliUsageError extends Error {
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
export const PROXY_NOT_RUNNING_MESSAGE = "Proxy is not running. Start the intended proxy with: ocx start. No request was sent.";
|
|
41
|
+
|
|
36
42
|
export class RuntimeApiError extends Error {
|
|
37
43
|
constructor(
|
|
38
44
|
message: string,
|
|
39
45
|
readonly status: number,
|
|
40
46
|
readonly body: unknown,
|
|
47
|
+
/** Local discovery refusals occur before the management request is sent. */
|
|
48
|
+
readonly code?: "proxy_not_running" | "client_role_management_unavailable",
|
|
41
49
|
) {
|
|
42
50
|
super(message);
|
|
43
51
|
this.name = "RuntimeApiError";
|
|
@@ -70,12 +78,12 @@ function clientRoleManagementRefusal(port: number): string {
|
|
|
70
78
|
export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
|
|
71
79
|
if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
|
|
72
80
|
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
73
|
-
if (!live) throw new RuntimeApiError(
|
|
81
|
+
if (!live) throw new RuntimeApiError(PROXY_NOT_RUNNING_MESSAGE, 503, null, "proxy_not_running");
|
|
74
82
|
// The role comes from the same identity-checked /healthz body liveness already parsed, so
|
|
75
83
|
// this costs no extra request. Only the client role is refused: an absent role is a
|
|
76
84
|
// standalone or hub proxy (or a legacy body that predates the field), and both serve /api/*.
|
|
77
85
|
if (live.role === "client") {
|
|
78
|
-
throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null);
|
|
86
|
+
throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null, "client_role_management_unavailable");
|
|
79
87
|
}
|
|
80
88
|
return `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
81
89
|
}
|
|
@@ -85,6 +93,33 @@ function stringField(record: Record<string, unknown>, key: string): string | und
|
|
|
85
93
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
function integrationRecoveryMessage(reason: string | undefined): string | undefined {
|
|
97
|
+
switch (reason) {
|
|
98
|
+
case "not_installed": return "The client is not installed. Run: ocx integration client status.";
|
|
99
|
+
case "non_loopback": return "The integration requires a loopback destination. Run: ocx integration client status.";
|
|
100
|
+
case "drift_requires_confirm": return "The client configuration changed. Run `ocx integration client status` before explicitly confirming restore.";
|
|
101
|
+
case "snapshot_expired": return "The recovery snapshot expired. Run `ocx integration client status` before creating a new change.";
|
|
102
|
+
case "superseded_store": return "The client uses a different configuration store. Run `ocx integration client status`, then disable and enable the selected client.";
|
|
103
|
+
case "unsafe": return "The client config path is unsafe to edit (for example a link or a competing file). Run `ocx integration client status` to see the affected path.";
|
|
104
|
+
case "conflict": return "The client config no longer matches the recorded ownership. Run `ocx integration client status`; add --overwrite-conflict to enable only if replacing it is intended.";
|
|
105
|
+
case "write_failed": return "The client config write failed. Run `ocx integration client status` before retrying.";
|
|
106
|
+
default: return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Integration writer detail is the only server prose this client appends beside the primary
|
|
112
|
+
* error. Scope it to the normalized request pathname (dot segments resolved, no query) and to
|
|
113
|
+
* an unredirected response so no other route's body can ride along.
|
|
114
|
+
*/
|
|
115
|
+
function isIntegrationRoute(path: string, response: Response): boolean {
|
|
116
|
+
if (response.redirected) return false;
|
|
117
|
+
let pathname: string;
|
|
118
|
+
try { pathname = new URL(path.startsWith("/") ? path : "/" + path, "http://cli.invalid").pathname; }
|
|
119
|
+
catch { return false; }
|
|
120
|
+
return /^\/api\/client-integrations(?:\/|$)/.test(pathname);
|
|
121
|
+
}
|
|
122
|
+
|
|
88
123
|
/**
|
|
89
124
|
* Compose the operator-facing message from a management error body.
|
|
90
125
|
*
|
|
@@ -102,7 +137,7 @@ function stringField(record: Record<string, unknown>, key: string): string | und
|
|
|
102
137
|
* (#4662). Name the route instead, so any listener that does not serve a path stays legible
|
|
103
138
|
* even if another one starts answering this way.
|
|
104
139
|
*/
|
|
105
|
-
function responseMessage(body: unknown, status: number): string {
|
|
140
|
+
function responseMessage(body: unknown, status: number, integrationRoute = false): string {
|
|
106
141
|
if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
|
|
107
142
|
if (!body || typeof body !== "object") return `Management request failed (${status})`;
|
|
108
143
|
const record = body as Record<string, unknown>;
|
|
@@ -119,6 +154,13 @@ function responseMessage(body: unknown, status: number): string {
|
|
|
119
154
|
if (primary) break;
|
|
120
155
|
}
|
|
121
156
|
const parts = [primary ?? `Management request failed (${status})`];
|
|
157
|
+
// Integration mutation routes carry a writer explanation beside a generic error. Its prose is
|
|
158
|
+
// server text (paths, upstream errors), so render fixed guidance keyed on the closed reason
|
|
159
|
+
// code instead of echoing it; other management bodies keep their presentation contract.
|
|
160
|
+
if (integrationRoute && stringField(record, "message") && stringField(record, "message") !== primary) {
|
|
161
|
+
const fixed = integrationRecoveryMessage(stringField(record, "reason"));
|
|
162
|
+
if (fixed) parts.push(`Details: ${fixed}`);
|
|
163
|
+
}
|
|
122
164
|
const reason = stringField(record, "reason");
|
|
123
165
|
// A body of {ok:false, reason:"…"} with no `error` key used to degrade to the
|
|
124
166
|
// generic line, discarding the only actionable field.
|
|
@@ -158,7 +200,9 @@ export async function runtimeRequest<T = unknown>(
|
|
|
158
200
|
try { body = JSON.parse(text); }
|
|
159
201
|
catch { body = text; }
|
|
160
202
|
}
|
|
161
|
-
if (!response.ok)
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
throw new RuntimeApiError(responseMessage(body, response.status, isIntegrationRoute(path, response)), response.status, body);
|
|
205
|
+
}
|
|
162
206
|
return body as T;
|
|
163
207
|
}
|
|
164
208
|
|
|
@@ -226,79 +270,6 @@ export function csv(value: string | undefined): string[] | undefined {
|
|
|
226
270
|
return [...new Set(value.split(",").map(item => item.trim()).filter(Boolean))];
|
|
227
271
|
}
|
|
228
272
|
|
|
229
|
-
/**
|
|
230
|
-
* Options whose VALUE is a credential (or can carry one), listed here so a parse
|
|
231
|
-
* error never prints one. `--headers` belongs on the list defensively: custom
|
|
232
|
-
* headers are documented as non-secret metadata and the validator rejects the
|
|
233
|
-
* standard credential names, but it cannot recognize an arbitrary one such as
|
|
234
|
-
* `X-My-Token`, so a parse error must not echo the value back either way.
|
|
235
|
-
*
|
|
236
|
-
* `takeOption` only understands `--flag value`. `--flag=value` therefore falls
|
|
237
|
-
* through to `rejectArgs`, which reports the offending argument verbatim — for
|
|
238
|
-
* `--code=https://…?code=SECRET` that writes the authorization code to stderr,
|
|
239
|
-
* which is the exact exposure the stdin path exists to avoid.
|
|
240
|
-
*/
|
|
241
|
-
const SECRET_OPTIONS = [
|
|
242
|
-
"--code",
|
|
243
|
-
"--headers",
|
|
244
|
-
"--token",
|
|
245
|
-
"--admin-token",
|
|
246
|
-
"--pairing-code",
|
|
247
|
-
"--credential-env",
|
|
248
|
-
"--admin-token-env",
|
|
249
|
-
"--pairing-code-env",
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Replace credential values before they are reported back.
|
|
254
|
-
*
|
|
255
|
-
* Both spellings have to be covered, and the space-separated one spans two
|
|
256
|
-
* tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
|
|
257
|
-
* does not parse `--code` leaves the flag AND its value in the leftovers, and
|
|
258
|
-
* reporting them verbatim writes the credential to stderr. Repeating the
|
|
259
|
-
* option does the same with the second value, since the parser takes only the
|
|
260
|
-
* first occurrence.
|
|
261
|
-
*
|
|
262
|
-
* The token after the option is redacted whatever it looks like. Skipping
|
|
263
|
-
* `--`-prefixed tokens read as "that is a flag, not a value", but the shell
|
|
264
|
-
* hands over whatever was typed: `--code --SUPERSECRET` and
|
|
265
|
-
* `--code -- SUPERSECRET` both put the credential straight in the message. A
|
|
266
|
-
* mistaken `--code --json` now reads `--code <redacted>`, which is worse
|
|
267
|
-
* diagnostics for a case that already prints the usage text, and better than
|
|
268
|
-
* printing a credential.
|
|
269
|
-
*
|
|
270
|
-
* `redactValues` extends that to bare leftovers, for commands whose positional
|
|
271
|
-
* argument is itself a credential.
|
|
272
|
-
*/
|
|
273
|
-
function redactSecretArgs(args: string[], redactValues = false): string[] {
|
|
274
|
-
const out: string[] = [];
|
|
275
|
-
for (let index = 0; index < args.length; index++) {
|
|
276
|
-
const arg = args[index] as string;
|
|
277
|
-
const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
|
|
278
|
-
if (inline) {
|
|
279
|
-
out.push(`${inline}=<redacted>`);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
if (SECRET_OPTIONS.includes(arg)) {
|
|
283
|
-
out.push(arg);
|
|
284
|
-
// Swallow the value that belongs to it. `--` is an end-of-options
|
|
285
|
-
// separator, so the value is the token after it.
|
|
286
|
-
let valueIndex = index + 1;
|
|
287
|
-
if (args[valueIndex] === "--") {
|
|
288
|
-
out.push("--");
|
|
289
|
-
valueIndex++;
|
|
290
|
-
}
|
|
291
|
-
if (args[valueIndex] !== undefined) {
|
|
292
|
-
out.push("<redacted>");
|
|
293
|
-
index = valueIndex;
|
|
294
|
-
}
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
|
|
298
|
-
}
|
|
299
|
-
return out;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
273
|
export interface RejectArgsOptions {
|
|
303
274
|
/**
|
|
304
275
|
* Report bare leftovers as `<redacted>`. Set by commands where a stray
|
|
@@ -399,16 +370,19 @@ export async function readSecretBytes(
|
|
|
399
370
|
deps: RuntimeApiDeps,
|
|
400
371
|
label: string,
|
|
401
372
|
maxBytes = MAX_LINK_CREDENTIAL_BYTES,
|
|
402
|
-
): Promise<Uint8Array
|
|
373
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
403
374
|
const input: CliStdin = deps.stdinImpl ?? process.stdin;
|
|
404
375
|
const timeoutMs = deps.stdinTimeoutMs ?? 120_000;
|
|
376
|
+
const signal = deps.stdinSignal;
|
|
377
|
+
signal?.throwIfAborted();
|
|
405
378
|
if (input.readableEnded === true) throw new CliUsageError(`${label} input was empty`);
|
|
406
|
-
return await new Promise<Uint8Array
|
|
379
|
+
return await new Promise<Uint8Array<ArrayBuffer>>((resolve, reject) => {
|
|
407
380
|
const chunks: Uint8Array[] = [];
|
|
408
381
|
let total = 0;
|
|
409
382
|
let settled = false;
|
|
410
383
|
const cleanup = () => {
|
|
411
384
|
clearTimeout(timer);
|
|
385
|
+
signal?.removeEventListener("abort", onAbort);
|
|
412
386
|
input.removeListener("data", onData);
|
|
413
387
|
input.removeListener("end", onEnd);
|
|
414
388
|
input.removeListener("error", onError);
|
|
@@ -459,6 +433,7 @@ export async function readSecretBytes(
|
|
|
459
433
|
resolve(result);
|
|
460
434
|
});
|
|
461
435
|
const onError = (error: Error) => finish(() => reject(error));
|
|
436
|
+
const onAbort = () => finish(() => reject(signal?.reason));
|
|
462
437
|
const timer = setTimeout(
|
|
463
438
|
() => finish(() => reject(new CliUsageError(`timed out waiting for ${label} on stdin`))),
|
|
464
439
|
timeoutMs,
|
|
@@ -466,12 +441,14 @@ export async function readSecretBytes(
|
|
|
466
441
|
input.on("data", onData);
|
|
467
442
|
input.on("end", onEnd);
|
|
468
443
|
input.on("error", onError);
|
|
444
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
445
|
+
if (signal?.aborted) onAbort();
|
|
469
446
|
});
|
|
470
447
|
}
|
|
471
448
|
|
|
472
449
|
export function printData(value: unknown, wantsJson: boolean, lines?: string[]): void {
|
|
473
450
|
if (wantsJson || !lines) console.log(JSON.stringify(value, null, 2));
|
|
474
|
-
else for (const line of lines) console.log(line);
|
|
451
|
+
else for (const line of lines) console.log(terminalSafeText(line));
|
|
475
452
|
}
|
|
476
453
|
|
|
477
454
|
/**
|