@bitkyc08/opencodex 2.82.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-DTBC8n7-.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/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- 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 +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -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 +44 -12
- 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/resolve.ts +1 -1
- 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/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- 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/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- 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 +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- 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 +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- 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/errors.ts +25 -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/redact.ts +2 -2
- 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 +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- 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-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- 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 +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- 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/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- 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 +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- 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 +333 -57
- 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 +92 -390
- 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 +7 -5
- 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 +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- 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 +129 -50
- 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-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- 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/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type KeyRecord = Record<string, unknown> & { id: string };
|
|
2
|
+
|
|
3
|
+
function index(value: unknown): Map<string, KeyRecord> | null {
|
|
4
|
+
if (value === undefined) return new Map();
|
|
5
|
+
if (!Array.isArray(value)) return null;
|
|
6
|
+
const rows = new Map<string, KeyRecord>();
|
|
7
|
+
for (const row of value) {
|
|
8
|
+
if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.id !== "string" || !row.id || rows.has(row.id)) return null;
|
|
9
|
+
rows.set(row.id, row);
|
|
10
|
+
}
|
|
11
|
+
return rows;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Stable-ID three-way merge: either side's revocation defeats stale edits to that ID. */
|
|
15
|
+
export function reconcileApiKeys(
|
|
16
|
+
baseline: unknown, live: unknown, persisted: unknown,
|
|
17
|
+
mergeRecord: (live: Record<string, unknown>, baseline: Record<string, unknown>, persisted: Record<string, unknown>) => void,
|
|
18
|
+
): unknown[] | null {
|
|
19
|
+
if (baseline === undefined && live === undefined && persisted === undefined) return null;
|
|
20
|
+
const before = index(baseline), pending = index(live), disk = index(persisted);
|
|
21
|
+
if (!before || !pending || !disk) return null;
|
|
22
|
+
const merged: KeyRecord[] = [];
|
|
23
|
+
const liveOrder = [...pending.keys()], diskOrder = [...disk.keys()];
|
|
24
|
+
const order = JSON.stringify(liveOrder) === JSON.stringify([...before.keys()])
|
|
25
|
+
? [...diskOrder, ...liveOrder] : [...liveOrder, ...diskOrder];
|
|
26
|
+
for (const id of new Set(order)) {
|
|
27
|
+
const local = pending.get(id), saved = disk.get(id);
|
|
28
|
+
if (before.has(id) && (!local || !saved)) continue;
|
|
29
|
+
const row = structuredClone(local ?? saved!);
|
|
30
|
+
if (local && saved) mergeRecord(row, before.get(id) ?? {}, saved);
|
|
31
|
+
merged.push(row);
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
@@ -648,6 +648,21 @@ export function metricsExportConfigError(value: unknown): string | null {
|
|
|
648
648
|
return null;
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
+
/** Load degrades a malformed service health recovery section to off; live writes reject it. */
|
|
652
|
+
export function serviceHealthRecoveryConfigError(value: unknown): string | null {
|
|
653
|
+
const raw = rawConfigRecord(value);
|
|
654
|
+
if (!raw || !Object.hasOwn(raw, "serviceHealthRecovery") || raw.serviceHealthRecovery === undefined) return null;
|
|
655
|
+
const section = rawConfigRecord(raw.serviceHealthRecovery);
|
|
656
|
+
if (!section) return "schema_invalid: serviceHealthRecovery: must be an object or omitted";
|
|
657
|
+
if (Object.keys(section).some(key => key !== "enabled")) {
|
|
658
|
+
return "schema_invalid: serviceHealthRecovery: contains an unsupported field";
|
|
659
|
+
}
|
|
660
|
+
if (section.enabled !== undefined && typeof section.enabled !== "boolean") {
|
|
661
|
+
return "schema_invalid: serviceHealthRecovery.enabled: must be a boolean";
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
651
666
|
|
|
652
667
|
function skillsConfigError(value: unknown): string | null {
|
|
653
668
|
const raw = rawConfigRecord(value);
|
|
@@ -710,6 +725,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
710
725
|
?? loopbackListenerPortError(value)
|
|
711
726
|
?? managementIngressConfigError(value)
|
|
712
727
|
?? metricsExportConfigError(value)
|
|
728
|
+
?? serviceHealthRecoveryConfigError(value)
|
|
713
729
|
?? skillsConfigError(value);
|
|
714
730
|
if (boundaryError) return { ok: false, error: boundaryError };
|
|
715
731
|
const result = configSchema.safeParse(value);
|
|
@@ -17,6 +17,11 @@ export function metricsExportEnabled(config: Pick<OcxConfig, "metricsExport">):
|
|
|
17
17
|
return config.metricsExport?.enabled === true;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Default-off service health recovery (#6671); read once when the proxy starts. */
|
|
21
|
+
export function serviceHealthRecoveryEnabled(config: Pick<OcxConfig, "serviceHealthRecovery">): boolean {
|
|
22
|
+
return config.serviceHealthRecovery?.enabled === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
/**
|
|
21
26
|
* Default cadence for catalog auto-refresh (issue #3630): one converge pass
|
|
22
27
|
* per hour. Each pass spends a live /models call against every enabled provider, and
|
|
@@ -18,6 +18,8 @@ import { withConfigMutationLockSync, bumpGenerationForCooperatingConfigWrite } f
|
|
|
18
18
|
import { ConfigWritePublishedError, persistConfigUnlocked, readRawConfigJson } from "./persist-unlocked";
|
|
19
19
|
import { configDiagnosticsFromRaw, readConfigDiagnostics } from "./diagnostics";
|
|
20
20
|
import { normalizePersistedClaudeCode } from "./load-degrade";
|
|
21
|
+
import { reconcileApiKeys } from "./api-key-reconcile";
|
|
22
|
+
import { captureLiveTopologyBaseline, LIVE_TOPOLOGY_KEYS, projectLiveTopology } from "./live-topology";
|
|
21
23
|
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
// Hand-edit protection for the `claudeCode` subtree (devlog 260726_claude_auth_auto/040 H1).
|
|
@@ -104,6 +106,7 @@ const detachedConfigSnapshots = new WeakSet<OcxConfig>();
|
|
|
104
106
|
export function armClaudeCodeBaseline(config: OcxConfig): void {
|
|
105
107
|
liveConfigBaseline.set(config, structuredClone(config));
|
|
106
108
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
109
|
+
captureLiveTopologyBaseline(config);
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/**
|
|
@@ -321,7 +324,12 @@ function reconcileConfigRecord(
|
|
|
321
324
|
&& baselineValue !== MISSING_CONFIG_VALUE
|
|
322
325
|
&& persistedValue === MISSING_CONFIG_VALUE
|
|
323
326
|
? MISSING_CONFIG_VALUE
|
|
324
|
-
: key === "
|
|
327
|
+
: key === "apiKeys"
|
|
328
|
+
? reconcileApiKeys(baselineValue === MISSING_CONFIG_VALUE ? undefined : baselineValue,
|
|
329
|
+
liveValue === MISSING_CONFIG_VALUE ? undefined : liveValue,
|
|
330
|
+
persistedValue === MISSING_CONFIG_VALUE ? undefined : persistedValue, reconcileConfigRecord)
|
|
331
|
+
?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
|
|
332
|
+
: key === "customModels"
|
|
325
333
|
? reconcileCustomModels(baselineValue, liveValue, persistedValue)
|
|
326
334
|
?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
|
|
327
335
|
: key === "disabledModels"
|
|
@@ -401,7 +409,7 @@ export function reconcileLiveConfigFromDisk(config: OcxConfig, persistedBaseline
|
|
|
401
409
|
config as unknown as Record<string, unknown>,
|
|
402
410
|
persistedBaseline as unknown as Record<string, unknown>,
|
|
403
411
|
persisted as unknown as Record<string, unknown>,
|
|
404
|
-
new Set([
|
|
412
|
+
new Set([...LIVE_TOPOLOGY_KEYS, ...(claudeGuardArmed ? ["claudeCode"] : [])]),
|
|
405
413
|
);
|
|
406
414
|
applyConfigObjectChildDeletions(config, childDeletions);
|
|
407
415
|
|
|
@@ -473,36 +481,32 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
473
481
|
// custom-model deletion migration. A second read could observe different bytes.
|
|
474
482
|
const onDisk = readRawConfigJson();
|
|
475
483
|
const baseline = liveConfigBaseline.get(config);
|
|
484
|
+
let diskDeletedKeys = new Set<string>();
|
|
485
|
+
let persistedSnapshot: OcxConfig | undefined;
|
|
476
486
|
if (baseline && onDisk !== undefined) {
|
|
477
487
|
const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
|
|
478
488
|
if (persistedDiagnostics.source === "file") {
|
|
479
|
-
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// key — including configRebaseProvenance, so a cooperating writer's
|
|
484
|
-
// deletion marker adopted from disk is honored instead of silently dropped —
|
|
485
|
-
// and captures deletion intent from the current disk snapshot before the
|
|
486
|
-
// merge can temporarily restore a key and invalidate its deletion marker.
|
|
489
|
+
persistedSnapshot = persistedDiagnostics.config;
|
|
490
|
+
// Capture authoritative disk deletions before reconciliation can restore a key.
|
|
491
|
+
// Live instances retain actual socket identity; detached discovery snapshots
|
|
492
|
+
// own no socket and also merge listener fields and disk-only sections.
|
|
487
493
|
const detached = detachedConfigSnapshots.has(config);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
494
|
+
diskDeletedKeys = configRebaseDeletionKeys(persistedDiagnostics.config);
|
|
495
|
+
// Preserve explicit pending live edits; stale retention is not an edit.
|
|
496
|
+
if (!detached) for (const key of diskDeletedKeys) {
|
|
497
|
+
if (!LIVE_TOPOLOGY_KEYS.has(key) && !deepEqual(
|
|
498
|
+
ownConfigValue(config as unknown as Record<string, unknown>, key),
|
|
499
|
+
ownConfigValue(baseline as unknown as Record<string, unknown>, key))) diskDeletedKeys.delete(key);
|
|
500
|
+
}
|
|
501
|
+
const deletedKeys = new Set([...diskDeletedKeys, ...configRebaseDeletionKeys(config)]);
|
|
491
502
|
const skipped = detached
|
|
492
503
|
? new Set(["claudeCode"])
|
|
493
|
-
: new Set([
|
|
504
|
+
: new Set([...LIVE_TOPOLOGY_KEYS, "claudeCode", CONFIG_REBASE_PROVENANCE_KEY]);
|
|
494
505
|
if (!detached) {
|
|
495
|
-
const provenanceExists = configHasRebaseProvenance(config);
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
// another process — is absent from the baseline as well as from the live
|
|
500
|
-
// config. Reconciling those keys reads "live never changed this" and adopts
|
|
501
|
-
// the disk value, which resurrects a field the live writer had deliberately
|
|
502
|
-
// deleted (#1462 regression: PUT /api/grok/selection with an empty list).
|
|
503
|
-
// Restrict the merge to keys the baseline knew about, plus keys the live
|
|
504
|
-
// config still carries; a key that exists only on disk is left to the
|
|
505
|
-
// ordinary whole-config write below.
|
|
506
|
+
const provenanceExists = configHasRebaseProvenance(config) || configHasRebaseProvenance(persistedDiagnostics.config);
|
|
507
|
+
// Without provenance, disk-only keys remain subject to the legacy whole-
|
|
508
|
+
// config write: adopting them could resurrect a deliberate live deletion
|
|
509
|
+
// absent from its baseline (#1462). Provenance makes that intent explicit.
|
|
506
510
|
const rebaseableKeys = new Set([
|
|
507
511
|
...Object.keys(baseline as unknown as Record<string, unknown>),
|
|
508
512
|
...Object.keys(config as unknown as Record<string, unknown>),
|
|
@@ -521,6 +525,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
521
525
|
skipped,
|
|
522
526
|
);
|
|
523
527
|
for (const key of deletedKeys) {
|
|
528
|
+
if (!detached && LIVE_TOPOLOGY_KEYS.has(key)) continue;
|
|
524
529
|
delete (config as unknown as Record<string, unknown>)[key];
|
|
525
530
|
}
|
|
526
531
|
}
|
|
@@ -537,15 +542,23 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
537
542
|
}
|
|
538
543
|
}
|
|
539
544
|
}
|
|
540
|
-
const
|
|
545
|
+
const desired = persistedSnapshot && !detachedConfigSnapshots.has(config)
|
|
546
|
+
? projectLiveTopology(config, persistedSnapshot, reconcileConfigRecord) : config;
|
|
547
|
+
const durableProjection = { ...desired, configRebaseProvenance: projectConfigRebaseProvenance(config).configRebaseProvenance };
|
|
548
|
+
for (const key of diskDeletedKeys) {
|
|
549
|
+
delete (durableProjection as unknown as Record<string, unknown>)[key];
|
|
550
|
+
}
|
|
551
|
+
if (diskDeletedKeys.size) durableProjection.configRebaseProvenance = { version: 1,
|
|
552
|
+
deletedTopLevelKeys: [...new Set([...configRebaseDeletionKeys(durableProjection), ...diskDeletedKeys])].sort() };
|
|
553
|
+
const provenanceProjection = projectConfigRebaseProvenance(durableProjection);
|
|
541
554
|
const projectedConfig = projectCustomModelCatalogMigration(
|
|
542
555
|
onDisk,
|
|
543
|
-
|
|
556
|
+
durableProjection,
|
|
544
557
|
);
|
|
545
558
|
if (provenanceProjection.configRebaseProvenance === undefined) delete projectedConfig.configRebaseProvenance;
|
|
546
559
|
else projectedConfig.configRebaseProvenance = provenanceProjection.configRebaseProvenance;
|
|
547
|
-
const persistedBinding = bindingBaseline && onDisk
|
|
548
|
-
? readPersistedServerBinding(onDisk, bindingBaseline)
|
|
560
|
+
const persistedBinding = (bindingBaseline || diskDeletedKeys.has("hostname")) && onDisk
|
|
561
|
+
? readPersistedServerBinding(onDisk, bindingBaseline ?? { port: config.port, hostname: config.hostname })
|
|
549
562
|
: bindingBaseline;
|
|
550
563
|
if (persistedBinding) {
|
|
551
564
|
const persistedConfig: OcxConfig = { ...projectedConfig, port: persistedBinding.port };
|
|
@@ -557,6 +570,7 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
557
570
|
persist(projectedConfig);
|
|
558
571
|
}
|
|
559
572
|
adoptCustomModelCatalogMigration(config, projectedConfig);
|
|
573
|
+
captureLiveTopologyBaseline(config);
|
|
560
574
|
if (claudeCodeBaseline.has(config)) {
|
|
561
575
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
562
576
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const LIVE_TOPOLOGY_KEYS = new Set(["hostname", "port", "runtimeRole", "hub", "client", "unauthenticatedLoopbackListener"]);
|
|
4
|
+
const baselines = new WeakMap<OcxConfig, Record<string, unknown>>();
|
|
5
|
+
|
|
6
|
+
function snapshot(config: OcxConfig): Record<string, unknown> {
|
|
7
|
+
const record = config as unknown as Record<string, unknown>;
|
|
8
|
+
return Object.fromEntries([...LIVE_TOPOLOGY_KEYS].filter(key => Object.hasOwn(record, key))
|
|
9
|
+
.map(key => [key, structuredClone(record[key])]));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Track actual active values independently of the desired config last persisted. */
|
|
13
|
+
export function captureLiveTopologyBaseline(config: OcxConfig): void {
|
|
14
|
+
baselines.set(config, snapshot(config));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Merge desired next-start topology without mutating the active listener/role objects. */
|
|
18
|
+
export function projectLiveTopology(
|
|
19
|
+
config: OcxConfig, persisted: OcxConfig,
|
|
20
|
+
mergeRecord: (live: Record<string, unknown>, baseline: Record<string, unknown>, disk: Record<string, unknown>, skipped: ReadonlySet<string>) => void,
|
|
21
|
+
): OcxConfig {
|
|
22
|
+
const projected = { ...config, ...snapshot(config) } as unknown as Record<string, unknown>;
|
|
23
|
+
const baseline = baselines.get(config) ?? snapshot(config);
|
|
24
|
+
const disk = persisted as unknown as Record<string, unknown>;
|
|
25
|
+
const skipped = new Set([...Object.keys(projected), ...Object.keys(baseline), ...Object.keys(disk)]
|
|
26
|
+
.filter(key => !LIVE_TOPOLOGY_KEYS.has(key)));
|
|
27
|
+
mergeRecord(projected, baseline, disk, skipped);
|
|
28
|
+
return projected as unknown as OcxConfig;
|
|
29
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./provider-validation";
|
|
17
17
|
import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../codex/upstream-host-health";
|
|
18
18
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
19
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
19
20
|
import { redactSecretString } from "../lib/redact";
|
|
20
21
|
import { isValidProviderName } from "./provider-name";
|
|
21
22
|
import { MODEL_ALIAS_PATTERN } from "../providers/default-aliases";
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
|
|
50
51
|
|
|
51
52
|
export function hardenExistingSecret(path: string): void {
|
|
52
|
-
if (existsSync(path)) {
|
|
53
|
+
if (existsSync(path) && !isPassiveRead()) {
|
|
53
54
|
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
54
55
|
if (process.platform === "win32") {
|
|
55
56
|
hardenSecretPath(path, { required: false });
|
package/src/config/paths.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { flushWindowsSecretAclReapsBeforeRemoval, hardenSecretDirAsync, windowsSecretAclApplies } from "../lib/windows-secret-acl";
|
|
5
5
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
6
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Expand a leading `~` in user-supplied paths without interpreting shell
|
|
@@ -29,6 +30,8 @@ export function getConfigPath(): string {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function hardenConfigDir(): void {
|
|
33
|
+
// A passive read (a Remote Link Child's, #6959) repairs nothing on this machine.
|
|
34
|
+
if (isPassiveRead()) return;
|
|
32
35
|
const dir = getConfigDir();
|
|
33
36
|
// The guard runs before any mutation: refusing after chmod/ACL would already
|
|
34
37
|
// have changed the protected directory used by the test-home boundary.
|
package/src/config/salvage.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import * as z from "zod/v4";
|
|
4
4
|
import { CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR } from "../codex/account-namespace-match";
|
|
5
5
|
import { redactSecretString } from "../lib/redact";
|
|
6
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
6
7
|
import { hasWarnedConfigFallback, markWarnedConfigFallback } from "./warn-memo";
|
|
7
8
|
import { configSchema } from "./schema/config-schema";
|
|
8
9
|
import type { OcxConfig } from "../types";
|
|
@@ -232,7 +233,8 @@ export function warnAndBackupInvalidConfig(configPath: string, error: unknown):
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
export function backupInvalidConfig(configPath: string): string | null {
|
|
235
|
-
|
|
236
|
+
// A passive read leaves a malformed file for the Home's own load to back up (#6959).
|
|
237
|
+
if (!existsSync(configPath) || isPassiveRead()) return null;
|
|
236
238
|
const backupPath = `${configPath}.invalid-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
237
239
|
try {
|
|
238
240
|
copyFileSync(configPath, backupPath);
|
|
@@ -105,6 +105,8 @@ export const configSchema = z.object({
|
|
|
105
105
|
skills: skillsConfigSchema.optional().catch(undefined),
|
|
106
106
|
// Malformed hand edits disable this opt-in exporter. Live writes reject them in diagnostics.ts.
|
|
107
107
|
metricsExport: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
108
|
+
// Same contract as metricsExport: a malformed hand edit leaves recovery off; live writes reject it.
|
|
109
|
+
serviceHealthRecovery: z.object({ enabled: z.boolean().optional() }).strict().optional().catch(undefined),
|
|
108
110
|
// Kept raw on purpose: `.catch(undefined)` would turn a mistyped `enabled` into "inherit",
|
|
109
111
|
// which can reopen a surface the operator meant to close. src/protocols/settings.ts parses it
|
|
110
112
|
// and fails closed instead.
|