@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-CppMz-6w.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
- package/gui/dist/assets/index-DQt9srNi.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
- package/gui/dist/index.html +4 -3
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/antigravity-refusal.ts +28 -0
- package/src/adapters/cursor/protobuf-request.ts +6 -0
- package/src/adapters/cursor/request-builder.ts +3 -0
- package/src/adapters/cursor/structured-output.ts +13 -0
- package/src/adapters/cursor/types.ts +2 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +62 -19
- package/src/adapters/openai-responses/passthrough.ts +133 -103
- package/src/adapters/openai-responses/request-strips.ts +10 -4
- package/src/adapters/zed.ts +26 -4
- package/src/bridge/response-json.ts +14 -14
- package/src/bridge/sse.ts +15 -5
- package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
- package/src/claude/agents-inject.ts +4 -43
- package/src/claude/gateway-cache.ts +16 -13
- package/src/claude/inbound-model-options.ts +9 -2
- package/src/claude/intercept/cli-catalog.ts +27 -16
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-bootstrap.ts +37 -16
- package/src/claude/intercept/picker-budget.ts +41 -0
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/claude/outbound.ts +2 -0
- package/src/claude/subagent-force-status.ts +53 -0
- package/src/claude/subagent-model.ts +93 -0
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +33 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +37 -9
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +29 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1190
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +35 -9
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/claude.ts +22 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +109 -0
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/connect.ts +19 -0
- package/src/cli/dispatch.ts +55 -82
- package/src/cli/doctor.ts +22 -4
- package/src/cli/export-command.ts +20 -9
- package/src/cli/gui.ts +8 -3
- package/src/cli/help-catalog.ts +101 -0
- package/src/cli/help-models-context.ts +17 -0
- package/src/cli/help-navigation.ts +50 -0
- package/src/cli/help-recovery.ts +66 -0
- package/src/cli/help.ts +69 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +9 -1
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/opencode.ts +54 -10
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +116 -64
- package/src/cli/registry.ts +90 -24
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +42 -9
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +57 -80
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/tray-proxy.ts +3 -2
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/contracts.ts +100 -0
- package/src/clients/config-export/droid.ts +54 -2
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export/kilo.ts +12 -2
- package/src/clients/config-export/model-metadata.ts +10 -4
- package/src/clients/config-export/reasoning-metadata.ts +47 -0
- package/src/clients/config-export.ts +53 -67
- package/src/clients/omo-role-models.ts +17 -3
- package/src/codex/account-credit-use.ts +4 -2
- package/src/codex/account-runtime-state.ts +21 -0
- package/src/codex/account-store.ts +11 -47
- package/src/codex/account-usability.ts +11 -8
- package/src/codex/auth-api/account-list.ts +73 -63
- package/src/codex/auth-api/main-account-probe.ts +25 -12
- package/src/codex/auth-api/pool-mode-gate.ts +6 -9
- package/src/codex/auth-api/pool-quota-probe.ts +2 -4
- package/src/codex/auth-api/reset-credit-service.ts +3 -2
- package/src/codex/auth-context.ts +30 -11
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/model-hints.ts +2 -1
- package/src/codex/catalog/model-visibility.ts +6 -3
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +13 -4
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +60 -11
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/chatgpt-refresh-failure.ts +56 -0
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +57 -16
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/main-account.ts +144 -11
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-refresh-outcome.ts +17 -2
- package/src/codex/quota-types.ts +36 -1
- package/src/codex/quota.ts +67 -16
- package/src/codex/refresh.ts +20 -9
- package/src/codex/routing/cooldown-math.ts +10 -4
- package/src/codex/routing/selection.ts +11 -4
- package/src/codex/routing.ts +9 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +145 -0
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +54 -107
- package/src/codex/subagent-model-fallback.ts +2 -1
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +101 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/jev.ts +13 -3
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +23 -1
- package/src/config/load-degrade.ts +7 -0
- package/src/config/provider-validation.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/compaction-triggers.ts +8 -0
- package/src/config/schema/config-schema.ts +43 -14
- package/src/config/schema/leaf-validators.ts +8 -3
- package/src/config/subagent-models.ts +12 -0
- package/src/generated/compatibility-version.json +645 -237
- package/src/grok/session-identity.ts +33 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +50 -5
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +106 -19
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +107 -35
- package/src/lib/gui-pair-capability.ts +17 -0
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/provider-client-headers.ts +62 -0
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-continuity.ts +65 -0
- package/src/oauth/anthropic-identity.ts +75 -0
- package/src/oauth/anthropic-routing.ts +20 -15
- package/src/oauth/anthropic.ts +34 -3
- package/src/oauth/generic-account-failover.ts +21 -0
- package/src/oauth/health.ts +32 -5
- package/src/oauth/index.ts +42 -35
- package/src/oauth/local-token-detect.ts +7 -0
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/oauth/refresh-policy.ts +2 -0
- package/src/oauth/store.ts +55 -6
- package/src/oauth/types.ts +9 -0
- package/src/protocols/encoders/messages.ts +4 -3
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/providers/antigravity-effort-families.ts +75 -0
- package/src/providers/antigravity-models.ts +54 -16
- package/src/providers/antigravity-wire-snapshot.ts +66 -0
- package/src/providers/derive.ts +2 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/new-model-policy.ts +48 -10
- package/src/providers/registry/entries-extended.ts +3 -0
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/model-seeds.ts +5 -0
- package/src/providers/registry/types.ts +4 -0
- package/src/responses/compaction.ts +19 -0
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +9 -0
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/chat-completions.ts +10 -0
- package/src/server/claude-messages.ts +29 -15
- package/src/server/droid-reasoning-default.ts +50 -0
- package/src/server/github-copilot-responses-repair.ts +3 -6
- package/src/server/grok-responses-control-frame.ts +2 -2
- package/src/server/grok-responses-snapshot-repair.ts +4 -5
- package/src/server/gui-session.ts +33 -11
- package/src/server/index/claude-intercept-lifecycle.ts +4 -0
- package/src/server/index/serve-options.ts +13 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +12 -134
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +69 -11
- package/src/server/management/link-routes.ts +20 -13
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/model-row-export-metadata.ts +150 -0
- package/src/server/management/model-rows.ts +148 -9
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +29 -27
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/subagent-model-routes.ts +214 -0
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/relay.ts +14 -44
- package/src/server/request-log-generation-window.ts +38 -0
- package/src/server/request-log.ts +13 -0
- package/src/server/responses/adapter-dispatch.ts +152 -28
- package/src/server/responses/codex-auth-error.ts +10 -2
- package/src/server/responses/combo-stream-preflight.ts +29 -3
- package/src/server/responses/compaction-routing.ts +10 -1
- package/src/server/responses/core-auth.ts +67 -30
- package/src/server/responses/core-codex-account.ts +6 -3
- package/src/server/responses/core-combo-failure.ts +5 -1
- package/src/server/responses/core-combo-native.ts +7 -1
- package/src/server/responses/core-combo.ts +64 -7
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/fetch-helpers.ts +9 -0
- package/src/server/responses/passthrough-delivery.ts +1 -1
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/request-prepare.ts +5 -0
- package/src/server/responses/request-spend.ts +30 -30
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/responses-hosted-image-display.ts +70 -9
- package/src/server/responses-terminal-repair.ts +15 -2
- package/src/server/sse-frame-buffer.ts +38 -36
- package/src/server/sse-payload-rewrite.ts +78 -49
- package/src/server/ws-bridge.ts +1 -11
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/service/windows-taskxml.ts +1 -2
- package/src/service-manager-probe.ts +36 -2
- package/src/types/config.ts +6 -0
- package/src/types/provider.ts +28 -10
- package/src/update/index.ts +2 -1
- package/src/update/npm-invocation.mjs +35 -3
- package/src/update/proxy-liveness-probe.mjs +38 -8
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/src/usage/expected-prices.ts +9 -0
- package/src/usage/log.ts +7 -0
- package/src/usage/summary.ts +82 -1
- package/src/web-search/loop.ts +11 -8
- package/src/web-search/run-turn-loop.ts +10 -7
- package/gui/dist/assets/App-CddorHdA.js +0 -51
- package/gui/dist/assets/App-DMudtISW.css +0 -1
- package/gui/dist/assets/index-Btze1I3T.css +0 -1
- package/gui/dist/assets/index-CvnY7E6i.js +0 -86
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Closed diagnostic vocabulary; provider descriptions and unknown codes never escape. */
|
|
2
|
+
const REFRESH_DIAGNOSTIC_CODES = [
|
|
3
|
+
"invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused",
|
|
4
|
+
"refresh_token_expired", "invalid_client", "invalid_request", "server_error",
|
|
5
|
+
"temporarily_unavailable", "rate_limit_exceeded", "unsupported_grant_type", "invalid_scope",
|
|
6
|
+
] as const;
|
|
7
|
+
export type ChatgptRefreshDiagnosticCode = typeof REFRESH_DIAGNOSTIC_CODES[number];
|
|
8
|
+
export type ChatgptRefreshFailure = {
|
|
9
|
+
reason: "expired" | "revoked" | "unknown";
|
|
10
|
+
code?: ChatgptRefreshDiagnosticCode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** HTTP availability failures cannot establish that a refresh grant was revoked. */
|
|
14
|
+
export function classifyChatgptRefreshFailure(status: number, body: string): ChatgptRefreshFailure {
|
|
15
|
+
if (body.length > 16_384) return { reason: "unknown" };
|
|
16
|
+
let payload: unknown;
|
|
17
|
+
try { payload = JSON.parse(body); } catch { return { reason: "unknown" }; }
|
|
18
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { reason: "unknown" };
|
|
19
|
+
const row = payload as Record<string, unknown>;
|
|
20
|
+
let exactCode: string | undefined;
|
|
21
|
+
let description = typeof row.error_description === "string" ? row.error_description : "";
|
|
22
|
+
if (typeof row.error === "string") {
|
|
23
|
+
exactCode = row.error.trim() || undefined;
|
|
24
|
+
} else if (row.error !== undefined) {
|
|
25
|
+
if (!row.error || typeof row.error !== "object" || Array.isArray(row.error)) return { reason: "unknown" };
|
|
26
|
+
const error = row.error as Record<string, unknown>;
|
|
27
|
+
if (error.code !== undefined && typeof error.code !== "string") return { reason: "unknown" };
|
|
28
|
+
exactCode = typeof error.code === "string" ? error.code.trim() || undefined : undefined;
|
|
29
|
+
if (typeof error.message === "string") description = `${error.message} ${description}`;
|
|
30
|
+
}
|
|
31
|
+
const code = (REFRESH_DIAGNOSTIC_CODES as readonly unknown[]).includes(exactCode)
|
|
32
|
+
? exactCode as ChatgptRefreshDiagnosticCode : undefined;
|
|
33
|
+
let reason: ChatgptRefreshFailure["reason"] = "unknown";
|
|
34
|
+
if (status === 400 || status === 401 || status === 403) {
|
|
35
|
+
if (["invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused"].includes(exactCode ?? "")) {
|
|
36
|
+
reason = "revoked";
|
|
37
|
+
} else if (exactCode === "refresh_token_expired") {
|
|
38
|
+
reason = "expired";
|
|
39
|
+
} else if (status === 400 && exactCode === undefined) {
|
|
40
|
+
// Preserve description-only OAuth 400 compatibility; a named code always wins.
|
|
41
|
+
if (description.includes("invalidated") || description.includes("revoked")) reason = "revoked";
|
|
42
|
+
else if (description.includes("expired")) reason = "expired";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { reason, ...(code ? { code } : {}) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Endpoint verdict only: no credential identity, body, or arbitrary upstream string. */
|
|
49
|
+
export function noteChatgptRefreshFailure(
|
|
50
|
+
kind: "pool" | "native main",
|
|
51
|
+
status: number,
|
|
52
|
+
failure: ChatgptRefreshFailure,
|
|
53
|
+
): void {
|
|
54
|
+
console.warn(`[codex] ${kind} refresh: ${failure.reason === "unknown" ? "transient" : "reauth"}`
|
|
55
|
+
+ ` status=${status} code=${failure.code ?? "none"}`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { getConfigDir } from "../config/paths";
|
|
4
|
+
import { samePathIdentity } from "./user-identity";
|
|
5
|
+
|
|
6
|
+
export const CODEX_HOME_JOURNAL_FILE = "opencodex-journal.json";
|
|
7
|
+
const MAX_JOURNAL_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
/** K imports this module; keep it independent of the config barrel and journal writer. */
|
|
10
|
+
export function currentOpencodexHome(): string {
|
|
11
|
+
const configured = getConfigDir();
|
|
12
|
+
try { return realpathSync.native(configured); }
|
|
13
|
+
catch { return resolve(configured); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CodexHomeOwnership =
|
|
17
|
+
| { readonly kind: "unbound" }
|
|
18
|
+
| { readonly kind: "owned" }
|
|
19
|
+
| { readonly kind: "stale"; readonly boundHome: string }
|
|
20
|
+
| { readonly kind: "foreign"; readonly boundHome: string }
|
|
21
|
+
| { readonly kind: "unknown" };
|
|
22
|
+
|
|
23
|
+
export type CodexHomeJournalInspection =
|
|
24
|
+
| { kind: "missing" }
|
|
25
|
+
| { kind: "read"; journal: Record<string, unknown> }
|
|
26
|
+
| { kind: "unknown" };
|
|
27
|
+
|
|
28
|
+
function absent(error: unknown): boolean {
|
|
29
|
+
return (error as NodeJS.ErrnoException)?.code === "ENOENT";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** ENOENT through a dangling link or an unresolvable ancestor is not proven absence. */
|
|
33
|
+
function provenAbsent(path: string): boolean {
|
|
34
|
+
try { lstatSync(path); return false; }
|
|
35
|
+
catch (error) { if (!absent(error)) return false; }
|
|
36
|
+
const parent = dirname(path);
|
|
37
|
+
if (parent === path) return false;
|
|
38
|
+
try { return statSync(realpathSync.native(parent)).isDirectory(); }
|
|
39
|
+
catch (error) { return absent(error) && provenAbsent(parent); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validJournal(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
44
|
+
const journal = value as Record<string, unknown>;
|
|
45
|
+
return journal.version === 1 && typeof journal.originalConfig === "string"
|
|
46
|
+
&& (journal.originalProfile === null || typeof journal.originalProfile === "string")
|
|
47
|
+
&& (journal.opencodexHome === undefined || (typeof journal.opencodexHome === "string"
|
|
48
|
+
&& isAbsolute(journal.opencodexHome) && !journal.opencodexHome.includes("\0")));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Bounded, regular-file-only observation. Failure never supplies cleanup authority. */
|
|
52
|
+
export function readCodexHomeJournal(journalPath: string): CodexHomeJournalInspection {
|
|
53
|
+
let fd: number | undefined;
|
|
54
|
+
try {
|
|
55
|
+
const before = lstatSync(journalPath);
|
|
56
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_JOURNAL_BYTES) return { kind: "unknown" };
|
|
57
|
+
fd = openSync(journalPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
58
|
+
const opened = fstatSync(fd);
|
|
59
|
+
if (!opened.isFile() || opened.size > MAX_JOURNAL_BYTES || before.dev !== opened.dev || before.ino !== opened.ino) return { kind: "unknown" };
|
|
60
|
+
const bytes = Buffer.alloc(opened.size + 1);
|
|
61
|
+
let length = 0;
|
|
62
|
+
while (length < bytes.length) {
|
|
63
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
64
|
+
if (!count) break;
|
|
65
|
+
length += count;
|
|
66
|
+
}
|
|
67
|
+
const after = lstatSync(journalPath);
|
|
68
|
+
if (length !== opened.size || !after.isFile() || after.dev !== opened.dev || after.ino !== opened.ino
|
|
69
|
+
|| after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) return { kind: "unknown" };
|
|
70
|
+
const journal: unknown = JSON.parse(bytes.subarray(0, length).toString("utf8"));
|
|
71
|
+
return validJournal(journal) ? { kind: "read", journal } : { kind: "unknown" };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return fd === undefined && absent(error) && provenAbsent(journalPath) ? { kind: "missing" } : { kind: "unknown" };
|
|
74
|
+
} finally { if (fd !== undefined) closeSync(fd); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inspectBinding(boundHome: string, current: string): CodexHomeOwnership {
|
|
78
|
+
let physical: string;
|
|
79
|
+
try {
|
|
80
|
+
physical = realpathSync.native(boundHome);
|
|
81
|
+
if (!statSync(physical).isDirectory()) return { kind: "unknown" };
|
|
82
|
+
} catch { return provenAbsent(boundHome) ? { kind: "stale", boundHome } : { kind: "unknown" }; }
|
|
83
|
+
try {
|
|
84
|
+
const own = realpathSync.native(current);
|
|
85
|
+
if (!statSync(own).isDirectory()) return { kind: "unknown" };
|
|
86
|
+
return samePathIdentity(physical, own) ? { kind: "owned" } : { kind: "foreign", boundHome };
|
|
87
|
+
} catch { return { kind: "unknown" }; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function inspectCodexHomeOwner(canonicalHome: string, current = currentOpencodexHome()): CodexHomeOwnership {
|
|
91
|
+
const read = readCodexHomeJournal(join(canonicalHome, CODEX_HOME_JOURNAL_FILE));
|
|
92
|
+
if (read.kind === "unknown") return { kind: "unknown" };
|
|
93
|
+
if (read.kind === "missing" || read.journal.opencodexHome === undefined) return { kind: "unbound" };
|
|
94
|
+
return inspectBinding(read.journal.opencodexHome as string, current);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Preserve extant and uncertain bindings; only legacy or proven stale records are adopted. */
|
|
98
|
+
export function opencodexHomeForInjection(recorded: string | undefined, current = currentOpencodexHome()): string {
|
|
99
|
+
if (recorded === undefined) return current;
|
|
100
|
+
return inspectBinding(recorded, current).kind === "stale" ? current : recorded;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type CodexHomeOwnerRefusalReason = "foreign-owner" | "owner-unknown";
|
|
104
|
+
export class CodexHomeOwnerRefusal extends Error {
|
|
105
|
+
constructor(readonly reason: CodexHomeOwnerRefusalReason) {
|
|
106
|
+
super(reason === "foreign-owner"
|
|
107
|
+
? "Codex home is bound to another OpenCodex home; configuration and journal were preserved. Restore from the owning home first."
|
|
108
|
+
: "Codex home ownership could not be verified; configuration and journal were preserved.");
|
|
109
|
+
this.name = "CodexHomeOwnerRefusal";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function assertCodexHomeOwner(home: string): void {
|
|
113
|
+
const owner = inspectCodexHomeOwner(home);
|
|
114
|
+
if (owner.kind === "foreign" || owner.kind === "unknown") {
|
|
115
|
+
throw new CodexHomeOwnerRefusal(owner.kind === "foreign" ? "foreign-owner" : "owner-unknown");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Compensation must not replace evidence another home published during this operation. */
|
|
120
|
+
export function codexHomeOwnerBlocksCompensation(home: string, error: unknown): boolean {
|
|
121
|
+
if (!(error instanceof CodexHomeOwnerRefusal)) return false;
|
|
122
|
+
const owner = inspectCodexHomeOwner(home);
|
|
123
|
+
return owner.kind === "foreign" || owner.kind === "unknown";
|
|
124
|
+
}
|
package/src/codex/convergence.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { projectAntigravitySelectedModels } from "../providers/antigravity-effort-families";
|
|
1
2
|
import { join } from "node:path";
|
|
3
|
+
import { sameCatalogHealPath } from "./catalog/heal-observation";
|
|
2
4
|
|
|
3
5
|
import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
4
6
|
import { reconcileSuccessfulModelDiscoveries } from "../providers/new-model-policy";
|
|
@@ -82,11 +84,20 @@ import {
|
|
|
82
84
|
type CodexModelEntitlementSnapshot,
|
|
83
85
|
} from "./model-entitlements";
|
|
84
86
|
import { resolveAdmittedCodexModelEntitlements } from "./model-entitlement-admission";
|
|
87
|
+
import {
|
|
88
|
+
FOREIGN_CODEX_HOME_OWNER_MESSAGE,
|
|
89
|
+
UNKNOWN_CODEX_HOME_OWNER_MESSAGE,
|
|
90
|
+
routedRemovalBackedByConfigFile,
|
|
91
|
+
unbackedRoutedRemovalMessage,
|
|
92
|
+
unconfiguredRoutedRemoval,
|
|
93
|
+
type UnconfiguredRoutedRemoval,
|
|
94
|
+
} from "./catalog/routed-removal";
|
|
85
95
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
86
96
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
87
97
|
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
88
98
|
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
89
99
|
import {
|
|
100
|
+
auditRefusedCatalogReplacement,
|
|
90
101
|
publishHashedCodexCatalogBackup,
|
|
91
102
|
publishLegacyCodexCatalogBackup,
|
|
92
103
|
replaceActiveCodexCatalog,
|
|
@@ -108,14 +119,20 @@ import type {
|
|
|
108
119
|
export interface CatalogWriteReceipt {
|
|
109
120
|
readonly keyedBackup: "written" | "preserved" | "not-requested";
|
|
110
121
|
readonly legacyBackup: "written" | "preserved" | "not-requested";
|
|
111
|
-
|
|
112
|
-
readonly
|
|
122
|
+
/** `unchanged`: the bytes on disk already matched, so nothing was rewritten. */
|
|
123
|
+
readonly catalog: "written" | "unchanged" | "not-written";
|
|
124
|
+
readonly cache: "written" | "unchanged" | "not-written";
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
export type CodexCatalogCommitResult =
|
|
116
128
|
| { readonly kind: "committed"; readonly changed: boolean; readonly writes: CatalogWriteReceipt }
|
|
117
129
|
| { readonly kind: "stale"; readonly reason: "generation" | "home-selection" | "source-observation" | "process-local" | "target-identity" | "candidate-consumed" | "account-entitlement" }
|
|
118
|
-
|
|
130
|
+
/**
|
|
131
|
+
* `unbacked-routed-removal`: the candidate drops routed namespaces config.json does not back, or
|
|
132
|
+
* clears every routed row while config.json is unreadable. `foreign-owner`: this Codex home is
|
|
133
|
+
* bound to another OPENCODEX_HOME (#6529).
|
|
134
|
+
*/
|
|
135
|
+
| { readonly kind: "refused"; readonly reason: "source-unreadable" | "source-ambiguous" | "target-unsafe" | "unbacked-routed-removal" | "foreign-owner" | "owner-unknown" }
|
|
119
136
|
| { readonly kind: "failed"; readonly surface: "disk"; readonly writes: CatalogWriteReceipt };
|
|
120
137
|
|
|
121
138
|
declare const catalogCandidateBrand: unique symbol;
|
|
@@ -139,10 +156,11 @@ interface CandidateState {
|
|
|
139
156
|
readonly cache: PreparedCatalogFileWrite;
|
|
140
157
|
readonly keyedBackup?: PreparedCatalogFileWrite;
|
|
141
158
|
readonly legacyBackup?: PreparedCatalogFileWrite;
|
|
142
|
-
readonly changed: boolean;
|
|
143
159
|
readonly notices: readonly CatalogNotice[];
|
|
144
160
|
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
145
161
|
readonly discoveryConfig?: OcxConfig;
|
|
162
|
+
/** Routed namespaces this candidate empties only because the driving config lacks them. */
|
|
163
|
+
readonly routedRemoval: UnconfiguredRoutedRemoval | null;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
const candidateStates = new WeakMap<object, CandidateState>();
|
|
@@ -347,7 +365,7 @@ function prepareCatalog(
|
|
|
347
365
|
const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
|
|
348
366
|
enabledProviders.flatMap(([name, provider]) => (
|
|
349
367
|
Array.isArray(provider.selectedModels) && provider.selectedModels.length > 0
|
|
350
|
-
? [[name, new Set(provider.selectedModels)] as const]
|
|
368
|
+
? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, routedModels))] as const]
|
|
351
369
|
: []
|
|
352
370
|
)),
|
|
353
371
|
);
|
|
@@ -536,11 +554,10 @@ export async function gatherCodexCatalogCandidate(
|
|
|
536
554
|
...(pristineBytes ? { keyedBackup: { path: paths.keyedBackup, content: pristineBytes } } : {}),
|
|
537
555
|
...(pristineBytes && paths.legacyBackup
|
|
538
556
|
? { legacyBackup: { path: paths.legacyBackup, content: pristineBytes } } : {}),
|
|
539
|
-
changed: Buffer.from(activeBytes ?? []).toString("utf8") !== preparedCatalogBytes
|
|
540
|
-
|| Buffer.from(cacheBytes ?? []).toString("utf8") !== preparedCacheBytes,
|
|
541
557
|
notices: Object.freeze([...notices]),
|
|
542
558
|
modelEntitlements,
|
|
543
559
|
...(discoveryChanged ? { discoveryConfig } : {}),
|
|
560
|
+
routedRemoval: unconfiguredRoutedRemoval(active, preparedCatalog, snapshot.config),
|
|
544
561
|
});
|
|
545
562
|
return { kind: "candidate", candidate };
|
|
546
563
|
} catch (error) {
|
|
@@ -625,11 +642,14 @@ function fixedCommit(state: CandidateState, permit: Parameters<typeof replaceAct
|
|
|
625
642
|
if (state.legacyBackup) {
|
|
626
643
|
writes = { ...writes, legacyBackup: publishLegacyCodexCatalogBackup(permit, state.home, state.legacyBackup) };
|
|
627
644
|
}
|
|
628
|
-
replaceActiveCodexCatalog(permit, state.home, state.catalog);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
return { kind: "
|
|
645
|
+
const catalog = replaceActiveCodexCatalog(permit, state.home, state.catalog);
|
|
646
|
+
// The funnel's backstop: every routed row would go while config.json is not a readable file.
|
|
647
|
+
// Nothing past the pristine backups was written, and the cache must not describe a catalog
|
|
648
|
+
// that was never published.
|
|
649
|
+
if (catalog.kind === "refused") return { kind: "refused", reason: "unbacked-routed-removal" };
|
|
650
|
+
writes = { ...writes, catalog: catalog.kind };
|
|
651
|
+
writes = { ...writes, cache: replaceCodexModelsCache(permit, state.home, state.cache).kind };
|
|
652
|
+
return { kind: "committed", changed: writes.catalog === "written" || writes.cache === "written", writes };
|
|
633
653
|
} catch {
|
|
634
654
|
return { kind: "failed", surface: "disk", writes };
|
|
635
655
|
}
|
|
@@ -638,6 +658,7 @@ function fixedCommit(state: CandidateState, permit: Parameters<typeof replaceAct
|
|
|
638
658
|
export async function commitCodexCatalogCandidate(
|
|
639
659
|
candidate: CodexCatalogCandidate,
|
|
640
660
|
deadlineMs: number,
|
|
661
|
+
lifecycle: Readonly<{ beforeCommit?: () => boolean; expectedCatalogPath?: string }> = {},
|
|
641
662
|
): Promise<CommitAttempt> {
|
|
642
663
|
const state = candidateStates.get(candidate as object);
|
|
643
664
|
if (!state) return { kind: "refused", reason: "source-ambiguous" };
|
|
@@ -648,13 +669,33 @@ export async function commitCodexCatalogCandidate(
|
|
|
648
669
|
state.consumed = true;
|
|
649
670
|
const guarded = withExpectedConfigGenerationSync(state.generation, () => {
|
|
650
671
|
const invalid = revalidateCandidate(state);
|
|
651
|
-
|
|
672
|
+
if (invalid) return invalid;
|
|
673
|
+
if (lifecycle.expectedCatalogPath !== undefined && !sameCatalogHealPath(state.catalog.path, lifecycle.expectedCatalogPath)) {
|
|
674
|
+
return { kind: "stale", reason: "target-identity" } as const;
|
|
675
|
+
}
|
|
676
|
+
if (lifecycle.beforeCommit && lifecycle.beforeCommit() !== true) return { kind: "stale", reason: "process-local" } as const;
|
|
677
|
+
// A refresh may empty a routed namespace only when config.json on disk agrees it is gone.
|
|
678
|
+
// Read under K, so a config that fell back to defaults during a transient read failure,
|
|
679
|
+
// or that belongs to another OPENCODEX_HOME, cannot publish a native-only catalog (#6529).
|
|
680
|
+
if (state.routedRemoval !== null && !routedRemovalBackedByConfigFile(state.routedRemoval)) {
|
|
681
|
+
auditRefusedCatalogReplacement(permit, state.home, state.catalog, "unbacked-routed-removal");
|
|
682
|
+
console.warn(`[opencodex] ${unbackedRoutedRemovalMessage(state.routedRemoval.namespaces.length)}`);
|
|
683
|
+
return { kind: "refused", reason: "unbacked-routed-removal" } as const;
|
|
684
|
+
}
|
|
685
|
+
return fixedCommit(state, permit);
|
|
652
686
|
});
|
|
653
687
|
if (guarded.kind === "conflict") return { kind: "stale", reason: "generation" } as const;
|
|
654
688
|
if (guarded.kind === "unavailable") return { kind: "busy" } as const;
|
|
655
689
|
return guarded.value;
|
|
656
|
-
});
|
|
690
|
+
}, { intent: "refresh", writer: "convergence" });
|
|
657
691
|
if (acquired.kind === "completed") return acquired.value;
|
|
692
|
+
if (acquired.reason === "foreign-owner" || acquired.reason === "owner-unknown") {
|
|
693
|
+
// Not retryable: another OPENCODEX_HOME owns this Codex home until it restores (#6529).
|
|
694
|
+
state.consumed = true;
|
|
695
|
+
console.warn(`[opencodex] ${acquired.reason === "foreign-owner"
|
|
696
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE : UNKNOWN_CODEX_HOME_OWNER_MESSAGE}`);
|
|
697
|
+
return { kind: "refused", reason: acquired.reason };
|
|
698
|
+
}
|
|
658
699
|
if (acquired.reason !== "busy" || Date.now() >= deadline) return { kind: "busy" };
|
|
659
700
|
await Bun.sleep(Math.min(10, Math.max(1, deadline - Date.now())));
|
|
660
701
|
}
|
|
@@ -675,7 +716,7 @@ function projectCommit(result: CommitAttempt, notices: readonly CatalogNotice[])
|
|
|
675
716
|
export async function convergeCodexCatalog(
|
|
676
717
|
snapshot: CatalogAdmissionSnapshot,
|
|
677
718
|
request: ConvergeRequest,
|
|
678
|
-
lifecycle: Readonly<{ onCommitBegin?: () => void }> = {},
|
|
719
|
+
lifecycle: Readonly<{ onCommitBegin?: () => void; beforeCommit?: () => boolean; expectedCatalogPath?: string }> = {},
|
|
679
720
|
): Promise<Readonly<{ changed: boolean; catalogRefresh: CatalogDisposition }>> {
|
|
680
721
|
if (request.scope !== "catalog" || request.action !== "converge") {
|
|
681
722
|
return {
|
|
@@ -687,7 +728,7 @@ export async function convergeCodexCatalog(
|
|
|
687
728
|
if (gathered.kind === "disposition") return { changed: false, catalogRefresh: gathered.disposition };
|
|
688
729
|
const state = candidateStates.get(gathered.candidate as object)!;
|
|
689
730
|
lifecycle.onCommitBegin?.();
|
|
690
|
-
const committed = await commitCodexCatalogCandidate(gathered.candidate, request.deadlineMs);
|
|
731
|
+
const committed = await commitCodexCatalogCandidate(gathered.candidate, request.deadlineMs, lifecycle);
|
|
691
732
|
if (committed.kind === "committed" && state.discoveryConfig) {
|
|
692
733
|
const mutable = snapshot.config as OcxConfig;
|
|
693
734
|
mutable.modelDiscovery = state.discoveryConfig.modelDiscovery;
|
package/src/codex/features.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWri
|
|
|
38
38
|
import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
|
|
39
39
|
import { CODEX_CONFIG_PATH } from "./paths";
|
|
40
40
|
import { resolveAndPersistCodexRuntime } from "./runtime";
|
|
41
|
+
import { decodeOverlayState } from "./shim-state-file";
|
|
41
42
|
import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy";
|
|
42
43
|
|
|
43
44
|
/** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
|
|
@@ -1232,13 +1233,19 @@ function codexNativeBinaryCandidates(command: string): string[] {
|
|
|
1232
1233
|
/** Backing paths tied to this exact OCX shim entry in codex-shim.json. */
|
|
1233
1234
|
function selectedShimBackingPaths(commandPath: string): string[] {
|
|
1234
1235
|
try {
|
|
1235
|
-
const
|
|
1236
|
+
const configDir = getConfigDir();
|
|
1237
|
+
const state = JSON.parse(readFileSync(join(configDir, "codex-shim.json"), "utf8")) as {
|
|
1238
|
+
schema?: unknown;
|
|
1239
|
+
mode?: unknown;
|
|
1236
1240
|
wrapperPath?: unknown;
|
|
1237
1241
|
originalPath?: unknown;
|
|
1238
1242
|
backupPath?: unknown;
|
|
1239
1243
|
realPath?: unknown;
|
|
1240
1244
|
wrappers?: Array<Record<string, unknown>>;
|
|
1241
1245
|
};
|
|
1246
|
+
const overlay = decodeOverlayState(state, configDir);
|
|
1247
|
+
if (overlay) return resolve(overlay.wrapperPath) === resolve(commandPath) ? [overlay.launcherPath] : [];
|
|
1248
|
+
if (state.schema !== undefined || state.mode !== undefined) return [];
|
|
1242
1249
|
const entries = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
|
|
1243
1250
|
const selected = resolve(commandPath);
|
|
1244
1251
|
for (const entry of entries) {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { assertCodexHomeOwner, codexHomeOwnerBlocksCompensation, CodexHomeOwnerRefusal, type CodexHomeOwnerRefusalReason } from "../codex-home-owner";
|
|
1
2
|
import { loadConfig } from "../../config";
|
|
2
3
|
import { shouldSyncCodexOnStart } from "../desired-state";
|
|
3
4
|
import { siblingOfLivePort, siblingSkipMessage } from "../sibling-start";
|
|
4
5
|
import { withCatalogWriteSerialization } from "../catalog-write-serialization";
|
|
5
6
|
import { restoreCodexCatalogWithPermit } from "../catalog/sync";
|
|
7
|
+
import { readCodexCatalogPath } from "../catalog/parsing";
|
|
8
|
+
import { notifyCatalogPublication } from "../catalog/publication-observer";
|
|
6
9
|
import { withCodexWriteLock, CodexWriteLockSkipped } from "../codex-write-lock";
|
|
7
10
|
import { inspectNativeCodexOwnership } from "../../integrations/native/ownership-preflight";
|
|
8
11
|
import { resolveCodexHistoryTransition } from "../history-transition";
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
import {
|
|
25
28
|
journaledInjectedCatalogPath,
|
|
26
29
|
removeJournal,
|
|
30
|
+
releaseJournalHomeBinding,
|
|
27
31
|
restoreJournalState,
|
|
28
32
|
} from "../journal";
|
|
29
33
|
import {
|
|
@@ -128,6 +132,7 @@ export interface CodexRestoreHistoryResult {
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
export interface CodexNativeRestoreResult {
|
|
135
|
+
ownershipRefusal?: CodexHomeOwnerRefusalReason;
|
|
131
136
|
success: boolean;
|
|
132
137
|
message: string;
|
|
133
138
|
externalProvider?: string;
|
|
@@ -339,6 +344,9 @@ export interface RestoreConfigOptions {
|
|
|
339
344
|
|
|
340
345
|
/** The config/profile half of a native restore, reported as one artifact. */
|
|
341
346
|
function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions = {}): CodexRestoreConfigResult {
|
|
347
|
+
assertCodexHomeOwner(getCodexHome());
|
|
348
|
+
beforeRestoreConfigForTests?.(kind);
|
|
349
|
+
assertCodexHomeOwner(getCodexHome());
|
|
342
350
|
const preImages = captureCodexPreImages();
|
|
343
351
|
const result = restoreCodexConfigInlineImpl(kind, options);
|
|
344
352
|
if (result.state === "failed") {
|
|
@@ -350,7 +358,6 @@ function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions =
|
|
|
350
358
|
|
|
351
359
|
function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOptions): CodexRestoreConfigResult {
|
|
352
360
|
try {
|
|
353
|
-
beforeRestoreConfigForTests?.(kind);
|
|
354
361
|
const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
|
|
355
362
|
if (disposition.kind === "refuse") {
|
|
356
363
|
return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${disposition.reason}.` };
|
|
@@ -368,12 +375,20 @@ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOption
|
|
|
368
375
|
// accepting that tagged conversations stop opening.
|
|
369
376
|
const capturedBlock = options.removeProviderTable === true ? null : readOcxProviderTableBlock();
|
|
370
377
|
const journal = restoreJournalState();
|
|
378
|
+
if (journal.ownershipRefusal) throw new CodexHomeOwnerRefusal(journal.ownershipRefusal);
|
|
371
379
|
if (journal.unverified) {
|
|
372
380
|
return {
|
|
373
381
|
state: "failed", changed: false, action: "failed",
|
|
374
382
|
message: "Codex journal recovery was not verified; current configuration files and the journal were preserved.",
|
|
375
383
|
};
|
|
376
384
|
}
|
|
385
|
+
if (journal.profileRestoreFailed) {
|
|
386
|
+
return {
|
|
387
|
+
state: "failed", changed: false, action: "failed",
|
|
388
|
+
message: "Codex profile could not be restored; native files and the journal binding were preserved.",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
assertCodexHomeOwner(getCodexHome());
|
|
377
392
|
const restored = journal.configRestored
|
|
378
393
|
? { success: true, message: "Codex config restored from opencodex journal.", retainedProviderTable: undefined as string[] | undefined }
|
|
379
394
|
: removeCodexConfig({
|
|
@@ -403,6 +418,9 @@ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOption
|
|
|
403
418
|
if (settled.kind === "stand-down" && settled.retainProviderTable && capturedBlock !== null) {
|
|
404
419
|
retainedLines = retainOcxProviderTableOnDisk(capturedBlock) ?? retainedLines;
|
|
405
420
|
}
|
|
421
|
+
// All native work and the final migration check succeeded. A field-level
|
|
422
|
+
// fallback retains its snapshot, but no longer owns this Codex home.
|
|
423
|
+
releaseJournalHomeBinding();
|
|
406
424
|
}
|
|
407
425
|
if (restored.success && retainedLines !== null) {
|
|
408
426
|
return {
|
|
@@ -430,6 +448,7 @@ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOption
|
|
|
430
448
|
}
|
|
431
449
|
: { state: "failed", changed: false, action: "failed", message: restored.message };
|
|
432
450
|
} catch (error) {
|
|
451
|
+
if (error instanceof CodexHomeOwnerRefusal) throw error;
|
|
433
452
|
return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
434
453
|
}
|
|
435
454
|
}
|
|
@@ -451,7 +470,8 @@ function restoreCodexCatalogArtifact(
|
|
|
451
470
|
const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
452
471
|
revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
|
|
453
472
|
? null
|
|
454
|
-
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath)
|
|
473
|
+
: restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath),
|
|
474
|
+
{ intent: "restore", writer: "codex-restore" });
|
|
455
475
|
return restored.kind === "completed" && restored.value !== null
|
|
456
476
|
? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
|
|
457
477
|
: restored.kind === "completed"
|
|
@@ -487,8 +507,10 @@ export async function restoreNativeCodexAsync(
|
|
|
487
507
|
const sibling = siblingRestoreSkip();
|
|
488
508
|
if (sibling) return sibling;
|
|
489
509
|
try {
|
|
510
|
+
assertCodexHomeOwner(getCodexHome());
|
|
490
511
|
return await restoreNativeCodexAsyncImpl(options);
|
|
491
512
|
} catch (error) {
|
|
513
|
+
if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
|
|
492
514
|
if (!(error instanceof CodexRestoreRefusal)) throw error;
|
|
493
515
|
return failedConfigRestoreEnvelope(error.config);
|
|
494
516
|
}
|
|
@@ -533,7 +555,7 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
533
555
|
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
534
556
|
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
535
557
|
// catalog we actually wrote (#1798).
|
|
536
|
-
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
558
|
+
const journaledCatalogPath = journaledInjectedCatalogPath() ?? readCodexCatalogPath();
|
|
537
559
|
let config: CodexRestoreConfigResult;
|
|
538
560
|
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
539
561
|
|
|
@@ -549,6 +571,7 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
549
571
|
readAdmissionUnderLock: () => witness,
|
|
550
572
|
},
|
|
551
573
|
(ctx) => {
|
|
574
|
+
assertCodexHomeOwner(getCodexHome());
|
|
552
575
|
if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
|
|
553
576
|
throw new CodexWriteLockSkipped("desired_enabled");
|
|
554
577
|
}
|
|
@@ -576,6 +599,7 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
576
599
|
// Throw inside N so the published remove transition rolls back too.
|
|
577
600
|
if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
|
|
578
601
|
} catch (error) {
|
|
602
|
+
if (codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error;
|
|
579
603
|
const compensated = restoreCodexPreImages(preImages);
|
|
580
604
|
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
581
605
|
throw error;
|
|
@@ -619,6 +643,7 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
619
643
|
}
|
|
620
644
|
|
|
621
645
|
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
646
|
+
notifyCatalogPublication({ kind: "native-released", path: journaledCatalogPath });
|
|
622
647
|
const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
|
|
623
648
|
// Re-asked after the config half, because the store can paginate mid-transaction. Deciding
|
|
624
649
|
// the history job from the pre-write answer alone would spawn a Worker whose own preflight
|
|
@@ -681,11 +706,27 @@ async function restoreNativeCodexAsyncImpl(
|
|
|
681
706
|
};
|
|
682
707
|
}
|
|
683
708
|
|
|
709
|
+
function homeOwnerRestoreRefusal(error: CodexHomeOwnerRefusal): CodexNativeRestoreResult {
|
|
710
|
+
return { ...skippedRestoreEnvelope(false, error.message), ownershipRefusal: error.reason };
|
|
711
|
+
}
|
|
712
|
+
|
|
684
713
|
export function restoreNativeCodex(
|
|
685
714
|
options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
686
715
|
): CodexNativeRestoreResult {
|
|
687
716
|
const sibling = siblingRestoreSkip();
|
|
688
717
|
if (sibling) return sibling;
|
|
718
|
+
try {
|
|
719
|
+
assertCodexHomeOwner(getCodexHome());
|
|
720
|
+
return restoreNativeCodexImpl(options);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function restoreNativeCodexImpl(
|
|
728
|
+
options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
|
|
729
|
+
): CodexNativeRestoreResult {
|
|
689
730
|
const activeProvider = currentExternalCodexModelProvider();
|
|
690
731
|
if (activeProvider) {
|
|
691
732
|
removeJournal();
|
|
@@ -700,9 +741,10 @@ export function restoreNativeCodex(
|
|
|
700
741
|
// Captured before the config half: a successful journal restore DELETES the journal, and
|
|
701
742
|
// restoring the config can drop `model_catalog_json`. Either one would hide the routed
|
|
702
743
|
// catalog we actually wrote (#1798).
|
|
703
|
-
const journaledCatalogPath = journaledInjectedCatalogPath();
|
|
744
|
+
const journaledCatalogPath = journaledInjectedCatalogPath() ?? readCodexCatalogPath();
|
|
704
745
|
const config = restoreCodexConfigInline("sync", options);
|
|
705
746
|
if (config.state === "failed") return failedConfigRestoreEnvelope(config);
|
|
747
|
+
notifyCatalogPublication({ kind: "native-released", path: journaledCatalogPath });
|
|
706
748
|
// Same mid-transaction pagination re-check as the async path.
|
|
707
749
|
const historyStoodDown = historyStandsDown
|
|
708
750
|
|| resolveRestoreHistoryDisposition(options.removeProviderTable).kind === "stand-down";
|
package/src/codex/inject.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readConfigAdmissionSnapshot,
|
|
8
8
|
withConfigMutationLockSync,
|
|
9
9
|
} from "../config";
|
|
10
|
+
import { assertCodexHomeOwner, codexHomeOwnerBlocksCompensation, CodexHomeOwnerRefusal, type CodexHomeOwnerRefusalReason } from "./codex-home-owner";
|
|
10
11
|
import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
|
|
11
12
|
import {
|
|
12
13
|
localClientSkipMessage,
|
|
@@ -134,6 +135,7 @@ function runClientWriteGuard(guard: InjectCodexOptions["beforeClientWrite"]): vo
|
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
export interface CodexInjectResult {
|
|
138
|
+
ownershipRefusal?: CodexHomeOwnerRefusalReason;
|
|
137
139
|
success: boolean;
|
|
138
140
|
message: string;
|
|
139
141
|
/** False when injection intentionally preserves configuration owned by another provider. */
|
|
@@ -192,8 +194,9 @@ export async function injectCodexConfig(
|
|
|
192
194
|
if (siblingOfLivePort() !== null) {
|
|
193
195
|
return { success: true, status: "skipped", skippedReason: "sibling", message: siblingSkipMessage() };
|
|
194
196
|
}
|
|
195
|
-
try { return await injectCodexConfigImpl(port, config, options); }
|
|
197
|
+
try { assertCodexHomeOwner(getCodexHome()); return await injectCodexConfigImpl(port, config, options); }
|
|
196
198
|
catch (error) {
|
|
199
|
+
if (error instanceof CodexHomeOwnerRefusal) return { success: false, ownershipRefusal: error.reason, message: error.message };
|
|
197
200
|
if (error instanceof CodexHistoryPreflightRefusal) return { success: false, historyPreflightFailureReason: error.message, message: `Codex config injection refused: ${error.message}. Existing configuration and history were preserved.` };
|
|
198
201
|
if (error instanceof CodexInjectRefusal) return error.result;
|
|
199
202
|
throw error;
|
|
@@ -415,6 +418,7 @@ async function injectCodexConfigImpl(
|
|
|
415
418
|
* flip included — before the result is reported.
|
|
416
419
|
*/
|
|
417
420
|
const reconcileAndDerivePlan = (): { plan: CodexInjectionPlanOk; nativeInput: string } => {
|
|
421
|
+
assertCodexHomeOwner(getCodexHome());
|
|
418
422
|
if (missingConfig) createEmptyCodexConfigInBoundary();
|
|
419
423
|
let nativeInput = rawContent;
|
|
420
424
|
let plan = admittedPlan;
|
|
@@ -453,6 +457,7 @@ async function injectCodexConfigImpl(
|
|
|
453
457
|
beforeHistoryArtifactCommitForTests?.(eligibility.kind);
|
|
454
458
|
plan.historyRelabelRefusal = observeHistoryRefusalOrThrow(plan);
|
|
455
459
|
historyArtifactStageForTests?.("after-preflight");
|
|
460
|
+
assertCodexHomeOwner(getCodexHome());
|
|
456
461
|
writeJournal({
|
|
457
462
|
currentStateIsNative: journalBaselineIsNative(nativeInput),
|
|
458
463
|
configContent: plan.baselineContent,
|
|
@@ -463,6 +468,7 @@ async function injectCodexConfigImpl(
|
|
|
463
468
|
if (hasUnverifiedJournalBaseline(plan.baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
|
|
464
469
|
atomicWriteFile(CODEX_CONFIG_PATH, plan.content);
|
|
465
470
|
historyArtifactStageForTests?.("after-config");
|
|
471
|
+
assertCodexHomeOwner(getCodexHome());
|
|
466
472
|
atomicWriteFile(CODEX_PROFILE_PATH, plan.profileContent);
|
|
467
473
|
markJournalInjectedState(plan.content, plan.profileContent, {
|
|
468
474
|
// A root override is ours whenever we wrote one and no user-owned value won. That is
|
|
@@ -526,6 +532,7 @@ async function injectCodexConfigImpl(
|
|
|
526
532
|
};
|
|
527
533
|
}
|
|
528
534
|
runClientWriteGuard(options.beforeClientWrite);
|
|
535
|
+
assertCodexHomeOwner(getCodexHome());
|
|
529
536
|
/*
|
|
530
537
|
* One preimage covers the reconcile and the artifact commit together: a
|
|
531
538
|
* refusal after the feature transition hands back the exact bytes the
|
|
@@ -537,6 +544,7 @@ async function injectCodexConfigImpl(
|
|
|
537
544
|
applyNativeArtifacts(resolved.plan, resolved.nativeInput);
|
|
538
545
|
effectivePlan = resolved.plan;
|
|
539
546
|
} catch (error) {
|
|
547
|
+
if (codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error;
|
|
540
548
|
const restored = restoreCodexPreImages(preImages);
|
|
541
549
|
if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored);
|
|
542
550
|
throw error;
|
|
@@ -582,6 +590,7 @@ async function injectCodexConfigImpl(
|
|
|
582
590
|
// transition or capturing preimages; rejection must not compensate over
|
|
583
591
|
// a disconnect's restored files.
|
|
584
592
|
runClientWriteGuard(options.beforeClientWrite);
|
|
593
|
+
assertCodexHomeOwner(getCodexHome());
|
|
585
594
|
/*
|
|
586
595
|
* Exact pre-images, captured under the lock and used for compensation.
|
|
587
596
|
*
|
|
@@ -630,6 +639,7 @@ async function injectCodexConfigImpl(
|
|
|
630
639
|
}
|
|
631
640
|
applyNativeArtifacts(resolved.plan, resolved.nativeInput);
|
|
632
641
|
} catch (error) {
|
|
642
|
+
if (codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error;
|
|
633
643
|
// Compensate, then ALWAYS throw. Returning a partial result would let the
|
|
634
644
|
// lock commit a row describing an apply that did not finish.
|
|
635
645
|
const restored = restoreCodexPreImages(preImages);
|