@bitkyc08/opencodex 2.77.0 → 2.78.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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- 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/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 +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- 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 +30 -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 +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -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 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- 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/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- 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/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -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 +7 -0
- 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/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 +112 -34
- package/src/cli/registry.ts +62 -20
- 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 +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- 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/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/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- 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/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 +58 -10
- 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/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- 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/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -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 +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- 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 +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- 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-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- 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 +5 -5
- package/src/server/management/model-routes.ts +3 -3
- 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 +25 -24
- 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/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- 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/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
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,6 +441,8 @@ 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
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Credential-option redaction for CLI argument errors. Kept dependency-free so the CLI head can use it. */
|
|
2
|
+
/**
|
|
3
|
+
* Options whose VALUE is a credential (or can carry one), listed here so a parse
|
|
4
|
+
* error never prints one. `--headers` belongs on the list defensively: custom
|
|
5
|
+
* headers are documented as non-secret metadata and the validator rejects the
|
|
6
|
+
* standard credential names, but it cannot recognize an arbitrary one such as
|
|
7
|
+
* `X-My-Token`, so a parse error must not echo the value back either way.
|
|
8
|
+
*
|
|
9
|
+
* `takeOption` only understands `--flag value`. `--flag=value` therefore falls
|
|
10
|
+
* through to `rejectArgs`, which reports the offending argument verbatim — for
|
|
11
|
+
* `--code=https://…?code=SECRET` that writes the authorization code to stderr,
|
|
12
|
+
* which is the exact exposure the stdin path exists to avoid.
|
|
13
|
+
*/
|
|
14
|
+
const SECRET_OPTIONS = [
|
|
15
|
+
"--code",
|
|
16
|
+
"--headers",
|
|
17
|
+
"--api-key",
|
|
18
|
+
"--key",
|
|
19
|
+
"--secret",
|
|
20
|
+
"--password",
|
|
21
|
+
"--token",
|
|
22
|
+
"--admin-token",
|
|
23
|
+
"--pairing-code",
|
|
24
|
+
"--credential-env",
|
|
25
|
+
"--admin-token-env",
|
|
26
|
+
"--pairing-code-env",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function isSecretOptionToken(token: string): boolean {
|
|
30
|
+
return SECRET_OPTIONS.includes(token) || SECRET_OPTIONS.some(option => token.startsWith(`${option}=`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let credentialArgvSeen = false;
|
|
34
|
+
let credentialValues: string[] = [];
|
|
35
|
+
let consoleScrubInstalled = false;
|
|
36
|
+
|
|
37
|
+
/** Operands of credential options in argv, longest first; very short values are not scrubbed. */
|
|
38
|
+
function credentialOperands(argv: readonly string[]): string[] {
|
|
39
|
+
const values = new Set<string>();
|
|
40
|
+
for (let index = 0; index < argv.length; index++) {
|
|
41
|
+
const token = argv[index] as string;
|
|
42
|
+
const inline = SECRET_OPTIONS.find(option => token.startsWith(`${option}=`));
|
|
43
|
+
let value: string | undefined;
|
|
44
|
+
if (inline) value = token.slice(inline.length + 1);
|
|
45
|
+
else if (SECRET_OPTIONS.includes(token)) {
|
|
46
|
+
value = argv[argv[index + 1] === "--" ? index + 2 : index + 1];
|
|
47
|
+
// A following bare credential option is not this option's operand; its own operand is
|
|
48
|
+
// collected on its turn. An inline value is always an operand, whatever it looks like.
|
|
49
|
+
if (value !== undefined && SECRET_OPTIONS.includes(value)) value = undefined;
|
|
50
|
+
}
|
|
51
|
+
if (value !== undefined && value.length >= 4) values.add(value);
|
|
52
|
+
}
|
|
53
|
+
return [...values].sort((a, b) => b.length - a.length);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Replace every argv credential operand in text printed by this process. */
|
|
57
|
+
export function scrubCredentialOperands(text: string): string {
|
|
58
|
+
let out = text;
|
|
59
|
+
// Case-insensitive: some parsers lowercase an action before echoing it.
|
|
60
|
+
for (const value of credentialValues) out = out.replace(new RegExp(value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi"), "<redacted>");
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Last-line guard for console diagnostics (stderr): parsers that echo a selector, an unknown
|
|
66
|
+
* subcommand or a leftover before any redaction would otherwise print an operand typed after
|
|
67
|
+
* a credential option. Installed once; it reads the operands recorded for the current argv.
|
|
68
|
+
*/
|
|
69
|
+
function installConsoleScrub(): void {
|
|
70
|
+
if (consoleScrubInstalled) return;
|
|
71
|
+
consoleScrubInstalled = true;
|
|
72
|
+
// Diagnostics only: stdout carries JSON documents whose primitives must not be rewritten.
|
|
73
|
+
for (const method of ["warn", "error"] as const) {
|
|
74
|
+
const original = console[method].bind(console);
|
|
75
|
+
console[method] = (...args: unknown[]) => {
|
|
76
|
+
if (credentialValues.length === 0) return original(...args);
|
|
77
|
+
return original(...args.map(arg => typeof arg === "string" ? scrubCredentialOperands(arg)
|
|
78
|
+
: arg instanceof Error ? scrubCredentialOperands(arg.stack ?? arg.message) : arg));
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Record whether the process argv carried any credential option. Parsers that take
|
|
85
|
+
* positionals can consume the option token itself (`ocx account use openai --token X`
|
|
86
|
+
* makes `--token` the id), leaving only its operand as a "leftover"; once a credential
|
|
87
|
+
* option was present anywhere, every bare leftover is reported as `<redacted>`.
|
|
88
|
+
*/
|
|
89
|
+
export function noteCredentialArgv(argv: readonly string[]): void {
|
|
90
|
+
credentialArgvSeen = argv.some(isSecretOptionToken);
|
|
91
|
+
credentialValues = credentialOperands(argv);
|
|
92
|
+
if (credentialValues.length > 0) installConsoleScrub();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Replace credential values before they are reported back.
|
|
97
|
+
*
|
|
98
|
+
* Both spellings have to be covered, and the space-separated one spans two
|
|
99
|
+
* tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
|
|
100
|
+
* does not parse `--code` leaves the flag AND its value in the leftovers, and
|
|
101
|
+
* reporting them verbatim writes the credential to stderr. Repeating the
|
|
102
|
+
* option does the same with the second value, since the parser takes only the
|
|
103
|
+
* first occurrence.
|
|
104
|
+
*
|
|
105
|
+
* The token after the option is redacted whatever it looks like. Skipping
|
|
106
|
+
* `--`-prefixed tokens read as "that is a flag, not a value", but the shell
|
|
107
|
+
* hands over whatever was typed: `--code --SUPERSECRET` and
|
|
108
|
+
* `--code -- SUPERSECRET` both put the credential straight in the message. A
|
|
109
|
+
* mistaken `--code --json` now reads `--code <redacted>`, which is worse
|
|
110
|
+
* diagnostics for a case that already prints the usage text, and better than
|
|
111
|
+
* printing a credential.
|
|
112
|
+
*
|
|
113
|
+
* `redactValues` extends that to bare leftovers, for commands whose positional
|
|
114
|
+
* argument is itself a credential.
|
|
115
|
+
*/
|
|
116
|
+
export function redactSecretArgs(args: string[], redactValues = false): string[] {
|
|
117
|
+
redactValues ||= credentialArgvSeen;
|
|
118
|
+
const out: string[] = [];
|
|
119
|
+
for (let index = 0; index < args.length; index++) {
|
|
120
|
+
const arg = args[index] as string;
|
|
121
|
+
const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
|
|
122
|
+
if (inline) {
|
|
123
|
+
out.push(`${inline}=<redacted>`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (SECRET_OPTIONS.includes(arg)) {
|
|
127
|
+
out.push(arg);
|
|
128
|
+
// Swallow the value that belongs to it. `--` is an end-of-options
|
|
129
|
+
// separator, so the value is the token after it.
|
|
130
|
+
let valueIndex = index + 1;
|
|
131
|
+
if (args[valueIndex] === "--") {
|
|
132
|
+
out.push("--");
|
|
133
|
+
valueIndex++;
|
|
134
|
+
}
|
|
135
|
+
const next = args[valueIndex];
|
|
136
|
+
// A following credential option is not this option's value: leave it for the
|
|
137
|
+
// next iteration so its own operand is redacted too (`--code --token SECRET`).
|
|
138
|
+
if (next !== undefined && !isSecretOptionToken(next)) {
|
|
139
|
+
out.push("<redacted>");
|
|
140
|
+
index = valueIndex;
|
|
141
|
+
} else if (next !== undefined) {
|
|
142
|
+
index = valueIndex - 1;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { printData } from "./runtime-api";
|
|
2
|
+
import type { CodexDesktopSwitchApply, CodexDesktopSwitchApplyReason } from "../codex/desktop-switches";
|
|
3
|
+
|
|
4
|
+
const APPLY_REASONS = {
|
|
5
|
+
not_requested: true, proxy_not_running: true, integration_disabled: true,
|
|
6
|
+
external_provider: true, ownership_undetermined: true, write_lock_busy: true, injection_refused: true,
|
|
7
|
+
} satisfies Record<CodexDesktopSwitchApplyReason, true>;
|
|
8
|
+
|
|
9
|
+
/** The shared native apply DTO, excluding exception/path detail. */
|
|
10
|
+
export function projectSettingsApply(value: unknown): CodexDesktopSwitchApply | null {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
12
|
+
const row = value as Record<string, unknown>;
|
|
13
|
+
if (row.applied === true) return { applied: true };
|
|
14
|
+
if (row.applied !== false || typeof row.reason !== "string" || !Object.hasOwn(APPLY_REASONS, row.reason)
|
|
15
|
+
|| typeof row.retryable !== "boolean") return null;
|
|
16
|
+
return { applied: false, reason: row.reason as CodexDesktopSwitchApplyReason, retryable: row.retryable };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Caller rebuilds the relevant task DTO; settings pending is not a CatalogDisposition. */
|
|
20
|
+
export function printSettingsResult(
|
|
21
|
+
data: Record<string, unknown>, pending: unknown, wantsJson: boolean, lines: readonly string[],
|
|
22
|
+
): number {
|
|
23
|
+
const verified = typeof pending === "boolean";
|
|
24
|
+
const note = !verified ? "Settings were accepted; catalog status is unverified. Read back before retrying."
|
|
25
|
+
: pending ? "Settings saved; catalog refresh is pending."
|
|
26
|
+
: "Settings saved; this response reports no pending catalog refresh.";
|
|
27
|
+
printData({ ...data, catalogRefreshPending: verified ? pending : null,
|
|
28
|
+
...(!verified ? { verification: "unverified" } : {}) }, wantsJson, [...lines, note]);
|
|
29
|
+
return verified && pending === false ? 0 : 1;
|
|
30
|
+
}
|
package/src/cli/storage.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import {
|
|
19
19
|
CliUsageError,
|
|
20
|
+
RuntimeApiError,
|
|
20
21
|
printData,
|
|
21
22
|
rejectArgs,
|
|
22
23
|
runCliAction,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
takeOption,
|
|
28
29
|
type RuntimeApiDeps,
|
|
29
30
|
} from "./runtime-api";
|
|
31
|
+
import type { StorageCleanupPolicy } from "../types/config";
|
|
30
32
|
|
|
31
33
|
const USAGE = `Usage:
|
|
32
34
|
ocx storage report [--json]
|
|
@@ -34,7 +36,8 @@ const USAGE = `Usage:
|
|
|
34
36
|
ocx storage trash [list] [--json]
|
|
35
37
|
ocx storage trash restore <entry-id> [--yes] [--json]
|
|
36
38
|
ocx storage policy [show] [--json]
|
|
37
|
-
ocx storage policy set [--enabled <true|false>] [--
|
|
39
|
+
ocx storage policy set [--enabled <true|false>] [--archived-bytes-over <bytes>]
|
|
40
|
+
[--reduce-to-bytes <bytes> | --remove-oldest-percent <1-100> | --percent <1-100>]
|
|
38
41
|
[--mode <quarantine|permanent>] [--schedule <startup|daily|weekly|manual>] [--json]
|
|
39
42
|
ocx storage policy run [--yes] [--json]
|
|
40
43
|
|
|
@@ -146,6 +149,41 @@ async function trash(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
146
149
|
printData(result, wantsJson, summaryLines(result));
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
/** New policy controls expose only observed settings and job status, never arbitrary API fields. */
|
|
153
|
+
type ObservedPolicy = Omit<StorageCleanupPolicy, "target"> & { target: { reduceToBytes: number } | { removeOldestPercent: number } };
|
|
154
|
+
function policyWriteReceipt(value: unknown): { ok: true; policy: ObservedPolicy; job: { status: "idle" | "running" } } {
|
|
155
|
+
const record = (item: unknown): item is Record<string, unknown> => item !== null && typeof item === "object" && !Array.isArray(item);
|
|
156
|
+
const count = (item: unknown): item is number => typeof item === "number" && Number.isInteger(item) && item >= 0;
|
|
157
|
+
const invalid = (): never => { throw new Error("Invalid storage policy response; inspect the policy before retrying."); };
|
|
158
|
+
if (!record(value) || value.ok !== true || !record(value.policy) || !record(value.job)) return invalid();
|
|
159
|
+
const source = value.policy;
|
|
160
|
+
if (typeof source.enabled !== "boolean" || !record(source.trigger) || !count(source.trigger.archivedBytesOver)
|
|
161
|
+
|| !record(source.target) || (source.mode !== "quarantine" && source.mode !== "permanent")
|
|
162
|
+
|| (source.schedule !== "startup" && source.schedule !== "daily" && source.schedule !== "weekly" && source.schedule !== "manual")
|
|
163
|
+
|| (value.job.status !== "idle" && value.job.status !== "running")) return invalid();
|
|
164
|
+
const { reduceToBytes, removeOldestPercent } = source.target;
|
|
165
|
+
let target: ObservedPolicy["target"];
|
|
166
|
+
if (count(reduceToBytes) && removeOldestPercent === undefined) target = { reduceToBytes };
|
|
167
|
+
else if (count(removeOldestPercent) && removeOldestPercent >= 1 && removeOldestPercent <= 100 && reduceToBytes === undefined) target = { removeOldestPercent };
|
|
168
|
+
else return invalid();
|
|
169
|
+
const policy: ObservedPolicy = {
|
|
170
|
+
enabled: source.enabled,
|
|
171
|
+
trigger: { archivedBytesOver: source.trigger.archivedBytesOver },
|
|
172
|
+
target,
|
|
173
|
+
mode: source.mode,
|
|
174
|
+
schedule: source.schedule,
|
|
175
|
+
};
|
|
176
|
+
if (source.lastRun !== undefined) {
|
|
177
|
+
if (!record(source.lastRun) || !count(source.lastRun.at) || !count(source.lastRun.freedBytes) || !count(source.lastRun.removed)) return invalid();
|
|
178
|
+
policy.lastRun = { at: source.lastRun.at, freedBytes: source.lastRun.freedBytes, removed: source.lastRun.removed };
|
|
179
|
+
}
|
|
180
|
+
if (source.nextRun !== undefined) {
|
|
181
|
+
if (!count(source.nextRun)) return invalid();
|
|
182
|
+
policy.nextRun = source.nextRun;
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, policy, job: { status: value.job.status } };
|
|
185
|
+
}
|
|
186
|
+
|
|
149
187
|
async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
150
188
|
const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "show";
|
|
151
189
|
const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
|
|
@@ -164,6 +202,9 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
164
202
|
const wantsJson = takeFlag(args, "--json");
|
|
165
203
|
const enabled = takeOption(args, "--enabled");
|
|
166
204
|
const percent = takeIntegerOption(args, "--percent", { min: 0 });
|
|
205
|
+
const removeOldestPercent = takeIntegerOption(args, "--remove-oldest-percent", { min: 0 });
|
|
206
|
+
const reduceToBytes = takeIntegerOption(args, "--reduce-to-bytes", { min: 0 });
|
|
207
|
+
const archivedBytesOver = takeIntegerOption(args, "--archived-bytes-over", { min: 0 });
|
|
167
208
|
const mode = takeOption(args, "--mode");
|
|
168
209
|
const schedule = takeOption(args, "--schedule");
|
|
169
210
|
rejectArgs(args, USAGE);
|
|
@@ -171,8 +212,13 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
171
212
|
if (enabled !== undefined && enabled !== "true" && enabled !== "false") {
|
|
172
213
|
throw new CliUsageError("--enabled must be true or false", USAGE);
|
|
173
214
|
}
|
|
215
|
+
if ([percent, removeOldestPercent, reduceToBytes].filter(value => value !== undefined).length > 1) {
|
|
216
|
+
throw new CliUsageError("choose one target: --percent, --remove-oldest-percent or --reduce-to-bytes", USAGE);
|
|
217
|
+
}
|
|
218
|
+
const extended = archivedBytesOver !== undefined || reduceToBytes !== undefined || removeOldestPercent !== undefined;
|
|
174
219
|
const body: Record<string, unknown> = {};
|
|
175
220
|
if (enabled !== undefined) body.enabled = enabled === "true";
|
|
221
|
+
if (archivedBytesOver !== undefined) body.trigger = { archivedBytesOver };
|
|
176
222
|
// The policy target is nested. A top-level `percent` is not part of the PUT contract:
|
|
177
223
|
// `normalizeStorageCleanupPolicy` reads only `target`, so the field was dropped and the
|
|
178
224
|
// previously stored target survived. `--percent 10` on a policy still holding the
|
|
@@ -183,20 +229,44 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
183
229
|
// vocabulary and answers with a named 400, which is a rejected write rather than the
|
|
184
230
|
// silent wrong write this replaces.
|
|
185
231
|
if (percent !== undefined) body.target = { removeOldestPercent: percent };
|
|
232
|
+
if (removeOldestPercent !== undefined) body.target = { removeOldestPercent };
|
|
233
|
+
if (reduceToBytes !== undefined) body.target = { reduceToBytes };
|
|
186
234
|
if (mode !== undefined) body.mode = mode;
|
|
187
235
|
if (schedule !== undefined) body.schedule = schedule;
|
|
188
236
|
if (Object.keys(body).length === 0) {
|
|
189
|
-
throw new CliUsageError("policy set needs at least one
|
|
237
|
+
throw new CliUsageError("policy set needs at least one policy field", USAGE);
|
|
190
238
|
}
|
|
191
239
|
// Values are NOT re-validated here beyond --enabled's shape. The server owns the mode and
|
|
192
240
|
// schedule vocabularies and returns a named 400; duplicating them is a second thing to
|
|
193
241
|
// keep in sync. `enabled` is checked because "--enabled maybe" would otherwise be sent as
|
|
194
242
|
// `false`, which is a wrong write rather than a rejected one.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
243
|
+
let result: unknown;
|
|
244
|
+
try {
|
|
245
|
+
result = await runtimeRequest("/api/storage/cleanup-policy", {
|
|
246
|
+
method: "PUT",
|
|
247
|
+
...(extended ? { redirect: "error" as const } : {}),
|
|
248
|
+
headers: { "content-type": "application/json" },
|
|
249
|
+
body: JSON.stringify(body),
|
|
250
|
+
}, deps);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (!extended) throw error;
|
|
253
|
+
if (error instanceof RuntimeApiError) {
|
|
254
|
+
throw new RuntimeApiError(`Storage policy request failed (${error.status}); inspect the policy before retrying.`, error.status, null);
|
|
255
|
+
}
|
|
256
|
+
throw new Error("Storage policy request failed; inspect the policy before retrying.");
|
|
257
|
+
}
|
|
258
|
+
if (extended) {
|
|
259
|
+
const receipt = policyWriteReceipt(result);
|
|
260
|
+
const saved = receipt.policy;
|
|
261
|
+
printData(receipt, wantsJson, [
|
|
262
|
+
"Policy saved. This command did not start cleanup.",
|
|
263
|
+
`Enabled: ${saved.enabled}`,
|
|
264
|
+
`Trigger: archived bytes over ${saved.trigger.archivedBytesOver}`,
|
|
265
|
+
"reduceToBytes" in saved.target ? `Target: reduce archives to ${saved.target.reduceToBytes} bytes` : `Target: remove oldest ${saved.target.removeOldestPercent}%`,
|
|
266
|
+
`Mode: ${saved.mode}; schedule: ${saved.schedule}; job: ${receipt.job.status}`,
|
|
267
|
+
]);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
200
270
|
printData(result, wantsJson, summaryLines(result));
|
|
201
271
|
return;
|
|
202
272
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
1
2
|
import {
|
|
2
3
|
CliUsageError,
|
|
3
4
|
desktopSwitchApplyReason,
|
|
@@ -14,8 +15,11 @@ import {
|
|
|
14
15
|
|
|
15
16
|
const USAGE = `Usage:
|
|
16
17
|
ocx system [status] [--json]
|
|
18
|
+
ocx system health [--json]
|
|
17
19
|
ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
|
|
18
|
-
[--desktop-authless <on|off>] [--client-compaction <on|off>]
|
|
20
|
+
[--desktop-authless <on|off>] [--client-compaction <on|off>]
|
|
21
|
+
[--show-codex-credits <on|off>] [--account-picker <on|off>] [--main-account-hard-lock <on|off>]
|
|
22
|
+
[--ultra-fast-tier <on|off>] [--fast-rows <on|off>] [--json]
|
|
19
23
|
ocx system startup <health|install-service|install-shim> [--json]
|
|
20
24
|
ocx system diagnostics [--json]
|
|
21
25
|
ocx system sync [--json]
|
|
@@ -45,6 +49,46 @@ async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
45
49
|
printData(result, wantsJson, summaryLines(result));
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
/** The management health observation is distinct from root liveness and aggregate status. */
|
|
53
|
+
async function health(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
54
|
+
return runCatalogAction(async () => {
|
|
55
|
+
const args = [...argv], wantsJson = takeFlag(args, "--json");
|
|
56
|
+
if (args.length) throw new CliUsageError("system health accepts only --json", USAGE);
|
|
57
|
+
const raw = await runtimeRequest("/api/system/health", { method: "GET", redirect: "error" }, deps);
|
|
58
|
+
const value = recordValue(raw), ledger = recordValue(value?.spendLedger);
|
|
59
|
+
const counter = (number: unknown): number => {
|
|
60
|
+
if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0 || number > 1_000_000) {
|
|
61
|
+
throw new Error("Invalid health counter");
|
|
62
|
+
}
|
|
63
|
+
return number;
|
|
64
|
+
};
|
|
65
|
+
if (!value || Array.isArray(raw) || !ledger || Array.isArray(value.spendLedger)
|
|
66
|
+
|| value.status !== "ok" || value.service !== "opencodex"
|
|
67
|
+
|| typeof value.version !== "string" || !value.version.trim()
|
|
68
|
+
|| typeof value.uptime !== "number" || !Number.isFinite(value.uptime) || value.uptime < 0
|
|
69
|
+
|| typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
|
|
70
|
+
|| (ledger.ownership !== "held" && ledger.ownership !== "unheld")
|
|
71
|
+
|| typeof ledger.initialized !== "boolean" || typeof ledger.configured !== "boolean"
|
|
72
|
+
|| typeof ledger.degraded !== "boolean") throw new Error("Invalid system health response");
|
|
73
|
+
const result = {
|
|
74
|
+
status: "ok", service: "opencodex", version: value.version, uptime: value.uptime, pid: value.pid,
|
|
75
|
+
spendLedger: {
|
|
76
|
+
ownership: ledger.ownership, initialized: ledger.initialized, configured: ledger.configured,
|
|
77
|
+
degraded: ledger.degraded, persistFailures: counter(ledger.persistFailures), corruptRecords: counter(ledger.corruptRecords),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
printData(result, wantsJson, [
|
|
81
|
+
`Management endpoint: ${result.status} (${result.service} ${result.version}, PID ${result.pid})`,
|
|
82
|
+
`Uptime: ${result.uptime} seconds`,
|
|
83
|
+
`Spend ledger: ${ledger.degraded ? "degraded" : "no degradation reported"}; ownership ${ledger.ownership}`,
|
|
84
|
+
`Ledger initialized: ${ledger.initialized}; configured: ${ledger.configured}`,
|
|
85
|
+
`Ledger persistence failures: ${result.spendLedger.persistFailures}; corrupt records: ${result.spendLedger.corruptRecords}`,
|
|
86
|
+
"This observation does not certify every subsystem healthy.",
|
|
87
|
+
...(ledger.degraded ? ["Next: inspect the proxy diagnostics and spend-ledger configuration before relying on spend limits."] : []),
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
48
92
|
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
49
93
|
return value !== null && typeof value === "object" ? value as Record<string, unknown> : undefined;
|
|
50
94
|
}
|
|
@@ -187,11 +231,25 @@ async function update(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
187
231
|
if (!yes) throw new CliUsageError("update run requires --yes", USAGE);
|
|
188
232
|
rejectArgs(args, USAGE);
|
|
189
233
|
const result = await runtimeRequest("/api/update/run", { method: "POST", body: JSON.stringify({ tag: channel, restart }) }, deps);
|
|
190
|
-
|
|
234
|
+
const job = recordValue(recordValue(result)?.job);
|
|
235
|
+
const jobId = typeof job?.id === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(job.id) ? job.id : undefined;
|
|
236
|
+
const jobStatus = ["running", "restarting", "succeeded", "failed"].find(state => state === job?.status);
|
|
237
|
+
const lines = jobId ? [
|
|
238
|
+
`Update job ${jobId}: ${jobStatus ?? "status unconfirmed"} (${channel}).`,
|
|
239
|
+
`Check progress: ocx system update status ${jobId}`,
|
|
240
|
+
] : ["Update request returned no usable job ID. Check: ocx system update check."];
|
|
241
|
+
printData(result, wantsJson, lines);
|
|
191
242
|
}
|
|
192
243
|
|
|
193
244
|
export async function handleSystemCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
194
245
|
const [sub = "status", ...rest] = argv;
|
|
246
|
+
if (sub === "health") return health(rest, deps);
|
|
247
|
+
if (sub === "settings") {
|
|
248
|
+
const { SYSTEM_PARITY_OPTIONS, handleSystemSettingsParity } = await import("./system-settings-parity");
|
|
249
|
+
if (rest.some(arg => SYSTEM_PARITY_OPTIONS.some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
|
|
250
|
+
return handleSystemSettingsParity(rest, deps);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
195
253
|
if (sub === "codex-cli-update") {
|
|
196
254
|
const { handleCodexCliUpdateCommand } = await import("./codex-cli-update");
|
|
197
255
|
return await handleCodexCliUpdateCommand(rest);
|