@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/registry.ts
CHANGED
|
@@ -144,18 +144,19 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
144
144
|
"Port: ocx link port [--json]",
|
|
145
145
|
"Issue: ocx link issue --alias <alias> --tunnel-port <port> [--json]",
|
|
146
146
|
"Status: ocx link status [--json]",
|
|
147
|
-
"Revoke: ocx link revoke --link-id <id> [--json]",
|
|
147
|
+
"Revoke: ocx link revoke --link-id <id> [--force --yes] [--json]",
|
|
148
148
|
"Issue, status, and revoke use the running proxy's loopback management API and admin token.",
|
|
149
149
|
],
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
name: "remote-workspace",
|
|
153
|
-
usage: "ocx remote-workspace <pair|agent|status>",
|
|
154
|
-
summary: "
|
|
153
|
+
usage: "ocx remote-workspace <pair|agent|status|hub>",
|
|
154
|
+
summary: "Manage this workspace executor or inspect the selected Hub.",
|
|
155
155
|
details: [
|
|
156
156
|
"Pair: ocx remote-workspace pair <hub-url> --pairing-code-stdin --root <absolute-path> [--toolchain-root <absolute-directory>] [--executor-helper <absolute-file>] [--name <device-name>]",
|
|
157
157
|
"Agent: ocx remote-workspace agent",
|
|
158
158
|
"Status: ocx remote-workspace status [--json]",
|
|
159
|
+
"Hub: ocx remote-workspace hub <status|runtimes|sessions> [--json]",
|
|
159
160
|
"Codex, Claude Code, Pi, provider logins, and API keys remain on the hub; only workspace tools execute here.",
|
|
160
161
|
],
|
|
161
162
|
},
|
|
@@ -282,10 +283,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
282
283
|
" list List configured and available providers",
|
|
283
284
|
" add <name> Add a provider (registry or custom)",
|
|
284
285
|
" edit <name> Edit live provider fields",
|
|
286
|
+
" pacing <name> Read/edit live request pacing rules",
|
|
287
|
+
" snapshot Read a non-secret provider editor baseline",
|
|
288
|
+
" apply Apply edited providers with baseline conflict checks",
|
|
285
289
|
" test <name> Test the provider's upstream model endpoint",
|
|
286
290
|
" remove <name> Remove a configured provider",
|
|
287
291
|
" show <name> Show provider config details",
|
|
288
292
|
" set-default <name> Change the default provider",
|
|
293
|
+
" add/remove/set-default support --live; live removal also requires --yes.",
|
|
289
294
|
" selected <name> Show or set the provider model allowlist",
|
|
290
295
|
" quota Show provider quota reports",
|
|
291
296
|
" resets Show recently detected quota resets",
|
|
@@ -299,6 +304,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
299
304
|
" ocx provider add anthropic --api-key <key>",
|
|
300
305
|
" ocx provider add my-ollama --adapter openai-chat --base-url http://localhost:11434/v1",
|
|
301
306
|
" ocx provider show anthropic --json",
|
|
307
|
+
" ocx provider pacing anthropic --json",
|
|
308
|
+
" ocx provider snapshot --json",
|
|
302
309
|
" ocx provider set-default anthropic",
|
|
303
310
|
" ocx provider edit xai --xai-chat on # opt Grok 4.5/4.6 into Chat Completions",
|
|
304
311
|
" ocx provider edit xai --xai-chat off # use Responses again",
|
|
@@ -320,6 +327,11 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
320
327
|
"priority <provider> <id|main> [first|earlier|normal|later|last|-100..100|reset] Selection order; omit the value to read it.",
|
|
321
328
|
"remove <provider> <id> --yes Remove a stored account or key after an existence check.",
|
|
322
329
|
"add-key <provider> [--label <label>] Add a key read only from piped stdin.",
|
|
330
|
+
"pool <provider> Read/edit runtime-supported pool policy.",
|
|
331
|
+
"credits openai ... Explicit paid-credit-after-limit policy; separate from displaying balances.",
|
|
332
|
+
"quota-activation openai Enable/disable one account reset-window refresh policy.",
|
|
333
|
+
"anthropic-reset-grants Read grant status only; consumption remains in the dashboard.",
|
|
334
|
+
"login supports explicit browser/add-account options only for applicable flows; human verification remains required.",
|
|
323
335
|
"login/reauth/code/cancel Run browser or manual-code auth from a headless shell.",
|
|
324
336
|
"reset-credits <id|main> [--consume --yes] Inspect or consume Codex reset credits.",
|
|
325
337
|
"grok-reset-coupons [<id>] [--consume --yes] Inspect or redeem Grok reset coupons.",
|
|
@@ -340,6 +352,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
340
352
|
summary: "List models and manage custom (manually registered) models.",
|
|
341
353
|
details: [
|
|
342
354
|
"List available models from static config with no subcommand (liveModels may add more at runtime).",
|
|
355
|
+
"add/remove support --live for the selected proxy; live or JSON removal requires --yes.",
|
|
356
|
+
"Local add/remove JSON reports saved state and opportunistic sync; no proxy keeps the save successful/pending.",
|
|
357
|
+
"display-name <provider/raw-model> --set <label> or --clear changes an exact upstream model label.",
|
|
358
|
+
"order status/set/reset manages routed picker order; reset native-inclusive saved order before replacement.",
|
|
343
359
|
"add: register a model the provider catalog does not advertise yet.",
|
|
344
360
|
" --display-name <name> Human label (no slashes).",
|
|
345
361
|
" --context-window <tokens> e.g. 200000.",
|
|
@@ -357,12 +373,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
357
373
|
},
|
|
358
374
|
{
|
|
359
375
|
name: "companion",
|
|
360
|
-
usage: "ocx companion <show|set|reset> ...",
|
|
361
|
-
summary: "Inspect
|
|
376
|
+
usage: "ocx companion <show|set|reset|usage|timeline> ...",
|
|
377
|
+
summary: "Inspect companion settings, filtered usage and timeline, or change preferences.",
|
|
362
378
|
details: [
|
|
363
379
|
"ocx companion and ocx companion show read settings; use --json for machine-readable output.",
|
|
364
380
|
"ocx companion set accepts one or more key=value assignments; values are parsed as JSON when possible.",
|
|
365
381
|
"ocx companion reset restores the default settings.",
|
|
382
|
+
"ocx companion usage reads today and 30-day totals with saved model/provider filters; unavailable ranges return partial output and exit 1.",
|
|
383
|
+
"ocx companion timeline reads buckets; --model selects models and --hide-provider excludes providers.",
|
|
366
384
|
],
|
|
367
385
|
},
|
|
368
386
|
{
|
|
@@ -373,8 +391,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
373
391
|
},
|
|
374
392
|
{
|
|
375
393
|
name: "route",
|
|
376
|
-
usage: "ocx route combo <
|
|
377
|
-
summary: "Manage
|
|
394
|
+
usage: "ocx route <combo|policy> <subcommand>",
|
|
395
|
+
summary: "Manage combo routing and routing profiles.",
|
|
378
396
|
},
|
|
379
397
|
{
|
|
380
398
|
name: "effort",
|
|
@@ -407,10 +425,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
407
425
|
"`inspect star` reads the repository star status only. Starring uses your GitHub identity and is available from the dashboard alone.",
|
|
408
426
|
],
|
|
409
427
|
},
|
|
410
|
-
{ name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Read or follow request logs
|
|
428
|
+
{ name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl|--events]", summary: "Read or follow request logs." },
|
|
411
429
|
{
|
|
412
430
|
name: "usage",
|
|
413
|
-
usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--json]",
|
|
431
|
+
usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--json]",
|
|
414
432
|
summary: "Report token usage and estimated cost (alias of ocx observe usage).",
|
|
415
433
|
},
|
|
416
434
|
{
|
|
@@ -425,14 +443,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
425
443
|
{ name: "memory", usage: "ocx memory [--json]", summary: "Alias of ocx observe memory." },
|
|
426
444
|
{
|
|
427
445
|
name: "access",
|
|
428
|
-
usage: "ocx access <key|endpoints|models|test> ...",
|
|
446
|
+
usage: "ocx access <key|endpoints|models|test|audio> ...",
|
|
429
447
|
summary: "Manage OpenCodex admission API keys and inspect external endpoints.",
|
|
430
448
|
details: [
|
|
431
449
|
"Key rotation start uses POST /api/keys/rotate and returns the replacement secret once.",
|
|
432
450
|
"Commit uses POST /api/keys/rotate/commit; abort uses DELETE /api/keys/rotate with the returned rotation id.",
|
|
451
|
+
"key rename changes a masked key's name without clearing its scopes.",
|
|
452
|
+
"test --api-key-stdin uses one supplied data key; it never substitutes management or enrolled credentials.",
|
|
453
|
+
"audio transcribe/live-check use explicit piped key input and may use provider quota; authorize the operation first.",
|
|
433
454
|
],
|
|
434
455
|
},
|
|
435
|
-
{ name: "api-key", usage: "ocx api-key <list|create|rotate|remove> ...", summary: "Alias of ocx access key." },
|
|
456
|
+
{ name: "api-key", usage: "ocx api-key <list|get|create|set|rename|rotate|remove> ...", summary: "Alias of ocx access key." },
|
|
436
457
|
{
|
|
437
458
|
name: "api",
|
|
438
459
|
usage: "ocx api <protocols|explain|policy> ...",
|
|
@@ -468,14 +489,18 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
468
489
|
summary: "Manage supported client integrations, and the native client toggles.",
|
|
469
490
|
details: [
|
|
470
491
|
"`native` shows or flips the native Claude/Claude Desktop/Codex/Grok toggles; the other subcommands manage the reversible file integrations.",
|
|
492
|
+
"client preview shows a proposed change; enable/disable/restore accept an optional --plan-fingerprint.",
|
|
493
|
+
"client history remove requires --yes; client sync --client aside refreshes managed Aside profiles.",
|
|
494
|
+
"native cursor status/local-installer are reads; installer lookup does not install anything.",
|
|
471
495
|
],
|
|
472
496
|
},
|
|
473
497
|
{
|
|
474
498
|
name: "system",
|
|
475
|
-
usage: "ocx system <status|settings|startup|diagnostics|sync|codex-app-server|codex-restart|update|codex-cli-update> ...",
|
|
499
|
+
usage: "ocx system <status|health|settings|startup|diagnostics|sync|codex-app-server|codex-restart|update|codex-cli-update> ...",
|
|
476
500
|
summary: "Manage headless runtime settings, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection.",
|
|
477
501
|
details: [
|
|
478
502
|
"system update manages OpenCodex itself.",
|
|
503
|
+
"system health reads authenticated service and spend-ledger diagnostics; root health remains liveness.",
|
|
479
504
|
"ocx system codex-cli-update check [--json]",
|
|
480
505
|
"ocx system codex-cli-update attest [--json]",
|
|
481
506
|
"ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]",
|
|
@@ -484,9 +509,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
484
509
|
},
|
|
485
510
|
{
|
|
486
511
|
name: "config",
|
|
487
|
-
usage: "ocx config
|
|
512
|
+
usage: "ocx config [show|get|set|unset|validate|export|import] ...",
|
|
488
513
|
summary: "Inspect and safely modify validated OpenCodex configuration.",
|
|
489
|
-
details: [
|
|
514
|
+
details: [
|
|
515
|
+
"show is the default: ocx config [show] [--json] [--source]. Flags alone also display the local configuration without a running proxy.",
|
|
516
|
+
"--json may precede an explicit action without changing which action runs.",
|
|
517
|
+
"--source is only supported for show and includes diagnostic source, error, and warning fields.",
|
|
518
|
+
"Secrets are masked by show/get. Import requires --yes and validates before writing.",
|
|
519
|
+
],
|
|
490
520
|
},
|
|
491
521
|
{
|
|
492
522
|
name: "claude",
|
|
@@ -511,6 +541,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
511
541
|
" ocx claude desktop default <family> <route|none>",
|
|
512
542
|
" ocx claude desktop export <path|-> Export versioned JSON (`-` = stdout)",
|
|
513
543
|
" ocx claude desktop import <path> [--apply] Validate and import JSON",
|
|
544
|
+
" ocx claude desktop profile show [--json] Read the running proxy's profile",
|
|
545
|
+
" ocx claude desktop profile import <file|-> [--json] Save the running profile without applying",
|
|
514
546
|
"Families: opus, fable, sonnet, haiku. New routes start in opus.",
|
|
515
547
|
"`none` is valid only when that family is empty.",
|
|
516
548
|
"Legacy apply flags remain supported: --static, --hybrid, --discovery-only.",
|
|
@@ -527,7 +559,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
527
559
|
"and `providers.opencodex` blocks injected through OpenCode's inline runtime layer",
|
|
528
560
|
"(`OPENCODE_CONFIG_CONTENT`). Any existing inline config in the environment is preserved",
|
|
529
561
|
"and only `provider.opencodex` and `providers.opencodex` are overwritten for this launch.",
|
|
530
|
-
"
|
|
562
|
+
"V1 receives reasoning-effort variant maps; V2 receives native variant arrays for the same declared choices and defaults.",
|
|
531
563
|
"Global/project opencode.json may be read to warn about an existing provider.opencodex or",
|
|
532
564
|
"providers.opencodex override; on-disk files are never modified.",
|
|
533
565
|
"Routed models appear in the model picker as opencodex/<provider>/<model>.",
|
|
@@ -560,8 +592,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
560
592
|
summary: "Connect ZCode (Z.ai desktop client) to the proxy via its managed provider.",
|
|
561
593
|
details: [
|
|
562
594
|
"Alias of ocx integration client <sub> --client zcode.",
|
|
563
|
-
"enable writes the
|
|
564
|
-
"
|
|
595
|
+
"enable manages ~/.zcode/v2/provider_config.json with schemaVersion 1; if the store is absent, it writes the legacy provider.opencodex block into ~/.zcode/v2/config.json.",
|
|
596
|
+
"A surviving owned legacy block prevents switching stores. Run `ocx zcode disable` before enabling the current store. Unknown store schemas are refused.",
|
|
565
597
|
"ZCode reads its config at startup — restart ZCode after enable/disable.",
|
|
566
598
|
"Select OpenCodex Proxy/<provider>/<model> from ZCode's model picker.",
|
|
567
599
|
],
|
|
@@ -569,7 +601,13 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
569
601
|
{
|
|
570
602
|
name: "restart",
|
|
571
603
|
usage: "ocx restart",
|
|
572
|
-
summary: "
|
|
604
|
+
summary: "Request a graceful restart of the verified running proxy, preserving routing.",
|
|
605
|
+
details: [
|
|
606
|
+
"If no proxy is running, use the normal `ocx ensure` start policy, including the Codex autostart setting.",
|
|
607
|
+
"Unverifiable running targets are refused without a stop/start fallback.",
|
|
608
|
+
"Restarting an older proxy from this newer CLI requires standalone macOS/Linux proxies; supervised, shared, foreground, or unverifiable targets are refused.",
|
|
609
|
+
"For a service-managed update, run `ocx service restart`; for a desktop-managed update, restart through the OpenCodex desktop app.",
|
|
610
|
+
],
|
|
573
611
|
},
|
|
574
612
|
{
|
|
575
613
|
name: "v2",
|
|
@@ -628,8 +666,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
628
666
|
},
|
|
629
667
|
{
|
|
630
668
|
name: "lab",
|
|
631
|
-
usage: "ocx lab <status|verdicts|subjects|subject|observations|events|event|artifacts|artifact|catalog> [options] [--json]",
|
|
632
|
-
summary: "
|
|
669
|
+
usage: "ocx lab <status|verdicts|subjects|subject|observations|events|event|artifacts|artifact|catalog|production-signals|public|automation|run> [options] [--json]",
|
|
670
|
+
summary: "Inspect local Compatibility Lab evidence and explicitly control automation.",
|
|
633
671
|
details: [
|
|
634
672
|
"status Projection availability, schema versions, and row counts.",
|
|
635
673
|
"verdicts Paginated derived compatibility verdicts with filters.",
|
|
@@ -638,6 +676,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
638
676
|
"events Event history; event <id> returns one safe typed event.",
|
|
639
677
|
"artifacts Artifact metadata only (no content download).",
|
|
640
678
|
"catalog Packaged protocol/live scenario catalog metadata.",
|
|
679
|
+
"production-signals Local passive routing evidence for a subject.",
|
|
680
|
+
"public Preview/export local evidence; verify/import a bundle.",
|
|
681
|
+
"automation Read status/runs or explicitly enable/disable local policy.",
|
|
682
|
+
"run Explicit manual scenario; live probes can consume quota.",
|
|
641
683
|
"Reads never rebuild the projection, trigger probes, or require the proxy.",
|
|
642
684
|
],
|
|
643
685
|
},
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { REMOTE_WORKSPACE_CAPABILITIES, isRemoteWorkspaceToolName } from "../remote-control/workspace-tools";
|
|
3
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
4
|
+
import { CliUsageError, RuntimeApiError, printData, rejectArgs, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
|
|
5
|
+
|
|
6
|
+
export const REMOTE_WORKSPACE_HUB_USAGE = "Usage: ocx remote-workspace hub [status|runtimes|sessions] [--json]";
|
|
7
|
+
const UNAVAILABLE = "Remote Workspace requires Hub mode and OCX_REMOTE_WORKSPACE_ENABLED=1. Run this command on the intended Hub.";
|
|
8
|
+
const identity = z.string().min(1);
|
|
9
|
+
const capabilities = z.array(z.enum(REMOTE_WORKSPACE_CAPABILITIES));
|
|
10
|
+
const root = z.object({ id: identity, label: z.string() });
|
|
11
|
+
const device = z.object({
|
|
12
|
+
id: identity, name: z.string(), platform: z.string(), capabilities, roots: z.array(root),
|
|
13
|
+
online: z.boolean(), createdAt: z.string(), lastSeenAt: z.string().nullable(),
|
|
14
|
+
});
|
|
15
|
+
const availability = z.object({ available: z.boolean(), version: z.string().optional(), reason: z.string().optional() });
|
|
16
|
+
const runtimes = z.object({ codex: availability, claude: availability, pi: availability });
|
|
17
|
+
const event = z.object({ sequence: z.number().int().nonnegative(), at: z.string(),
|
|
18
|
+
type: z.enum(["status", "assistant", "tool", "error"]), text: z.string() });
|
|
19
|
+
const session = z.object({
|
|
20
|
+
id: identity, profile: z.enum(["codex", "claude", "pi"]), accessMode: z.enum(["read-only", "workspace"]),
|
|
21
|
+
deviceId: identity, deviceName: z.string(), rootId: identity, rootLabel: z.string(), capabilities,
|
|
22
|
+
tools: z.array(z.custom<string>(isRemoteWorkspaceToolName)), threadId: z.string().nullable(),
|
|
23
|
+
resumable: z.boolean(), status: z.enum(["starting", "ready", "running", "waiting_for_executor", "failed", "stopped"]),
|
|
24
|
+
createdAt: z.string(), updatedAt: z.string(), events: z.array(event),
|
|
25
|
+
});
|
|
26
|
+
const sessions = z.array(session);
|
|
27
|
+
const status = z.object({ available: z.literal(true), devices: z.array(device), runtimes, sessions });
|
|
28
|
+
const unavailable = z.object({ available: z.literal(false), reason: z.string(),
|
|
29
|
+
devices: z.array(z.unknown()).length(0), runtimes: z.object({}).strict(), sessions: z.array(z.unknown()).length(0) });
|
|
30
|
+
|
|
31
|
+
/** Hub observations use management admission; executor pairing and session writes remain separate. */
|
|
32
|
+
export async function handleRemoteWorkspaceHubCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
33
|
+
return runCatalogAction(async () => {
|
|
34
|
+
const args = [...argv];
|
|
35
|
+
const wantsJson = takeFlag(args, "--json");
|
|
36
|
+
const action = args.shift() ?? "status";
|
|
37
|
+
rejectArgs(args, REMOTE_WORKSPACE_HUB_USAGE);
|
|
38
|
+
if (!["status", "runtimes", "sessions"].includes(action)) {
|
|
39
|
+
throw new CliUsageError("Expected Hub status, runtimes or sessions", REMOTE_WORKSPACE_HUB_USAGE);
|
|
40
|
+
}
|
|
41
|
+
const path = `/api/remote-workspace${action === "status" ? "" : `/${action}`}`;
|
|
42
|
+
let raw: unknown;
|
|
43
|
+
try {
|
|
44
|
+
raw = await runtimeRequest(path, { redirect: "error" }, deps);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// The inner owner can refuse after activation changed; the outer owner uses a 200 observation.
|
|
47
|
+
if (error instanceof RuntimeApiError && error.status === 409) {
|
|
48
|
+
console.error(`Error: ${UNAVAILABLE}`);
|
|
49
|
+
return 5;
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (unavailable.safeParse(raw).success) {
|
|
54
|
+
printData({ available: false, reason: UNAVAILABLE, devices: [], runtimes: {}, sessions: [] }, wantsJson, [UNAVAILABLE]);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
if (action === "status") {
|
|
58
|
+
const parsed = status.safeParse(raw);
|
|
59
|
+
if (!parsed.success) throw new Error("Invalid Hub observation");
|
|
60
|
+
const data = parsed.data;
|
|
61
|
+
printData(data, wantsJson, [
|
|
62
|
+
`Remote Workspace Hub: ${data.devices.length} device(s), ${data.sessions.length} session(s).`,
|
|
63
|
+
...data.devices.map(row => `${row.id} ${row.name}: ${row.online ? "online" : "offline"} (${row.roots.length} workspace root(s))`),
|
|
64
|
+
...(data.devices.length ? [] : ["No paired devices. Pair a device from the Hub dashboard."]),
|
|
65
|
+
...Object.entries(data.runtimes).map(([name, row]) => `${name}: ${row.available ? "available" : "unavailable"}${row.version ? ` (${row.version})` : ""}`),
|
|
66
|
+
]);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (action === "runtimes") {
|
|
70
|
+
const parsed = z.object({ runtimes }).safeParse(raw);
|
|
71
|
+
if (!parsed.success) throw new Error("Invalid runtime observation");
|
|
72
|
+
printData(parsed.data, wantsJson, Object.entries(parsed.data.runtimes).map(([name, row]) =>
|
|
73
|
+
`${name}: ${row.available ? "available" : "unavailable"}${row.version ? ` (${row.version})` : ""}${row.reason ? ` — ${row.reason}` : ""}`));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const parsed = z.object({ sessions }).safeParse(raw);
|
|
77
|
+
if (!parsed.success) throw new Error("Invalid session observation");
|
|
78
|
+
printData(parsed.data, wantsJson, parsed.data.sessions.length
|
|
79
|
+
? parsed.data.sessions.map(row => `${row.id} ${row.profile}: ${row.status} (${row.deviceName} / ${row.rootLabel}, ${row.accessMode})`)
|
|
80
|
+
: ["No Remote Workspace sessions. Create a session from the Hub dashboard."]);
|
|
81
|
+
return 0;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -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
|
@@ -15,6 +15,7 @@ import { parseStopApproval } from "./stop-approval";
|
|
|
15
15
|
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
|
|
16
16
|
import { findCommand } from "./registry";
|
|
17
17
|
import { printUnknownCommand } from "./help-recovery";
|
|
18
|
+
import { noteCredentialArgv, redactSecretArgs } from "./secret-args";
|
|
18
19
|
|
|
19
20
|
export interface CliHead {
|
|
20
21
|
kind: "version" | "help" | "ready" | "resolve" | "command";
|
|
@@ -78,7 +79,13 @@ export function parseCliHead(argv: string[]): CliHead {
|
|
|
78
79
|
return { kind: "command", command, args };
|
|
79
80
|
}
|
|
80
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
|
+
|
|
81
87
|
export async function runCli(argv: string[]): Promise<CliHead> {
|
|
88
|
+
noteCredentialArgv(argv);
|
|
82
89
|
const head = parseCliHead(argv);
|
|
83
90
|
switch (head.kind) {
|
|
84
91
|
case "version":
|
|
@@ -129,6 +136,11 @@ export async function runCli(argv: string[]): Promise<CliHead> {
|
|
|
129
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>]]");
|
|
130
137
|
process.exit(64);
|
|
131
138
|
}
|
|
139
|
+
const uninstallError = uninstallArgsError(head.command, head.args);
|
|
140
|
+
if (uninstallError) {
|
|
141
|
+
console.error(uninstallError);
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
132
144
|
maybeAutoRestoreCodexShim(head.command, head.args);
|
|
133
145
|
return head;
|
|
134
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);
|