@bitkyc08/opencodex 2.83.0 → 2.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
package/src/cli/account.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
cmdRoutes,
|
|
22
22
|
cmdStrategy,
|
|
23
23
|
} from "./account-extended";
|
|
24
|
-
import { apiError, apiJson, classifyAccount, fetchRows, proxyUnreachable, resolveBaseUrl, type AccountDeps, type AccountRow, type AccountType, type ApiResult }
|
|
24
|
+
import { accountClassificationConfig, apiError, apiJson, classifyAccount, fetchRows, proxyUnreachable, resolveBaseUrl, type AccountDeps, type AccountRow, type AccountType, type ApiResult }
|
|
25
25
|
from "./account-api";
|
|
26
26
|
|
|
27
27
|
export { classifyAccount } from "./account-api";
|
|
@@ -199,7 +199,12 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
199
199
|
console.error(ACCOUNT_USAGE);
|
|
200
200
|
return 1;
|
|
201
201
|
}
|
|
202
|
-
const
|
|
202
|
+
const classification = await accountClassificationConfig(deps, deps.loadConfigImpl?.() ?? loadConfig());
|
|
203
|
+
if ("error" in classification) {
|
|
204
|
+
console.error(`Error: ${classification.error}`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
const config = classification.config;
|
|
203
208
|
const baseUrl = await resolveBaseUrl(deps);
|
|
204
209
|
if (!baseUrl) return proxyUnreachable();
|
|
205
210
|
|
|
@@ -289,7 +294,12 @@ async function cmdCurrent(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
289
294
|
console.error(ACCOUNT_USAGE);
|
|
290
295
|
return 1;
|
|
291
296
|
}
|
|
292
|
-
const
|
|
297
|
+
const classification = await accountClassificationConfig(deps, deps.loadConfigImpl?.() ?? loadConfig());
|
|
298
|
+
if ("error" in classification) {
|
|
299
|
+
console.error(`Error: ${classification.error}`);
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
const config = classification.config;
|
|
293
303
|
const c = classifyAccount(config, name);
|
|
294
304
|
if ("error" in c) {
|
|
295
305
|
console.error(`Error: ${c.error}. Known candidates: ${candidateNames(config)}`);
|
|
@@ -332,7 +342,20 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
332
342
|
console.error(ACCOUNT_USAGE);
|
|
333
343
|
return 1;
|
|
334
344
|
}
|
|
335
|
-
|
|
345
|
+
// #6957: an account change on a link Child is refused before any Home read.
|
|
346
|
+
if (!deps.baseUrl) {
|
|
347
|
+
const refusal = (await import("./link-child")).linkChildProviderChangeRefusal("ocx account use");
|
|
348
|
+
if (refusal) {
|
|
349
|
+
console.error(`Error: ${refusal}`);
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const classification = await accountClassificationConfig(deps, deps.loadConfigImpl?.() ?? loadConfig());
|
|
354
|
+
if ("error" in classification) {
|
|
355
|
+
console.error(`Error: ${classification.error}`);
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
const config = classification.config;
|
|
336
359
|
const c = classifyAccount(config, name);
|
|
337
360
|
if ("error" in c) {
|
|
338
361
|
console.error(`Error: ${c.error}. Known candidates: ${candidateNames(config)}`);
|
|
@@ -392,7 +415,20 @@ async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
|
|
|
392
415
|
console.error(ACCOUNT_USAGE);
|
|
393
416
|
return 1;
|
|
394
417
|
}
|
|
395
|
-
|
|
418
|
+
// #6957: an account change on a link Child is refused before any Home read.
|
|
419
|
+
if (!deps.baseUrl) {
|
|
420
|
+
const refusal = (await import("./link-child")).linkChildProviderChangeRefusal("ocx account clear");
|
|
421
|
+
if (refusal) {
|
|
422
|
+
console.error(`Error: ${refusal}`);
|
|
423
|
+
return 1;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const classification = await accountClassificationConfig(deps, deps.loadConfigImpl?.() ?? loadConfig());
|
|
427
|
+
if ("error" in classification) {
|
|
428
|
+
console.error(`Error: ${classification.error}`);
|
|
429
|
+
return 1;
|
|
430
|
+
}
|
|
431
|
+
const config = classification.config;
|
|
396
432
|
const c = classifyAccount(config, name);
|
|
397
433
|
if ("error" in c) {
|
|
398
434
|
console.error(`Error: ${c.error}. Known candidates: ${candidateNames(config)}`);
|
|
@@ -2,6 +2,14 @@ import type { Capability } from "./capability-types";
|
|
|
2
2
|
|
|
3
3
|
/** Verified existing handlers only; local operations never claim management routes. */
|
|
4
4
|
export const ACCESS_REMOTE_CAPABILITIES: readonly Capability[] = [
|
|
5
|
+
{
|
|
6
|
+
command: ["hub", "disable"], summary: "Return a hub to standalone in one validated config write.",
|
|
7
|
+
usage: "ocx hub disable [--revoke-clients --client-key <id> ...] [--json]",
|
|
8
|
+
routes: [],
|
|
9
|
+
flags: [{ name: "--revoke-clients", value: "boolean", summary: "Revoke only explicitly selected saved client key IDs." }, { name: "--client-key", value: "string", summary: "Exact saved key ID to revoke; repeatable, requires --revoke-clients." }, { name: "--json", value: "boolean", summary: "Emit the result and restart guidance as JSON." }],
|
|
10
|
+
mutates: true, json: "envelope",
|
|
11
|
+
details: ["Removes hub, unauthenticatedLoopbackListener, runtimeRole and hostname atomically after validation; non-hub roles refuse without changes.", "Existing keys have no verified client origin. Names never select keys; --revoke-clients requires exact --client-key IDs when keys exist. Unselected keys remain.", "Prints only safe key metadata, never plaintext keys or pending rotations. Restart advice is informational; the running proxy and external publishing are unchanged."],
|
|
12
|
+
},
|
|
5
13
|
{
|
|
6
14
|
command: ["access", "key"], summary: "Manage API access keys and inspect masked metadata.",
|
|
7
15
|
usage: "ocx access key list [--json]",
|
|
@@ -26,6 +26,8 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
26
26
|
bannerLines: ["ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)"],
|
|
27
27
|
details: ["Default off; launch requires chatgptDesktop.appServerShim: true. Restore removes the generated launcher."],
|
|
28
28
|
},
|
|
29
|
+
{"command": ["link","probe"],"summary": "Probe an SSH host fingerprint and OpenCodex version without trusting it.","routes": [{"method": "POST","path": "/api/link/probe"}],"flags": [{"name": "--json","value": "boolean","summary": "Emit alias, fingerprint, key type and version as JSON."}],"mutates": true,"json": "payload","usage": "ocx link probe <alias> [--json]","details": ["Requires loopback admin authority on the Home. Stores only a temporary pending offer; does not persist host trust or issue a link key."]},
|
|
30
|
+
{"command": ["link","add"],"summary": "Connect an SSH Child to this Home after confirming its fingerprint.","routes": [{"method": "POST","path": "/api/link/probe"},{"method": "POST","path": "/api/link/confirm-host"},{"method": "POST","path": "/api/link/apply"}],"flags": [{"name": "--expect-fingerprint","value": "string","summary": "Exact SHA256 host fingerprint; required without an interactive TTY."},{"name": "--json","value": "boolean","summary": "Emit the verified link result as JSON."}],"mutates": true,"json": "payload","usage": "ocx link add <alias> [--expect-fingerprint SHA256:…] [--json]","details": ["Requires loopback admin authority on the Home. Without a fingerprint, requires a TTY and explicit yes after showing the offered fingerprint. Never prints the link key.","Preflights bound Child restart support and curl, connects, restarts once and verifies a fresh authenticated catalog relay before success. Post-connect failures can leave Child config; run ocx disconnect and ocx restart there before retrying."]},
|
|
29
31
|
{
|
|
30
32
|
command: ["link","port"],
|
|
31
33
|
summary: "Allocate a free loopback port for a remote home link.",
|
|
@@ -65,6 +67,16 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
65
67
|
details: ["Requires a valid link ID and loopback admin authority. Explicit force additionally requires --yes.","Ordinary revoke always emits {linkId} JSON; forced revoke has human output or a JSON cleanup receipt. Already-revoked link_not_found is idempotent success.","Ordinary revoke retains its no-body and idempotent behavior. Forced success reports remoteCleanup:skipped from invocation intent; already-missing link reports unverified. Neither proves the remote client disconnected.","If forced cleanup was needed, run ocx disconnect on the remote client. No new enrollment, SSH trust or credential action is implied."],
|
|
66
68
|
usage: "ocx link revoke --link-id <id> [--force --yes] [--json]",
|
|
67
69
|
},
|
|
70
|
+
{
|
|
71
|
+
command: ["link","reconnect"],
|
|
72
|
+
summary: "Restart a Home-initiated link's SSH tunnel now.",
|
|
73
|
+
routes: [{"method":"POST","path":"/api/link/{id}/reconnect"}],
|
|
74
|
+
flags: [{"name":"--link-id","value":"string","required":true,"summary":"Home-initiated link id whose tunnel to restart."},{"name":"--json","value":"boolean","summary":"Emit the restarted link id as JSON."}],
|
|
75
|
+
mutates: true,
|
|
76
|
+
json: "envelope",
|
|
77
|
+
usage: "ocx link reconnect --link-id <id> [--json]",
|
|
78
|
+
details: ["Requires a valid link ID and loopback admin authority. Stops the current ssh child, clears any backoff or failed state and starts a fresh attempt; read ocx link status for the outcome.","A Child-initiated link is refused with link_not_home_initiated: its own computer owns and retries that tunnel.","Always emits {linkId} JSON."],
|
|
79
|
+
},
|
|
68
80
|
{
|
|
69
81
|
command: ["remote-workspace","pair"],
|
|
70
82
|
summary: "Enroll this executor with one Hub using a one-time code from stdin and locally approved roots.",
|
|
@@ -717,7 +729,7 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
717
729
|
flags: [{"name":"--json","value":"boolean","summary":"Emit the role list, the write result or the proposals as JSON."},{"name":"--effort","value":"string","summary":"set: also write the role's reasoning effort."},{"name":"--model","value":"string","summary":"suggest: size the roles with this model instead of the Codex default model."},{"name":"--apply","value":"boolean","summary":"suggest: write every proposal through the role model write."}],
|
|
718
730
|
mutates: true,
|
|
719
731
|
json: "payload",
|
|
720
|
-
details: ["A bare invocation reads and never writes.","Requires Codex-based omo (LazyCodex): the omo@sisyphuslabs Codex plugin enabled in config.toml and installed; otherwise status lists no roles, and set and suggest are refused.","set rewrites only the root model value of $CODEX_HOME/agents/<role>.toml, plus model_reasoning_effort with --effort; omo.jsonc gets the same model and, when LazyCodex has that level, the effort as reasoning; it is skipped when absent or when it contains comments.","suggest sizes every role with one model call and prints proposals without writing; --apply writes each proposed model
|
|
732
|
+
details: ["A bare invocation reads and never writes.","Requires Codex-based omo (LazyCodex): the omo@sisyphuslabs Codex plugin enabled in config.toml and installed; otherwise status lists no roles, and set and suggest are refused.","set rewrites only the root model value of $CODEX_HOME/agents/<role>.toml, plus model_reasoning_effort with --effort; omo.jsonc gets the same model and, when LazyCodex has that level, the effort as reasoning; it is skipped when absent or when it contains comments.","suggest sizes every role with one model call and prints proposals without writing; --apply writes each proposed model and effort."],
|
|
721
733
|
usage: "ocx agent roles [status] [--json]; ocx agent roles set <role> <model> [--effort <level>] [--json]; ocx agent roles suggest [--model <id>] [--apply] [--json]",
|
|
722
734
|
},
|
|
723
735
|
{
|
|
@@ -23,7 +23,8 @@ export async function runCatalogAction(
|
|
|
23
23
|
if (error instanceof RuntimeApiError) {
|
|
24
24
|
if (error.code) {
|
|
25
25
|
console.error(`Error: ${error.code === "proxy_not_running" ? PROXY_NOT_RUNNING_MESSAGE
|
|
26
|
-
:
|
|
26
|
+
: error.code === "link_child_management_unavailable" ? terminalSafeText(error.message)
|
|
27
|
+
: "This listener has no management API. Run management commands on the connected Hub. No request was sent."}`);
|
|
27
28
|
return 1;
|
|
28
29
|
}
|
|
29
30
|
const code = own(error.body, "code") ?? own(own(error.body, "error"), "code");
|
package/src/cli/claude.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { spawn } from "node:child_process";
|
|
|
12
12
|
import { loadConfig } from "../config";
|
|
13
13
|
import { injectClaudeAgentDefs } from "../claude/agents-inject";
|
|
14
14
|
import { CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
|
|
15
|
-
import { claudeToolSearchEnv, effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
|
|
15
|
+
import { claudeTierDisplayName, claudeToolSearchEnv, effectiveModelEnv, isClaudeTierLabelSlot, resolveAutoContext } from "../claude/context-windows";
|
|
16
16
|
import { claudeConfigDir, fetchGatewayModels, writeGatewayModelCache } from "../claude/gateway-cache";
|
|
17
17
|
import { commandInvocation } from "../lib/win-exec";
|
|
18
18
|
import { isProxyAdmissionSecret } from "../server/auth-cors";
|
|
@@ -211,10 +211,12 @@ export function buildClaudeEnv(
|
|
|
211
211
|
// real cost to a documented entry point, and the escape hatch is the launcher: run
|
|
212
212
|
// through `ocx` (the published bin) and genuine shell exports are preserved by proof.
|
|
213
213
|
deleteUntrustedAnthropicSlots(env, deps);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
214
|
+
/** Returns whether the value was written; a value the user already has wins. */
|
|
215
|
+
const setDefault = (name: string, value: string | undefined): boolean => {
|
|
216
|
+
if (value === undefined || value.length === 0) return false;
|
|
217
|
+
if (env[name] !== undefined && env[name] !== "") return false; // user wins
|
|
217
218
|
env[name] = value;
|
|
219
|
+
return true;
|
|
218
220
|
};
|
|
219
221
|
if (config.claudeCode?.subagentModelForce !== undefined) {
|
|
220
222
|
const forced = resolveSubagentForceModel(config, contextWindows, deps.forceAvailableSelectors !== undefined ? { selectors: deps.forceAvailableSelectors } : { entries: deps.forceAvailable ?? [] });
|
|
@@ -415,7 +417,9 @@ export function buildClaudeEnv(
|
|
|
415
417
|
// with automatic [1m] context-variant marking when the slot's target model has an
|
|
416
418
|
// authoritative >=1M window (Claude Code then accounts 1M, compaction preserved).
|
|
417
419
|
for (const [name, value] of Object.entries(effectiveModelEnv(config.claudeCode, contextWindows, auto))) {
|
|
418
|
-
|
|
420
|
+
// A tier slot this launch wrote gets its picker label too (#6926). A slot the user exported
|
|
421
|
+
// stays unlabelled, and a `<slot>_NAME` the user exported wins.
|
|
422
|
+
if (setDefault(name, value) && isClaudeTierLabelSlot(name)) setDefault(`${name}_NAME`, claudeTierDisplayName(value));
|
|
419
423
|
}
|
|
420
424
|
return env;
|
|
421
425
|
}
|
|
@@ -682,7 +686,12 @@ export function buildNativeClaudeEnv(
|
|
|
682
686
|
const providerNames = Object.keys(config.providers);
|
|
683
687
|
for (const name of MODEL_ENV_SLOT_NAMES) {
|
|
684
688
|
const value = env[name];
|
|
685
|
-
if (value
|
|
689
|
+
if (!value || !isProxyOnlyModelId(value, providerNames)) continue;
|
|
690
|
+
delete env[name];
|
|
691
|
+
// The label opencodex paired with this slot names a proxy route the native session cannot use.
|
|
692
|
+
const labelName = `${name}_NAME`;
|
|
693
|
+
const label = claudeTierDisplayName(value);
|
|
694
|
+
if (isClaudeTierLabelSlot(name) && label !== undefined && env[labelName] === label) delete env[labelName];
|
|
686
695
|
}
|
|
687
696
|
if (deps.allowRootSkipPermissions === true && !env.IS_SANDBOX) env.IS_SANDBOX = "1";
|
|
688
697
|
return env;
|
|
@@ -60,11 +60,14 @@ export function remoteHubConfigNote(
|
|
|
60
60
|
// alone describes a machine that cannot read its hub, and `ocx status` is still the command
|
|
61
61
|
// that has the facts — so the note points there in every case, connected or not.
|
|
62
62
|
const connected = connection.state === "connected" && connection.token === "owned";
|
|
63
|
+
const link = config.client.transport === "link";
|
|
63
64
|
const note = connection.state !== "connected"
|
|
64
65
|
? `this machine is configured as a client but its connection is ${connection.state}${connection.reason ? ` (${connection.reason})` : ""}; run ocx connect status`
|
|
65
66
|
: connection.token !== "owned"
|
|
66
|
-
? `this machine is configured as a client but its hub data-plane token is ${connection.token}; run ocx connect status`
|
|
67
|
-
:
|
|
67
|
+
? `this machine is configured as a client but its ${link ? "Remote Link key" : "hub data-plane token"} is ${connection.token}; run ocx connect status`
|
|
68
|
+
: link
|
|
69
|
+
? "this machine is a Remote Link Child: provider credentials, model availability and the Codex settings OpenCodex writes come from its Home; the settings in this file are local and inactive while linked; run ocx status"
|
|
70
|
+
: "provider credentials and model availability live on the hub; run ocx status";
|
|
68
71
|
return { connected, origin: config.client.serverUrl, note };
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -235,6 +238,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
235
238
|
const path = args.shift();
|
|
236
239
|
if (!path) throw new CliUsageError("config path is required", USAGE);
|
|
237
240
|
rejectArgs(args, USAGE);
|
|
241
|
+
await (await import("./link-child")).printLinkChildLocalNotice("The values");
|
|
238
242
|
const diagnostics = readConfigDiagnostics();
|
|
239
243
|
if (diagnostics.source === "fallback" || diagnostics.error) {
|
|
240
244
|
console.error(`Warning: Config at ${getConfigPath()} is invalid or unreadable; defaults are being shown for invalid settings. Run: ocx config validate. Inspect: ocx config show --source.`);
|
|
@@ -246,6 +250,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
246
250
|
return;
|
|
247
251
|
}
|
|
248
252
|
if (action === "set" || action === "unset") {
|
|
253
|
+
await (await import("./link-child")).printLinkChildLocalWriteNotice();
|
|
249
254
|
const path = args.shift();
|
|
250
255
|
const raw = action === "set" ? args.shift() : undefined;
|
|
251
256
|
if (!path || (action === "set" && raw === undefined)) throw new CliUsageError(action === "unset" ? "config path is required" : "config path and value are required", USAGE);
|
|
@@ -316,6 +321,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
|
|
|
316
321
|
return;
|
|
317
322
|
}
|
|
318
323
|
if (action === "import") {
|
|
324
|
+
await (await import("./link-child")).printLinkChildLocalWriteNotice();
|
|
319
325
|
const path = args.shift();
|
|
320
326
|
const yes = takeFlag(args, "--yes");
|
|
321
327
|
if (!path) throw new CliUsageError("import path is required", USAGE);
|
package/src/cli/debug.ts
CHANGED
|
@@ -10,6 +10,14 @@ async function requireLiveProxy() {
|
|
|
10
10
|
console.error("Proxy is not running. Start it with: ocx start");
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
|
+
if (live.role === "client") {
|
|
14
|
+
// Debug capture is the serving proxy's; a connected client has no management API (#6957).
|
|
15
|
+
const { readLinkChildContext, linkChildRefusal } = await import("./link-child");
|
|
16
|
+
const link = readLinkChildContext();
|
|
17
|
+
console.error(link ? linkChildRefusal(link, "ocx debug")
|
|
18
|
+
: "This listener runs in the client role and has no management API. Run this command on its serving hub. No request was sent.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
13
21
|
return live;
|
|
14
22
|
}
|
|
15
23
|
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex
|
|
|
31
31
|
import { stripGrokConfig } from "../grok/inject";
|
|
32
32
|
import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
|
|
33
33
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
34
|
-
import { isJsonOption, takeFlag, terminalSafeError } from "./runtime-api";
|
|
34
|
+
import { isJsonOption, takeFlag, terminalSafeError, terminalSafeText } from "./runtime-api";
|
|
35
35
|
import { printStopSummary, type StopOutcome } from "./stop-report";
|
|
36
36
|
import { parseStopApproval, type StopApproval } from "./stop-approval";
|
|
37
37
|
import type { ResolveArgs } from "./resolve";
|
|
@@ -407,9 +407,22 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
407
407
|
try {
|
|
408
408
|
const { syncConnectedClient } = await import("../client/connect");
|
|
409
409
|
const result = await syncConnectedClient({ restartCodex });
|
|
410
|
+
const link = clientState.value.transport === "link";
|
|
410
411
|
console.log(result.stale
|
|
411
|
-
? "Hub unavailable; retained and applied the last-known-good remote catalog (stale)
|
|
412
|
-
: "Remote hub catalog synchronized.");
|
|
412
|
+
? `${link ? "Home" : "Hub"} unavailable; retained and applied the last-known-good remote catalog (stale).`
|
|
413
|
+
: link ? "Remote Link Home catalog synchronized." : "Remote hub catalog synchronized.");
|
|
414
|
+
if (result.homeSettings) {
|
|
415
|
+
const { describeHomeCodexSettings } = await import("../link/codex-settings");
|
|
416
|
+
const settings = result.homeSettings;
|
|
417
|
+
if (settings.source === "local") {
|
|
418
|
+
console.log(`Codex settings: kept this machine's local values (${terminalSafeText(settings.reason)}).`);
|
|
419
|
+
} else {
|
|
420
|
+
console.log(settings.source === "home"
|
|
421
|
+
? `Codex settings applied from Home ${terminalSafeText(settings.document.home.name)}:`
|
|
422
|
+
: `Codex settings applied from the last recorded Home read (${terminalSafeText(settings.reason)}):`);
|
|
423
|
+
for (const line of describeHomeCodexSettings(settings.document.settings)) console.log(` ${terminalSafeText(line)}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
413
426
|
await handleConnectedSyncCatalogWrite(result, restartScope);
|
|
414
427
|
// `process.exitCode` rather than a literal 0, for the same reason every other
|
|
415
428
|
// runner does it (tests/cli/cli-transport-honesty.test.ts): the catalog-write helper
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { writeFileSync } from "node:fs";
|
|
30
30
|
import { loadConfig } from "../config";
|
|
31
31
|
import {
|
|
32
|
+
ClientPathError,
|
|
32
33
|
EXPORT_CLIENTS,
|
|
33
34
|
EXPORT_CLIENT_IDS,
|
|
34
35
|
buildClientConfigText,
|
|
@@ -193,6 +194,18 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
193
194
|
const clientConfig = built.document;
|
|
194
195
|
const text = built.text;
|
|
195
196
|
|
|
197
|
+
// Resolve the human-facing destination BEFORE writing anything. A client whose
|
|
198
|
+
// config location cannot be determined (a relative override, or ZCode bootstrap
|
|
199
|
+
// settings naming an unknown data root) still exports the document: the export
|
|
200
|
+
// writes nothing into the client and the destination is advisory text.
|
|
201
|
+
let destination: string;
|
|
202
|
+
try {
|
|
203
|
+
destination = spec.destination(process.env);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
206
|
+
destination = `unknown (${error.message})`;
|
|
207
|
+
}
|
|
208
|
+
|
|
196
209
|
// Every serializer already ends with exactly one newline.
|
|
197
210
|
if (out !== undefined) writeExport(out, text, force);
|
|
198
211
|
// stderr, so `--json` stdout stays a standalone JSON document.
|
|
@@ -208,7 +221,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
208
221
|
...text.trimEnd().split("\n"),
|
|
209
222
|
"",
|
|
210
223
|
...(out !== undefined ? [`Wrote ${out}`] : []),
|
|
211
|
-
`Destination: ${
|
|
224
|
+
`Destination: ${destination}`,
|
|
212
225
|
"Merge this generated configuration into that file; do not replace it.",
|
|
213
226
|
`Before launching: ${spec.exportHint}`,
|
|
214
227
|
`${modelCount} model${modelCount === 1 ? "" : "s"}; ${modelsWithoutLimits} omit context limits (the client applies its own defaults).`,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mutatePersistedConfig, validateConfigCandidate } from "../config";
|
|
2
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
3
|
+
import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
|
|
4
|
+
import { probeServiceInstallation } from "../service/cli";
|
|
5
|
+
import { redactSecretString } from "../lib/redact";
|
|
6
|
+
import { CliUsageError, printData, runCliAction } from "./runtime-api";
|
|
7
|
+
import type { OcxConfig } from "../types";
|
|
8
|
+
|
|
9
|
+
export const HUB_DISABLE_USAGE = "ocx hub disable [--revoke-clients --client-key <id> ...] [--json]";
|
|
10
|
+
const HUB_KEYS = ["hub", "unauthenticatedLoopbackListener", "runtimeRole", "hostname"] as const;
|
|
11
|
+
const PUBLISHING_NOTE = "External publishing is unchanged. Check tailscale serve status and any other frontend for mappings to the old hub ports.";
|
|
12
|
+
|
|
13
|
+
export interface HubDisableOptions { json: boolean; revokeClients: boolean; clientKeys: string[] }
|
|
14
|
+
export interface HubDisableDeps {
|
|
15
|
+
mutateConfig?: typeof mutatePersistedConfig;
|
|
16
|
+
restartHint?: () => string;
|
|
17
|
+
}
|
|
18
|
+
interface HubDisableResult {
|
|
19
|
+
removed: string[];
|
|
20
|
+
keys: { id: string; name: string; createdAt: string; revoked: boolean }[];
|
|
21
|
+
revoked: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseHubDisableArgs(args: string[]): HubDisableOptions {
|
|
25
|
+
const options: HubDisableOptions = { json: false, revokeClients: false, clientKeys: [] };
|
|
26
|
+
for (let index = 0; index < args.length; index++) {
|
|
27
|
+
const arg = args[index];
|
|
28
|
+
if (arg === "--json" && !options.json) options.json = true;
|
|
29
|
+
else if (arg === "--revoke-clients" && !options.revokeClients) options.revokeClients = true;
|
|
30
|
+
else if (arg === "--client-key") {
|
|
31
|
+
const id = args[++index];
|
|
32
|
+
if (!id || id.startsWith("-") || options.clientKeys.includes(id)) throw new CliUsageError("Each --client-key requires a unique exact key ID.", HUB_DISABLE_USAGE);
|
|
33
|
+
options.clientKeys.push(id);
|
|
34
|
+
} else throw new CliUsageError("Unknown or repeated hub disable argument.", HUB_DISABLE_USAGE);
|
|
35
|
+
}
|
|
36
|
+
if (options.clientKeys.length && !options.revokeClients) throw new CliUsageError("--client-key requires --revoke-clients.", HUB_DISABLE_USAGE);
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Selection is explicit: legacy keys have editable names but no trustworthy issuance marker. */
|
|
41
|
+
export function disableHubConfig(config: OcxConfig, options: HubDisableOptions): HubDisableResult {
|
|
42
|
+
if (config.runtimeRole !== "hub") throw new CliUsageError("This machine is not a hub; no settings or keys were changed.");
|
|
43
|
+
const keys = config.apiKeys ?? [];
|
|
44
|
+
const secrets = keys.flatMap(key => [key.key, key.pendingRotation?.key]).filter((secret): secret is string => Boolean(secret));
|
|
45
|
+
const safeMetadata = (value: string) => redactSecretString(secrets.reduce((text, secret) => text.split(secret).join("[REDACTED]"), value));
|
|
46
|
+
if (options.revokeClients && keys.length && !options.clientKeys.length) {
|
|
47
|
+
throw new CliUsageError("Keys have no verified client origin. List IDs with 'ocx access key list --json', then repeat --client-key <id> for each client key to revoke.", HUB_DISABLE_USAGE);
|
|
48
|
+
}
|
|
49
|
+
const selected = new Set(options.clientKeys);
|
|
50
|
+
if (options.clientKeys.some(id => !keys.some(key => key.id === id))) {
|
|
51
|
+
throw new CliUsageError("A selected client key ID is not in the current config; no settings or keys were changed.");
|
|
52
|
+
}
|
|
53
|
+
const candidate = structuredClone(config);
|
|
54
|
+
const removed = HUB_KEYS.filter(key => Object.hasOwn(candidate, key));
|
|
55
|
+
for (const key of HUB_KEYS) delete candidate[key];
|
|
56
|
+
if (options.revokeClients) candidate.apiKeys = keys.filter(key => !selected.has(key.id));
|
|
57
|
+
const validated = validateConfigCandidate(candidate);
|
|
58
|
+
if (!validated.ok) throw new CliUsageError(validated.error);
|
|
59
|
+
// Keep deletion intent so a later live save cannot resurrect the hub's old bindings.
|
|
60
|
+
for (const key of HUB_KEYS) deleteConfigTopLevelKey(config, key);
|
|
61
|
+
Object.assign(config, validated.config);
|
|
62
|
+
return {
|
|
63
|
+
removed,
|
|
64
|
+
keys: keys.map(key => ({ id: safeMetadata(key.id), name: safeMetadata(key.name), createdAt: safeMetadata(key.createdAt), revoked: selected.has(key.id) })),
|
|
65
|
+
revoked: options.revokeClients ? [...selected].map(safeMetadata) : [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function hubDisableRestartHint(): string {
|
|
70
|
+
try {
|
|
71
|
+
if (inspectDesktopSupervision().kind === "desktop") return "Restart to apply: ocx restart (OpenCodex Desktop supervises the replacement).";
|
|
72
|
+
const service = probeServiceInstallation();
|
|
73
|
+
if (service.state === "installed") return "Restart to apply: ocx service restart.";
|
|
74
|
+
} catch { /* An unavailable probe must not prevent an already validated config change. */ }
|
|
75
|
+
return "Restart to apply: use your current proxy supervisor; for a manually started proxy, ocx restart.";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runHubDisable(args: string[], deps: HubDisableDeps = {}): Promise<number> {
|
|
79
|
+
return runCliAction(async () => {
|
|
80
|
+
const options = parseHubDisableArgs(args);
|
|
81
|
+
const outcome = (deps.mutateConfig ?? mutatePersistedConfig)(config => ({
|
|
82
|
+
changed: true, value: disableHubConfig(config, options),
|
|
83
|
+
}));
|
|
84
|
+
if (outcome.status === "unavailable") throw new Error(`Hub disable could not read or commit the config (${outcome.reason}); retry after fixing the config.`);
|
|
85
|
+
let restart: string;
|
|
86
|
+
try { restart = (deps.restartHint ?? hubDisableRestartHint)(); }
|
|
87
|
+
catch { restart = "Restart to apply: use your current proxy supervisor."; }
|
|
88
|
+
const result = { ok: true, ...outcome.value, restart, note: PUBLISHING_NOTE, keyOrigin: "unverified", effectiveAfterRestart: true };
|
|
89
|
+
// Explicit field projection excludes secrets and pending rotations; names are arbitrary input.
|
|
90
|
+
const safe = { ...result, keys: result.keys.map(key => ({ ...key, name: redactSecretString(key.name) })) };
|
|
91
|
+
printData(safe, options.json, [
|
|
92
|
+
`Removed hub settings: ${safe.removed.join(", ")}.`,
|
|
93
|
+
...safe.keys.map(key => `${key.revoked ? "Revoked" : "Retained"} saved key: ${key.id} (${key.name})`),
|
|
94
|
+
"Saved keys have no verified client origin; revocation uses only explicitly selected IDs.",
|
|
95
|
+
restart, PUBLISHING_NOTE,
|
|
96
|
+
"Restart is required to apply all saved settings; this command does not terminate existing sessions.",
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
}
|
package/src/cli/hub.ts
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
type GuiPairRequestResult,
|
|
8
8
|
} from "./gui-pair-client";
|
|
9
9
|
import type { RuntimeApiDeps } from "./runtime-api";
|
|
10
|
+
import { HUB_DISABLE_USAGE, runHubDisable, type HubDisableDeps } from "./hub-disable";
|
|
10
11
|
|
|
11
12
|
export const HUB_USAGE =
|
|
12
|
-
"ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]";
|
|
13
|
+
"ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]\n " + HUB_DISABLE_USAGE;
|
|
13
14
|
|
|
14
15
|
const PAIRING_WARNING = "Pairing codes are secret, single-use, and expire quickly. Do not save them.";
|
|
15
16
|
|
|
@@ -23,7 +24,7 @@ const PAIRING_WARNING = "Pairing codes are secret, single-use, and expire quickl
|
|
|
23
24
|
*/
|
|
24
25
|
const DEFAULT_CLIENT_BROWSER_ORIGIN = "http://localhost:10100";
|
|
25
26
|
|
|
26
|
-
export interface HubCommandDeps extends RuntimeApiDeps {
|
|
27
|
+
export interface HubCommandDeps extends RuntimeApiDeps, HubDisableDeps {
|
|
27
28
|
loadConfig: () => OcxConfig;
|
|
28
29
|
findLiveProxy?: () => Promise<LiveProxy | null>;
|
|
29
30
|
requestPairingGrant?: (
|
|
@@ -361,6 +362,7 @@ async function runInvite(args: string[], deps: HubCommandDeps): Promise<number>
|
|
|
361
362
|
}
|
|
362
363
|
|
|
363
364
|
export async function runHubCommand(args: string[], deps: HubCommandDeps): Promise<number> {
|
|
365
|
+
if (args[0] === "disable") return runHubDisable(args.slice(1), deps);
|
|
364
366
|
if (args[0] !== "invite") {
|
|
365
367
|
console.error(`Usage: ${HUB_USAGE}`);
|
|
366
368
|
return 1;
|
package/src/cli/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { packageVersion } from "../lib/package-version";
|
|
|
11
11
|
import { codexHomeIsAbsent } from "../codex/codex-home-owner";
|
|
12
12
|
import { getCodexHome } from "../codex/paths";
|
|
13
13
|
import { admitUpdateRestartChild } from "./update-restart-child";
|
|
14
|
+
import { HEALTH_SENTINEL_STANDALONE_ARG, startServiceHealthSentinel } from "../service/health-sentinel-launch";
|
|
14
15
|
import { UpdateRestartRequired } from "./update-restart-candidate";
|
|
15
16
|
import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
|
|
16
17
|
import { findGuiDist } from "../server/gui-static";
|
|
@@ -212,6 +213,7 @@ initializeNodeLauncherContext();
|
|
|
212
213
|
// The compiled executable is also the capture-only MCP server's launcher.
|
|
213
214
|
// Handle this private entrypoint before CLI preflight or command dispatch.
|
|
214
215
|
if (process.argv[2] === "__keyring-load-check") { console.log(JSON.stringify((await import("../lib/keyring-native")).inspectKeyringBinding())); process.exit(0); }
|
|
216
|
+
if (process.argv[2] === HEALTH_SENTINEL_STANDALONE_ARG) process.exit(await (await import("../service/health-sentinel-main")).runHealthSentinelProcess());
|
|
215
217
|
|
|
216
218
|
// The compiled executable also launches the isolated MCP servers.
|
|
217
219
|
// Handle these private entrypoints before CLI preflight or command dispatch.
|
|
@@ -623,6 +625,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
623
625
|
// Records are visible now; background work may observe this runtime without a gap.
|
|
624
626
|
scheduleCatalogPrewarm();
|
|
625
627
|
installCrashGuards();
|
|
628
|
+
let healthSentinel: ReturnType<typeof startServiceHealthSentinel> = null; // opt-in #6671, started once ready
|
|
626
629
|
// No pre-emptive snapshot here. `injectCodexConfig` journals the exact bytes it
|
|
627
630
|
// is about to transform; snapshotting earlier only captured a baseline that could
|
|
628
631
|
// already be stale by the time injection ran (#477).
|
|
@@ -646,6 +649,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
646
649
|
try { routingHealer?.stop(); } catch { /* best-effort */ }
|
|
647
650
|
try { guardian.stop(); } catch { /* best-effort */ }
|
|
648
651
|
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
652
|
+
try { healthSentinel?.stop(); } catch { /* best-effort */ }
|
|
649
653
|
// Dashboard drain-and-restart (#563) must not tear down injection: the replacement
|
|
650
654
|
// process expects Codex/Grok/env fences to still be in place. A sibling owns none of them.
|
|
651
655
|
const teardown = decideStartExitTeardown({ sibling: siblingStart, recycling: isRecyclingForExit(), ocxService: process.env.OCX_SERVICE });
|
|
@@ -746,6 +750,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
746
750
|
);
|
|
747
751
|
if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config), server.port ?? port));
|
|
748
752
|
await refreshOwnedRaycastCatalog(config, port);
|
|
753
|
+
if (!cleaned && !shuttingDown) healthSentinel = startServiceHealthSentinel(config); // after startup reconciliation; never mid-boot or mid-drain
|
|
749
754
|
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
750
755
|
// invalidation happens first and the catalog sync second, so the mtime is only
|
|
751
756
|
// final here — and neither write site warns on its own, or a boot that hits
|
|
@@ -1717,7 +1722,7 @@ async function handleStatus() {
|
|
|
1717
1722
|
// are tagged `(local)`. A reader who sees neither draws the wrong conclusion from a correct
|
|
1718
1723
|
// report — an agent read `xai ✗ not logged in` off a client and decided the hub could not
|
|
1719
1724
|
// serve grok.
|
|
1720
|
-
const remoteHubBanner = remoteHubBannerLine(status.json.remoteHub);
|
|
1725
|
+
const remoteHubBanner = remoteHubBannerLine(status.json.remoteHub, status.json.connection.transport);
|
|
1721
1726
|
if (remoteHubBanner) console.log(remoteHubBanner);
|
|
1722
1727
|
// `(local)` only while connected: on a standalone install every line is local, and tagging
|
|
1723
1728
|
// them all would be noise that trains the reader to skip the tag.
|
|
@@ -1798,7 +1803,9 @@ async function handleStatus() {
|
|
|
1798
1803
|
if (status.json.hub) {
|
|
1799
1804
|
for (const line of hubStatusLines(status.json.hub)) console.log(` ${line}`);
|
|
1800
1805
|
}
|
|
1801
|
-
console.log(
|
|
1806
|
+
console.log(status.json.remoteHub.link
|
|
1807
|
+
? ` Remote Link Home: ${status.json.connection.state} (${status.json.remoteHub.link.homePhrase}, ${status.json.remoteHub.link.linkId})`
|
|
1808
|
+
: ` ${status.json.connection.transport === "link" ? "Remote Link Home" : "Remote hub"}: ${status.json.connection.state}${status.json.connection.serverUrl ? ` (${status.json.connection.serverUrl})` : ""}`);
|
|
1802
1809
|
if (status.json.connection.state === "invalid" || status.json.connection.state === "mismatched") {
|
|
1803
1810
|
console.log(` ⚠️ ${status.json.connection.reason}`);
|
|
1804
1811
|
}
|
|
@@ -1854,10 +1861,12 @@ async function handleStatus() {
|
|
|
1854
1861
|
// that decide what a request can route to. The local block still prints — an operator debugging
|
|
1855
1862
|
// a half-migrated machine needs to see it — but under a heading that says it is not in use, and
|
|
1856
1863
|
// below the hub's, so the hub's is what a reader (or an agent) encounters first (#4236).
|
|
1857
|
-
for (const line of remoteHubStatusLines(status.json.remoteHub)) console.log(` ${line}`);
|
|
1864
|
+
for (const line of remoteHubStatusLines(status.json.remoteHub, status.json.connection.transport)) console.log(` ${line}`);
|
|
1858
1865
|
if (status.json.remoteHub.connected) {
|
|
1859
1866
|
console.log(status.json.remoteHub.stateSource === "unavailable"
|
|
1860
|
-
?
|
|
1867
|
+
? status.json.connection.transport === "link"
|
|
1868
|
+
? " Local-only credential state (Home state is unavailable over Remote Link):"
|
|
1869
|
+
: " Local-only credential state (this is NOT the hub's; the hub's state could not be read):"
|
|
1861
1870
|
: " Local-only (not used for routing while connected):");
|
|
1862
1871
|
}
|
|
1863
1872
|
console.log(` OAuth logins${local}:`);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Management commands on a Remote Link Child (#6957).
|
|
3
|
+
*
|
|
4
|
+
* A link Child's listener serves no management API, and the Home owns every setting that decides
|
|
5
|
+
* routing. Each management command therefore does one of two things: an allowlisted read goes
|
|
6
|
+
* to the Home through the connection-bound link relay, and anything else is refused with the one
|
|
7
|
+
* message `linkChildManagementRefusal` builds, which names the Home and the link.
|
|
8
|
+
*/
|
|
9
|
+
import { readBoundedResponseBytes } from "../lib/bounded-body";
|
|
10
|
+
import { readServiceApiTokenState } from "../lib/service-secrets";
|
|
11
|
+
import { linkChildManagementRefusal, linkManagementReadAllowed } from "../link/management-routes";
|
|
12
|
+
import { readClientConnectionState } from "../client/state";
|
|
13
|
+
import { fetchLinkHomeManagement, linkHomePhrase, type LinkHomeDeps } from "../client/link-home";
|
|
14
|
+
import type { OcxClientConnectionConfig } from "../types";
|
|
15
|
+
|
|
16
|
+
const RESPONSE_MAX_BYTES = 32 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
export interface LinkChildContext {
|
|
19
|
+
connection: OcxClientConnectionConfig;
|
|
20
|
+
linkId: string;
|
|
21
|
+
homePhrase: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The link Child this CLI runs on, or null on a standalone, a hub or a hub client. */
|
|
25
|
+
export function readLinkChildContext(read: typeof readClientConnectionState = readClientConnectionState): LinkChildContext | null {
|
|
26
|
+
let state: ReturnType<typeof readClientConnectionState>;
|
|
27
|
+
try { state = read(); } catch { return null; }
|
|
28
|
+
if (state.kind !== "connected" || state.value.transport !== "link" || !state.value.link) return null;
|
|
29
|
+
return { connection: state.value, linkId: state.value.link.linkId, homePhrase: linkHomePhrase(state.value) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function linkChildRefusal(context: LinkChildContext, operation?: string): string {
|
|
33
|
+
return linkChildManagementRefusal(context.homePhrase, context.linkId, operation);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The stderr label for a local read on a link Child (`ocx provider list`, `ocx config get`):
|
|
38
|
+
* the values are real, but they are this machine's and the Home's apply.
|
|
39
|
+
*/
|
|
40
|
+
export function linkChildLocalNotice(what: string): string | null {
|
|
41
|
+
const link = readLinkChildContext();
|
|
42
|
+
if (!link) return null;
|
|
43
|
+
return `Note: this machine is a Remote Link Child managed by ${link.homePhrase} (link ${link.linkId}). `
|
|
44
|
+
+ `${what} below are this machine's local configuration. Its provider, model and routing settings are inactive while it is linked; the Home's apply. Run 'ocx status' for the Home's state.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function printLinkChildLocalNotice(what: string): Promise<void> {
|
|
48
|
+
const notice = linkChildLocalNotice(what);
|
|
49
|
+
if (notice) console.error(notice);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `ocx config set|unset|import` on a link Child: the write is local and its routing parts inactive. */
|
|
53
|
+
export async function printLinkChildLocalWriteNotice(): Promise<void> {
|
|
54
|
+
const link = readLinkChildContext();
|
|
55
|
+
if (!link) return;
|
|
56
|
+
console.error(`Note: this machine is a Remote Link Child managed by ${link.homePhrase} (link ${link.linkId}). `
|
|
57
|
+
+ "This writes this machine's local configuration; its provider, model and routing settings stay inactive while it is linked, and the Home's apply.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The refusal for a local provider change on a link Child, or null elsewhere. */
|
|
61
|
+
export function linkChildProviderChangeRefusal(operation: string): string | null {
|
|
62
|
+
const link = readLinkChildContext();
|
|
63
|
+
return link ? linkChildRefusal(link, operation) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Can this management request be answered by reading the Home? */
|
|
67
|
+
export function linkChildReadable(method: string, path: string): boolean {
|
|
68
|
+
const url = new URL(path.startsWith("/") ? path : `/${path}`, "http://cli.invalid");
|
|
69
|
+
return linkManagementReadAllowed(method, url.pathname, url.searchParams);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One allowlisted read from the Home. Throws with an operator sentence when it cannot be sent. */
|
|
73
|
+
export async function readLinkHomeJson(
|
|
74
|
+
context: LinkChildContext,
|
|
75
|
+
path: string,
|
|
76
|
+
deps: LinkHomeDeps = {},
|
|
77
|
+
): Promise<{ status: number; body: unknown }> {
|
|
78
|
+
const token = readServiceApiTokenState();
|
|
79
|
+
if (token.kind !== "present" || token.fingerprint !== context.connection.tokenFingerprint) {
|
|
80
|
+
throw new Error(`The enrolled link key for ${context.homePhrase} is missing or changed; re-link this machine from the Home.`);
|
|
81
|
+
}
|
|
82
|
+
let response: Response;
|
|
83
|
+
try {
|
|
84
|
+
response = await fetchLinkHomeManagement(context.connection, token.token, path.startsWith("/") ? path : `/${path}`, {}, deps);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const detail = error instanceof Error && error.name === "LinkRelayAuthenticationError" ? error.message : "the link tunnel did not answer";
|
|
87
|
+
throw new Error(`Could not read ${context.homePhrase} over Remote Link (${context.linkId}): ${detail}.`);
|
|
88
|
+
}
|
|
89
|
+
const result = await readBoundedResponseBytes(response, { maxBytes: RESPONSE_MAX_BYTES, signal: AbortSignal.timeout(30_000) });
|
|
90
|
+
if (result.oversized) throw new Error(`${context.homePhrase} returned a response larger than the CLI accepts.`);
|
|
91
|
+
const text = new TextDecoder().decode(result.bytes);
|
|
92
|
+
let body: unknown = null;
|
|
93
|
+
if (text) {
|
|
94
|
+
try { body = JSON.parse(text); } catch { body = text; }
|
|
95
|
+
}
|
|
96
|
+
// A Home that predates the relay answers its listener's generic unknown-endpoint 404.
|
|
97
|
+
const error = body && typeof body === "object" ? (body as Record<string, unknown>).error : undefined;
|
|
98
|
+
const unknownEndpoint = error === "not_found"
|
|
99
|
+
|| (!!error && typeof error === "object" && (error as Record<string, unknown>).code === "not_found");
|
|
100
|
+
if (response.status === 404 && unknownEndpoint) {
|
|
101
|
+
return { status: 503, body: { error: `${context.homePhrase} does not serve Remote Link management reads; upgrade the Home.` } };
|
|
102
|
+
}
|
|
103
|
+
return { status: response.status, body };
|
|
104
|
+
}
|