@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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { createClineIO } from "./cline-io";
|
|
12
12
|
import { parseClineDocument } from "./cline-document";
|
|
13
|
-
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
13
|
+
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, droidDefaultsFromOwnedRows, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
14
14
|
import type { ConfigFormat } from "../clients/config-export";
|
|
15
15
|
import type { OcxConfig } from "../types";
|
|
16
16
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
} from "./merge";
|
|
26
26
|
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
27
27
|
import {
|
|
28
|
+
droidNormalizedContributionMatchesRecord,
|
|
29
|
+
droidNormalizedFileMatchesRecord,
|
|
28
30
|
isHermesAffinityUpgrade,
|
|
29
31
|
protectedContributionFingerprint,
|
|
30
32
|
refreshablePathsOf,
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
unresolvedPathHintFor,
|
|
41
43
|
type IntegrationClientId,
|
|
42
44
|
} from "./registry";
|
|
43
|
-
import { resolveIntegrationTarget, type IntegrationTarget } from "./target";
|
|
45
|
+
import { resolveIntegrationTarget, type IneffectiveWriteReason, type IntegrationTarget } from "./target";
|
|
44
46
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
45
47
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
46
48
|
|
|
@@ -84,6 +86,13 @@ export interface IntegrationStatus {
|
|
|
84
86
|
* `current` has to be able to report this beside it.
|
|
85
87
|
*/
|
|
86
88
|
supersededBy?: string;
|
|
89
|
+
/** Why `supersededBy` is not written; present exactly when it is. */
|
|
90
|
+
supersededReason?: IneffectiveWriteReason;
|
|
91
|
+
/**
|
|
92
|
+
* `missing-store` only: the document that recreates the store. A surface that localizes the
|
|
93
|
+
* remedy needs the content to name, not the writer's English sentence.
|
|
94
|
+
*/
|
|
95
|
+
missingStoreDocument?: string;
|
|
87
96
|
/** Snapshot files retained for this client; -1 when they cannot be inspected. */
|
|
88
97
|
snapshotCount: number;
|
|
89
98
|
/** Pruning is behind, so older (possibly credential-bearing) snapshots remain. */
|
|
@@ -208,6 +217,18 @@ function recordedContribution(
|
|
|
208
217
|
};
|
|
209
218
|
}
|
|
210
219
|
|
|
220
|
+
function observedContributionMatchesRecord(
|
|
221
|
+
observed: ManagedContribution,
|
|
222
|
+
record: OwnershipRecord,
|
|
223
|
+
): boolean {
|
|
224
|
+
if (fingerprint(canonicalContribution(observed)) === record.blockFingerprint) return true;
|
|
225
|
+
if (
|
|
226
|
+
typeof record.semanticBlockFingerprint === "string"
|
|
227
|
+
&& fingerprint(semanticContribution(observed)) === record.semanticBlockFingerprint
|
|
228
|
+
) return true;
|
|
229
|
+
return droidNormalizedContributionMatchesRecord(observed, record);
|
|
230
|
+
}
|
|
231
|
+
|
|
211
232
|
/**
|
|
212
233
|
* Prove that every protected field still matches what OpenCodex wrote.
|
|
213
234
|
*
|
|
@@ -223,13 +244,8 @@ function recordedBlockIsOwned(
|
|
|
223
244
|
): boolean {
|
|
224
245
|
const observed = recordedContribution(doc, record);
|
|
225
246
|
if (!observed) return false;
|
|
226
|
-
if (
|
|
227
|
-
|
|
247
|
+
if (observedContributionMatchesRecord(observed, record)) return true;
|
|
228
248
|
const observedSemanticFingerprint = fingerprint(semanticContribution(observed));
|
|
229
|
-
if (
|
|
230
|
-
typeof record.semanticBlockFingerprint === "string"
|
|
231
|
-
&& observedSemanticFingerprint === record.semanticBlockFingerprint
|
|
232
|
-
) return true;
|
|
233
249
|
|
|
234
250
|
const desiredFingerprint = fingerprint(canonicalContribution(desired));
|
|
235
251
|
if (
|
|
@@ -304,6 +320,12 @@ export function classifyIntegration(input: {
|
|
|
304
320
|
* the wrong way.
|
|
305
321
|
*/
|
|
306
322
|
format?: ConfigFormat;
|
|
323
|
+
/**
|
|
324
|
+
* Whether the file being classified is patched in place, which makes a
|
|
325
|
+
* sibling edit harmless. Like `format`, it belongs to the target; omitted, the
|
|
326
|
+
* client's config-file declaration answers.
|
|
327
|
+
*/
|
|
328
|
+
sourcePreservingYaml?: boolean;
|
|
307
329
|
}): { state: IntegrationState; reason?: StateReason } {
|
|
308
330
|
if (input.fileText !== null && !input.fileIsRegular) {
|
|
309
331
|
return { state: "unsafe", reason: "not-regular-file" };
|
|
@@ -405,12 +427,16 @@ export function classifyIntegration(input: {
|
|
|
405
427
|
&& !isHermesAffinityUpgrade(input.parsed, input.record, input.contribution)) {
|
|
406
428
|
return { state: "conflict", reason: "foreign-edit" };
|
|
407
429
|
}
|
|
408
|
-
if (!INTEGRATION_CLIENTS[clientId].sourcePreservingYaml
|
|
409
|
-
&& fingerprint(input.fileText ?? "") !== input.record.fileFingerprint
|
|
430
|
+
if (!(input.sourcePreservingYaml ?? INTEGRATION_CLIENTS[clientId].sourcePreservingYaml !== undefined)
|
|
431
|
+
&& fingerprint(input.fileText ?? "") !== input.record.fileFingerprint
|
|
432
|
+
&& !droidNormalizedFileMatchesRecord(input.parsed, input.record)) {
|
|
410
433
|
/*
|
|
411
|
-
* The file changed since we wrote it
|
|
412
|
-
* byte-for-byte what we put there
|
|
413
|
-
*
|
|
434
|
+
* The file changed since we wrote it. Every fragment we own is still
|
|
435
|
+
* byte-for-byte what we put there, so this is a sibling edit rather than
|
|
436
|
+
* tampering. Known Droid row normalization is accepted before this branch
|
|
437
|
+
* only when removing its two client fields reproduces the recorded file.
|
|
438
|
+
* Other drift reaches this branch. Apply rewrites the WHOLE document, so
|
|
439
|
+
* for comment-capable formats (yaml,
|
|
414
440
|
* json5, toml) it would drop comments the user wrote next to us: fail
|
|
415
441
|
* closed there. Strict JSON cannot carry comments — a commented file
|
|
416
442
|
* never reaches this branch because parsing already failed — so the only
|
|
@@ -449,6 +475,7 @@ export interface IntegrationStateInput {
|
|
|
449
475
|
models: readonly ExportModel[];
|
|
450
476
|
config: OcxConfig;
|
|
451
477
|
port: number;
|
|
478
|
+
droidReasoningDefaults?: Record<string, string>;
|
|
452
479
|
env?: NodeJS.ProcessEnv;
|
|
453
480
|
home?: string;
|
|
454
481
|
/** The whole integration state store, bound to one root. */
|
|
@@ -462,7 +489,8 @@ export function exportContextOf(input: {
|
|
|
462
489
|
models: readonly ExportModel[];
|
|
463
490
|
config: OcxConfig;
|
|
464
491
|
port: number;
|
|
465
|
-
|
|
492
|
+
droidReasoningDefaults?: Record<string, string>;
|
|
493
|
+
}): { baseUrl: string; models: readonly ExportModel[]; config: OcxConfig; droidReasoningDefaults?: Record<string, string> } {
|
|
466
494
|
return {
|
|
467
495
|
/*
|
|
468
496
|
* Composed through the SAME helper `ocx export` uses. Interpolating the
|
|
@@ -476,9 +504,35 @@ export function exportContextOf(input: {
|
|
|
476
504
|
baseUrl: opencodeProxyBaseUrl(input.port, input.config.hostname, input.config),
|
|
477
505
|
models: input.models,
|
|
478
506
|
config: input.config,
|
|
507
|
+
...(input.droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: input.droidReasoningDefaults }),
|
|
479
508
|
};
|
|
480
509
|
}
|
|
481
510
|
|
|
511
|
+
export function buildIntegrationContribution(
|
|
512
|
+
input: IntegrationStateInput,
|
|
513
|
+
effective: IntegrationTarget,
|
|
514
|
+
parsed: unknown,
|
|
515
|
+
record: OwnershipRecord | null,
|
|
516
|
+
): ManagedContribution {
|
|
517
|
+
const context = exportContextOf(input);
|
|
518
|
+
if (input.clientId === "droid" && input.droidReasoningDefaults === undefined && record?.configPath === effective.configPath) {
|
|
519
|
+
const inherited = recordedDroidContributionMatches(parsed, record)
|
|
520
|
+
? droidDefaultsFromOwnedRows(context, parsed, record.fragmentPaths)
|
|
521
|
+
: {};
|
|
522
|
+
if (Object.keys(inherited).length > 0) context.droidReasoningDefaults = inherited;
|
|
523
|
+
}
|
|
524
|
+
return effective.buildContribution(context);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function recordedDroidContributionMatches(document: unknown, record: OwnershipRecord): boolean {
|
|
528
|
+
try {
|
|
529
|
+
const observed = recordedContribution(document, record);
|
|
530
|
+
return observed !== null && observedContributionMatchesRecord(observed, record);
|
|
531
|
+
} catch {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
482
536
|
let retriedThisProcess = false;
|
|
483
537
|
|
|
484
538
|
/**
|
|
@@ -535,9 +589,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
535
589
|
try {
|
|
536
590
|
// One resolution for both, so a client whose paths come from mutable state
|
|
537
591
|
// cannot report one account's install beside another account's config path.
|
|
538
|
-
const
|
|
539
|
-
const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home, context);
|
|
540
|
-
if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
|
|
592
|
+
const paths = resolveStatePaths(input);
|
|
541
593
|
installed = io.statKind(paths.detectDir) === "dir";
|
|
542
594
|
if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
|
|
543
595
|
/*
|
|
@@ -608,7 +660,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
608
660
|
const parsed = input.clientId === "cline"
|
|
609
661
|
? parseClineDocument(loaded.before)
|
|
610
662
|
: parseConfig(loaded.before, effective.format, EXPORT_CLIENTS[input.clientId].jsonc ? { jsonc: true } : undefined);
|
|
611
|
-
const contribution =
|
|
663
|
+
const contribution = buildIntegrationContribution(input, effective, parsed, record);
|
|
612
664
|
if (input.clientId === "droid" && input.models.length > 0 && contribution.fragments.length === 0
|
|
613
665
|
&& (!record || record.configPath !== configPath)) {
|
|
614
666
|
return { clientId: input.clientId, state: "unsafe", installed, configPath,
|
|
@@ -623,6 +675,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
623
675
|
configPath,
|
|
624
676
|
clientId: input.clientId,
|
|
625
677
|
format: effective.format,
|
|
678
|
+
sourcePreservingYaml: effective.sourcePreservingYaml !== null,
|
|
626
679
|
});
|
|
627
680
|
if (input.clientId === "droid" && record && (state === "current" || state === "stale")) {
|
|
628
681
|
try { assertDroidRecordedSettingsUnambiguous(spec.detectDir(input.env, input.home), parsed, record); }
|
|
@@ -644,9 +697,43 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
644
697
|
* about. A store we are writing needs no notice; the path already names it.
|
|
645
698
|
*/
|
|
646
699
|
...(effective.ineffective && effective.ineffective.store !== configPath
|
|
647
|
-
? {
|
|
700
|
+
? {
|
|
701
|
+
supersededBy: effective.ineffective.store,
|
|
702
|
+
supersededReason: effective.ineffective.why,
|
|
703
|
+
...(effective.ineffective.emptyDocument === undefined ? {} : { missingStoreDocument: effective.ineffective.emptyDocument }),
|
|
704
|
+
}
|
|
648
705
|
: {}),
|
|
649
706
|
...(record ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
|
|
650
707
|
...retention,
|
|
651
708
|
};
|
|
652
709
|
}
|
|
710
|
+
|
|
711
|
+
function resolveStatePaths(input: IntegrationStateInput): { configPath: string; detectDir: string } {
|
|
712
|
+
const context = exportContextOf(input);
|
|
713
|
+
const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home, context);
|
|
714
|
+
if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
|
|
715
|
+
return paths;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function readOwnedDroidReasoningDefaults(input: IntegrationStateInput): Record<string, string> {
|
|
719
|
+
if (input.clientId !== "droid") return {};
|
|
720
|
+
const store = input.store ?? createIntegrationStateStore();
|
|
721
|
+
const io = input.io ?? store.io();
|
|
722
|
+
try {
|
|
723
|
+
const paths = resolveStatePaths(input);
|
|
724
|
+
const record = store.readRecords().droid;
|
|
725
|
+
if (!record || record.clientId !== "droid" || record.configPath !== paths.configPath) return {};
|
|
726
|
+
const effective = resolveIntegrationTarget({
|
|
727
|
+
clientId: "droid", configPath: paths.configPath, io, record, env: input.env, home: input.home,
|
|
728
|
+
});
|
|
729
|
+
const loaded = loadTarget(io, effective.configPath);
|
|
730
|
+
if (!loaded.ok) return {};
|
|
731
|
+
const parsed = parseConfig(loaded.before, "json");
|
|
732
|
+
if (parsed === PARSE_FAILED) return {};
|
|
733
|
+
if (!recordedDroidContributionMatches(parsed, record)) return {};
|
|
734
|
+
return droidDefaultsFromOwnedRows(exportContextOf(input), parsed, record.fragmentPaths);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (error instanceof ClientPathError) return {};
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
@@ -36,6 +36,12 @@ export interface IntegrationTarget {
|
|
|
36
36
|
readonly format: ConfigFormat;
|
|
37
37
|
/** The contribution shape that file's reader understands. */
|
|
38
38
|
readonly buildContribution: BuildContribution;
|
|
39
|
+
/**
|
|
40
|
+
* The one YAML leaf patched in place in that file, or null when the writer
|
|
41
|
+
* re-renders the document. A client's config file and its store have
|
|
42
|
+
* different shapes, so this belongs to the target rather than the client.
|
|
43
|
+
*/
|
|
44
|
+
readonly sourcePreservingYaml: { readonly path: readonly string[] } | null;
|
|
39
45
|
/**
|
|
40
46
|
* Set when a write to the file above would not reach the client.
|
|
41
47
|
*
|
|
@@ -60,10 +66,20 @@ export interface IneffectiveWrite {
|
|
|
60
66
|
* with the store itself as the target: a client that bumps its schema after
|
|
61
67
|
* we wrote the store leaves our block there, removable, and the file no
|
|
62
68
|
* longer one we may merge into.
|
|
69
|
+
*
|
|
70
|
+
* `missing-store` — the store does not exist, but the client already manages it
|
|
71
|
+
* (DSH: a Desktop profile manifest is present). DSH renames `settings.yaml` to
|
|
72
|
+
* `settings.yaml.imported` and imports it on startup, so a config-file write would
|
|
73
|
+
* orphan our ownership record. `remedy` says how to bring the store back, and
|
|
74
|
+
* `emptyDocument` is what to create it with.
|
|
63
75
|
*/
|
|
64
|
-
readonly why:
|
|
76
|
+
readonly why: IneffectiveWriteReason;
|
|
77
|
+
readonly remedy?: string;
|
|
78
|
+
readonly emptyDocument?: string;
|
|
65
79
|
}
|
|
66
80
|
|
|
81
|
+
export type IneffectiveWriteReason = "owned-config-file" | "unestablished-schema" | "missing-store";
|
|
82
|
+
|
|
67
83
|
function configFileTarget(
|
|
68
84
|
clientId: IntegrationClientId,
|
|
69
85
|
configPath: string,
|
|
@@ -74,6 +90,7 @@ function configFileTarget(
|
|
|
74
90
|
configPath,
|
|
75
91
|
format: exportSpec.format,
|
|
76
92
|
buildContribution: exportSpec.buildContribution,
|
|
93
|
+
sourcePreservingYaml: INTEGRATION_CLIENTS[clientId].sourcePreservingYaml ?? null,
|
|
77
94
|
ineffective,
|
|
78
95
|
};
|
|
79
96
|
}
|
|
@@ -87,6 +104,7 @@ function storeTarget(
|
|
|
87
104
|
configPath,
|
|
88
105
|
format: declared.format,
|
|
89
106
|
buildContribution: declared.buildContribution,
|
|
107
|
+
sourcePreservingYaml: declared.sourcePreservingYaml ?? null,
|
|
90
108
|
ineffective,
|
|
91
109
|
};
|
|
92
110
|
}
|
|
@@ -141,7 +159,12 @@ export function resolveIntegrationTarget(args: {
|
|
|
141
159
|
if (!declared) return configFileTarget(clientId, configPath, null);
|
|
142
160
|
const storePath = declared.path(args.env, args.home);
|
|
143
161
|
const kind = io.statKind(storePath);
|
|
144
|
-
if (kind === "missing")
|
|
162
|
+
if (kind === "missing") {
|
|
163
|
+
const missing = declared.missingStore;
|
|
164
|
+
return missing?.readsStore(storePath, path => io.statKind(path)) === true
|
|
165
|
+
? configFileTarget(clientId, configPath, { store: storePath, why: "missing-store", remedy: missing.remedy, emptyDocument: missing.emptyDocument })
|
|
166
|
+
: configFileTarget(clientId, configPath, null);
|
|
167
|
+
}
|
|
145
168
|
// Only proven absence permits a legacy write. Unreadable or non-file stores
|
|
146
169
|
// cannot establish what the client reads; preserve the recorded removal target.
|
|
147
170
|
if (kind !== "file") {
|
|
@@ -18,7 +18,7 @@ import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
|
18
18
|
import { detachedConfigSnapshot } from "../config/admitted-identity";
|
|
19
19
|
import { copyPlainData } from "../lib/plain-data";
|
|
20
20
|
import type { OcxConfig } from "../types";
|
|
21
|
-
import { defaultIntegrationIO, loadTarget, type IntegrationIO } from "./config-io";
|
|
21
|
+
import { defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
22
22
|
import { inspectKiloCandidates } from "./kilo-candidates";
|
|
23
23
|
import {
|
|
24
24
|
fingerprint,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
refreshablePathsOf,
|
|
34
34
|
semanticProtectedContributionFingerprint,
|
|
35
35
|
} from "./ownership-policy";
|
|
36
|
-
import { AmbiguousSelectorError, createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
36
|
+
import { AmbiguousSelectorError, createdContainerPaths, mergeContribution, parseSegment, removeFragments } from "./merge";
|
|
37
37
|
import {
|
|
38
38
|
INTEGRATION_CLIENTS,
|
|
39
39
|
assertDroidPathsUnambiguous,
|
|
@@ -43,12 +43,12 @@ import {
|
|
|
43
43
|
type IntegrationClientId,
|
|
44
44
|
} from "./registry";
|
|
45
45
|
import { declaredIntegrationTarget } from "./target";
|
|
46
|
-
import { exportContextOf } from "./state";
|
|
46
|
+
import { buildIntegrationContribution, exportContextOf } from "./state";
|
|
47
47
|
import type { IntegrationState } from "./state";
|
|
48
48
|
import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
49
49
|
import { ClientPathError } from "../clients/config-export";
|
|
50
50
|
import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
|
|
51
|
-
import { observeIntegration, type IntegrationWriteInput, type RefusalReason } from "./mutation-plan";
|
|
51
|
+
import { observeIntegration, restoreStoreDirectoryRefusal, type IntegrationWriteInput, type RefusalReason } from "./mutation-plan";
|
|
52
52
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
53
53
|
import {
|
|
54
54
|
patchYamlFragmentSource,
|
|
@@ -300,6 +300,8 @@ function applyOrRefreshIntegration(
|
|
|
300
300
|
return refuse(clientId, "superseded_store", classified.state,
|
|
301
301
|
target.ineffective.why === "owned-config-file"
|
|
302
302
|
? `${clientId} now reads its providers from ${readsFrom}, and opencodex still has a block in ${configPath}, which it no longer reads. Disable the ${clientId} integration to remove that block, then enable it again to write ${readsFrom}.`
|
|
303
|
+
: target.ineffective.why === "missing-store"
|
|
304
|
+
? `${clientId} reads its providers from ${readsFrom}, which does not exist. ${clientId} renames ${configPath} away and imports it on its next start, so opencodex will not write there. ${target.ineffective.remedy ?? fallback}`
|
|
303
305
|
: readsFrom === configPath
|
|
304
306
|
? `opencodex does not recognise the schema of ${readsFrom}, the file ${clientId} reads its providers from, so it will not merge into it. ${fallback}`
|
|
305
307
|
: `${clientId} now reads its providers from ${readsFrom}, whose schema opencodex does not recognise, so writing ${configPath} would change nothing it loads. ${fallback}`);
|
|
@@ -383,8 +385,15 @@ function applyOrRefreshIntegration(
|
|
|
383
385
|
let created: string[];
|
|
384
386
|
let text: string;
|
|
385
387
|
try {
|
|
388
|
+
// The source patcher updates this selected row in place. Keep it in the
|
|
389
|
+
// semantic merge too, even when we originally created it, and retain that
|
|
390
|
+
// provenance so disable can still remove the row when it becomes empty.
|
|
391
|
+
const sourceRoot = target.sourcePreservingYaml?.path[0];
|
|
392
|
+
const retainedRow = sourceRoot && parseSegment(sourceRoot).kind === "select" ? sourceRoot : null;
|
|
393
|
+
const priorCreated = record?.createdContainers ?? [];
|
|
394
|
+
const prunable = new Set(priorCreated.filter(path => path !== retainedRow));
|
|
386
395
|
const base = classified.state === "stale" && record
|
|
387
|
-
? removeFragments(parsed, record.fragmentPaths,
|
|
396
|
+
? removeFragments(parsed, record.fragmentPaths, prunable).doc
|
|
388
397
|
: classified.state === "conflict" && record
|
|
389
398
|
/*
|
|
390
399
|
* A forced overwrite of a `foreign-edit` conflict drops what the previous
|
|
@@ -399,26 +408,27 @@ function applyOrRefreshIntegration(
|
|
|
399
408
|
* them, so a later disable removes our leaves and leaves their structure
|
|
400
409
|
* standing.
|
|
401
410
|
*/
|
|
402
|
-
? removeFragments(parsed, record.fragmentPaths,
|
|
411
|
+
? removeFragments(parsed, record.fragmentPaths, prunable).doc
|
|
403
412
|
: parsed;
|
|
404
413
|
// Computed against the document as it stands BEFORE the merge: afterwards
|
|
405
414
|
// every container exists and "did we create this?" is unanswerable.
|
|
406
415
|
created = createdContainerPaths(base, contribution);
|
|
416
|
+
if (retainedRow && priorCreated.includes(retainedRow)) created = [...new Set([retainedRow, ...created])];
|
|
407
417
|
const nextDocument = mergeContribution(base, contribution);
|
|
408
418
|
if (clientId === "cline") preserveClineSelection(parsed, nextDocument);
|
|
409
|
-
if (
|
|
410
|
-
const value = sourcePreservingFragmentValue(contribution,
|
|
419
|
+
if (target.sourcePreservingYaml && before !== null) {
|
|
420
|
+
const value = sourcePreservingFragmentValue(contribution, target.sourcePreservingYaml.path);
|
|
411
421
|
const patched = value === undefined
|
|
412
422
|
? null
|
|
413
423
|
: patchYamlFragmentSource(
|
|
414
424
|
before,
|
|
415
|
-
|
|
425
|
+
target.sourcePreservingYaml.path,
|
|
416
426
|
{ kind: "upsert", value },
|
|
417
427
|
nextDocument,
|
|
418
428
|
);
|
|
419
429
|
if (patched === null) {
|
|
420
430
|
return refuse(clientId, "unsafe", "unsafe",
|
|
421
|
-
yamlRefusalReason(before,
|
|
431
|
+
yamlRefusalReason(before, target.sourcePreservingYaml.path, configPath, "it was left alone"));
|
|
422
432
|
}
|
|
423
433
|
text = patched;
|
|
424
434
|
} else {
|
|
@@ -565,12 +575,12 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
565
575
|
* comment makes its ancestor user-owned without protecting our leaf.
|
|
566
576
|
*/
|
|
567
577
|
const recordedCreated = record!.createdContainers ?? [];
|
|
568
|
-
const prunableCreated =
|
|
569
|
-
? sourcePrunableYamlContainers(before,
|
|
578
|
+
const prunableCreated = target.sourcePreservingYaml && before !== null
|
|
579
|
+
? sourcePrunableYamlContainers(before, target.sourcePreservingYaml.path, recordedCreated)
|
|
570
580
|
: recordedCreated;
|
|
571
581
|
if (prunableCreated === null) {
|
|
572
582
|
return refuse(clientId, "unsafe", "unsafe",
|
|
573
|
-
yamlRefusalReason(before ?? "",
|
|
583
|
+
yamlRefusalReason(before ?? "", target.sourcePreservingYaml!.path, configPath, "nothing was removed"));
|
|
574
584
|
}
|
|
575
585
|
let doc: unknown;
|
|
576
586
|
let removed: boolean;
|
|
@@ -586,14 +596,14 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
586
596
|
}
|
|
587
597
|
let text: string;
|
|
588
598
|
try {
|
|
589
|
-
if (
|
|
590
|
-
const patched = patchYamlFragmentSource(before,
|
|
599
|
+
if (target.sourcePreservingYaml && before !== null) {
|
|
600
|
+
const patched = patchYamlFragmentSource(before, target.sourcePreservingYaml.path, {
|
|
591
601
|
kind: "remove",
|
|
592
602
|
createdContainers: prunableCreated,
|
|
593
603
|
}, doc);
|
|
594
604
|
if (patched === null) {
|
|
595
605
|
return refuse(clientId, "unsafe", "unsafe",
|
|
596
|
-
yamlRefusalReason(before,
|
|
606
|
+
yamlRefusalReason(before, target.sourcePreservingYaml.path, configPath, "nothing was removed"));
|
|
597
607
|
}
|
|
598
608
|
text = patched;
|
|
599
609
|
} else {
|
|
@@ -651,6 +661,10 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
651
661
|
return refuse(clientId, "conflict", "conflict",
|
|
652
662
|
`that operation was recorded for ${configPath}, which this client no longer writes; it now resolves to ${resolvedPath}`);
|
|
653
663
|
}
|
|
664
|
+
const directoryRefusal = restoreStoreDirectoryRefusal(input, configPath, io);
|
|
665
|
+
if (directoryRefusal !== null) {
|
|
666
|
+
return refuse(clientId, "unsafe", "unsafe", directoryRefusal);
|
|
667
|
+
}
|
|
654
668
|
/*
|
|
655
669
|
* Preview refuses this in observeRestore. Refusing here too is what keeps an
|
|
656
670
|
* undo of an older candidate from replacing the record a newer candidate owns.
|
|
@@ -713,7 +727,11 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
713
727
|
// exact bytes when the snapshot was taken. Re-deriving it from the file would
|
|
714
728
|
// mean guessing which entries are ours, and a wrong guess deletes a user's.
|
|
715
729
|
const restoredRecord = entry.priorRecord;
|
|
716
|
-
const fresh =
|
|
730
|
+
const fresh = buildIntegrationContribution(
|
|
731
|
+
{ ...input, droidReasoningDefaults: undefined }, rowTarget,
|
|
732
|
+
clientId === "droid" && restoredText !== null ? parseConfig(restoredText, "json") : undefined,
|
|
733
|
+
restoredRecord,
|
|
734
|
+
);
|
|
717
735
|
/*
|
|
718
736
|
* Does the restored record actually describe the restored bytes?
|
|
719
737
|
*
|
|
@@ -812,6 +830,12 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
812
830
|
if (!models.ok) {
|
|
813
831
|
throw new UncopyableIntegrationInputError("the model roster could not be captured for this write");
|
|
814
832
|
}
|
|
833
|
+
const droidReasoningDefaults = input.droidReasoningDefaults === undefined
|
|
834
|
+
? undefined
|
|
835
|
+
: copyPlainData(input.droidReasoningDefaults);
|
|
836
|
+
if (droidReasoningDefaults !== undefined && !droidReasoningDefaults.ok) {
|
|
837
|
+
throw new UncopyableIntegrationInputError("Droid reasoning defaults could not be captured for this write");
|
|
838
|
+
}
|
|
815
839
|
/*
|
|
816
840
|
* One resolution for both paths. Aside derives them from the account id in
|
|
817
841
|
* its manifest, so two independent calls could verify one account's install
|
|
@@ -828,7 +852,11 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
828
852
|
* checked in one state and written from another, which is the substitution the fingerprint
|
|
829
853
|
* exists to prevent. Copying both here gives the plan and the document one input.
|
|
830
854
|
*/
|
|
831
|
-
return {
|
|
855
|
+
return {
|
|
856
|
+
...input, config, models: models.value,
|
|
857
|
+
...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: droidReasoningDefaults.value }),
|
|
858
|
+
env, home, store, io, resolvedPaths,
|
|
859
|
+
};
|
|
832
860
|
}
|
|
833
861
|
|
|
834
862
|
function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
@@ -848,6 +876,57 @@ function tryFreezeIntegrationInput(input: IntegrationWriteInput):
|
|
|
848
876
|
}
|
|
849
877
|
}
|
|
850
878
|
|
|
879
|
+
/**
|
|
880
|
+
* The store lock to hold beside the config file's, or null.
|
|
881
|
+
*
|
|
882
|
+
* Only for a store whose directory exists: a lock is never what creates a
|
|
883
|
+
* client's profile. A store path that cannot be resolved names nothing to lock;
|
|
884
|
+
* target resolution reports it on its own.
|
|
885
|
+
*/
|
|
886
|
+
function storeLockFile(frozen: FrozenIntegrationInput): string | null {
|
|
887
|
+
const declared = INTEGRATION_CLIENTS[frozen.clientId].currentStore;
|
|
888
|
+
if (!declared?.lockFile) return null;
|
|
889
|
+
let storePath: string;
|
|
890
|
+
try {
|
|
891
|
+
storePath = declared.path(frozen.env, frozen.home);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (error instanceof ClientPathError) return null;
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
return frozen.io.statKind(dirname(storePath)) === "dir" ? declared.lockFile(storePath) : null;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Hold the config file's sibling lock, then the store's. Every operation takes
|
|
901
|
+
* them in that order, so two of ours cannot deadlock, and the client's own
|
|
902
|
+
* writer only ever holds one of them.
|
|
903
|
+
*/
|
|
904
|
+
function withClientLocks(
|
|
905
|
+
frozen: FrozenIntegrationInput,
|
|
906
|
+
suffix: ".lock",
|
|
907
|
+
operation: () => WriteOutcome,
|
|
908
|
+
options?: CoordinatedIntegrationOptions,
|
|
909
|
+
): Promise<WriteOutcome> {
|
|
910
|
+
const run = async (heldStoreLock: string | null): Promise<WriteOutcome> => {
|
|
911
|
+
const refused = await options?.revalidate?.(frozen);
|
|
912
|
+
if (refused) return refused;
|
|
913
|
+
// A profile can appear while revalidation awaits. Acquire its lock and
|
|
914
|
+
// repeat the validation before observing or mutating its document.
|
|
915
|
+
const required = storeLockFile(frozen);
|
|
916
|
+
if (required !== null && required !== heldStoreLock) {
|
|
917
|
+
return withIntegrationWriterLock(required, () => run(required), options?.lockSeams, ".lock");
|
|
918
|
+
}
|
|
919
|
+
// No await between the last store probe and the synchronous transaction.
|
|
920
|
+
return operation();
|
|
921
|
+
};
|
|
922
|
+
return withIntegrationWriterLock(frozen.resolvedPaths.configPath, () => {
|
|
923
|
+
// Decide only after the outer lock has actually been acquired.
|
|
924
|
+
const storeLock = storeLockFile(frozen);
|
|
925
|
+
return storeLock === null ? run(null)
|
|
926
|
+
: withIntegrationWriterLock(storeLock, () => run(storeLock), options?.lockSeams, ".lock");
|
|
927
|
+
}, options?.lockSeams, suffix);
|
|
928
|
+
}
|
|
929
|
+
|
|
851
930
|
async function coordinatedWrite(
|
|
852
931
|
input: IntegrationWriteInput,
|
|
853
932
|
operation: (frozen: IntegrationWriteInput) => WriteOutcome,
|
|
@@ -865,16 +944,14 @@ async function coordinatedWrite(
|
|
|
865
944
|
// An absent client home is not created merely to acquire a sibling lock.
|
|
866
945
|
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
|
|
867
946
|
const refused = await options?.revalidate?.(frozen);
|
|
868
|
-
|
|
947
|
+
if (refused) return refused;
|
|
948
|
+
if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") return operation(frozen);
|
|
869
949
|
}
|
|
870
|
-
return
|
|
871
|
-
frozen
|
|
872
|
-
async () => {
|
|
873
|
-
const refused = await options?.revalidate?.(frozen);
|
|
874
|
-
return refused ?? operation(frozen);
|
|
875
|
-
},
|
|
876
|
-
options?.lockSeams,
|
|
950
|
+
return withClientLocks(
|
|
951
|
+
frozen,
|
|
877
952
|
spec.writerLock.suffix,
|
|
953
|
+
() => operation(frozen),
|
|
954
|
+
options,
|
|
878
955
|
);
|
|
879
956
|
}
|
|
880
957
|
|
|
@@ -927,15 +1004,10 @@ export async function restoreIntegrationCoordinated(
|
|
|
927
1004
|
`${frozen.resolvedPaths.detectDir} is missing; restore will not create the client home`,
|
|
928
1005
|
);
|
|
929
1006
|
}
|
|
930
|
-
return
|
|
931
|
-
frozen
|
|
932
|
-
async () => {
|
|
933
|
-
// An undo is bound like any other confirmation, and this is the only place where that check
|
|
934
|
-
// happens with the lock held and before the snapshot, the write and the journal row.
|
|
935
|
-
const refused = await options?.revalidate?.(frozen);
|
|
936
|
-
return refused ?? run();
|
|
937
|
-
},
|
|
938
|
-
options?.lockSeams,
|
|
1007
|
+
return withClientLocks(
|
|
1008
|
+
frozen,
|
|
939
1009
|
spec.writerLock.suffix,
|
|
1010
|
+
run,
|
|
1011
|
+
options,
|
|
940
1012
|
);
|
|
941
1013
|
}
|
|
@@ -65,6 +65,23 @@ export function canonicalHttpOrigin(value: unknown): string | null {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Standalone pairing is only for this process's configured literal loopback origin.
|
|
70
|
+
* No CORS entry, public hub URL, wildcard bind, alias or client role can widen it.
|
|
71
|
+
* The CLI separately checks the attested runtime's actual port before requesting a grant.
|
|
72
|
+
*/
|
|
73
|
+
export function standaloneGuiPairingOrigin(config: {
|
|
74
|
+
runtimeRole?: string;
|
|
75
|
+
hostname?: string;
|
|
76
|
+
port: number;
|
|
77
|
+
}): string | null {
|
|
78
|
+
if ((config.runtimeRole ?? "standalone") !== "standalone") return null;
|
|
79
|
+
const hostname = config.hostname ?? "127.0.0.1";
|
|
80
|
+
if (hostname !== "127.0.0.1" && hostname !== "::1") return null;
|
|
81
|
+
if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) return null;
|
|
82
|
+
return new URL(`http://${hostname === "::1" ? "[::1]" : hostname}:${config.port}`).origin;
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
function capabilityPayload(
|
|
69
86
|
nonce: string,
|
|
70
87
|
method: string,
|