@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922
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/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +5 -1
- package/src/adapters/anthropic.ts +16 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +10 -2
- package/src/adapters/command-code.ts +2 -1
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +13 -3
- package/src/adapters/cursor.ts +11 -2
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin.ts +75 -23
- package/src/adapters/google-antigravity-wire.ts +5 -2
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google.ts +29 -5
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat.ts +18 -57
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +10 -0
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +43 -4
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/index.ts +200 -61
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-listener.ts +2 -5
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/provider-models.ts +24 -15
- package/src/codex/catalog/retained-sync.ts +2 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/history-provider.ts +12 -1
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +52 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +87 -44
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +5 -41
- package/src/codex/shim-templates.ts +29 -3
- package/src/companion/settings.ts +132 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/load-degrade.ts +34 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/schema/config-schema.ts +27 -1
- package/src/config/schema/leaf-validators.ts +47 -0
- package/src/config.ts +1 -1
- package/src/generated/compatibility-version.json +418 -174
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +124 -18
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +49 -11
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug.ts +40 -0
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +59 -14
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/index.ts +24 -1
- package/src/oauth/login-cli.ts +80 -29
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/key-failover.ts +31 -1
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +124 -37
- package/src/providers/quota/vendor-probes-key.ts +37 -22
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +9 -4
- package/src/providers/registry/entries-extended.ts +29 -4
- package/src/providers/registry/model-seeds.ts +47 -10
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/schema.ts +3 -0
- package/src/router.ts +17 -2
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +55 -2
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +55 -17
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +56 -10
- package/src/server/index/spend-ledger-lifecycle.ts +34 -8
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index.ts +21 -28
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +6 -0
- package/src/server/management/shared.ts +18 -1
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +8 -9
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/relay.ts +19 -2
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +114 -0
- package/src/server/request-metrics.ts +92 -30
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +11 -0
- package/src/server/responses/core-opaque-recovery.ts +90 -0
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +14 -1
- package/src/server/responses/passthrough-dispatch.ts +179 -35
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/request-prepare.ts +42 -1
- package/src/server/responses/request-send-budget.ts +12 -0
- package/src/server/responses/request-transport.ts +24 -4
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +4 -1
- package/src/server/search.ts +25 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +1 -1
- package/src/types/config.ts +37 -0
- package/src/types/provider.ts +73 -0
- package/src/types/request.ts +28 -2
- package/src/types/tools.ts +19 -0
- package/src/types.ts +3 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/web-search/alpha-search.ts +21 -1
- package/gui/dist/assets/index-BTuCbqQd.css +0 -1
- package/gui/dist/assets/index-DoBVdPHP.js +0 -134
|
@@ -11,10 +11,18 @@
|
|
|
11
11
|
import { createClineIO } from "./cline-io";
|
|
12
12
|
import { parseClineDocument } from "./cline-document";
|
|
13
13
|
import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
|
|
14
|
+
import type { ConfigFormat } from "../clients/config-export";
|
|
14
15
|
import type { OcxConfig } from "../types";
|
|
15
16
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
16
17
|
import { SNAPSHOT_RETENTION } from "./journal";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
AmbiguousSelectorError,
|
|
20
|
+
InvalidSelectorError,
|
|
21
|
+
parseSegment,
|
|
22
|
+
readPath,
|
|
23
|
+
selectIndex,
|
|
24
|
+
type PathSegment,
|
|
25
|
+
} from "./merge";
|
|
18
26
|
import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
|
|
19
27
|
import {
|
|
20
28
|
protectedContributionFingerprint,
|
|
@@ -28,6 +36,7 @@ import {
|
|
|
28
36
|
unresolvedPathHintFor,
|
|
29
37
|
type IntegrationClientId,
|
|
30
38
|
} from "./registry";
|
|
39
|
+
import { resolveIntegrationTarget, type IntegrationTarget } from "./target";
|
|
31
40
|
import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
|
|
32
41
|
|
|
33
42
|
export type IntegrationState = "absent" | "current" | "stale" | "conflict" | "unsafe";
|
|
@@ -50,6 +59,21 @@ export interface IntegrationStatus {
|
|
|
50
59
|
appliedAt?: string;
|
|
51
60
|
lastOpId?: string;
|
|
52
61
|
reason?: StateReason;
|
|
62
|
+
/**
|
|
63
|
+
* The store this client reads instead of `configPath`.
|
|
64
|
+
*
|
|
65
|
+
* Present only when the integration is NOT writing that store: either our
|
|
66
|
+
* block is still in the config file, or the store is not a document whose
|
|
67
|
+
* shape has been observed. Where the store is written, `configPath` names it
|
|
68
|
+
* and there is nothing to report beside the state.
|
|
69
|
+
*
|
|
70
|
+
* Orthogonal to `state`, which answers "what is on disk, and did we put it
|
|
71
|
+
* there?" — and answers it correctly here: the block can be byte-for-byte
|
|
72
|
+
* current in a file the client stopped opening. That pair is not a
|
|
73
|
+
* contradiction, it is the whole of #5348, so the surface that reports
|
|
74
|
+
* `current` has to be able to report this beside it.
|
|
75
|
+
*/
|
|
76
|
+
supersededBy?: string;
|
|
53
77
|
/** Snapshot files retained for this client; -1 when they cannot be inspected. */
|
|
54
78
|
snapshotCount: number;
|
|
55
79
|
/** Pruning is behind, so older (possibly credential-bearing) snapshots remain. */
|
|
@@ -64,37 +88,14 @@ function assertNever(segment: never): never {
|
|
|
64
88
|
throw new Error(`unknown path segment ${JSON.stringify(segment)}`);
|
|
65
89
|
}
|
|
66
90
|
|
|
67
|
-
/** The element a selector names, or `undefined` when none matches. */
|
|
68
|
-
function selectElement(items: readonly unknown[], segment: PathSegment & { kind: "select" }): unknown {
|
|
69
|
-
return items[selectIndex(items, segment.field, segment.value)];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
91
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
92
|
+
* The path reader lives with the path grammar, in `merge`, and is re-exported
|
|
93
|
+
* here because the classifier was its original home and every caller imports
|
|
94
|
+
* it from this module. One implementation is the point: a reader that resolved
|
|
95
|
+
* a selector differently from the writer would report one element as ours and
|
|
96
|
+
* then rewrite another.
|
|
77
97
|
*/
|
|
78
|
-
export
|
|
79
|
-
let cursor: unknown = doc;
|
|
80
|
-
for (const raw of path) {
|
|
81
|
-
const segment = parseSegment(raw);
|
|
82
|
-
switch (segment.kind) {
|
|
83
|
-
case "key":
|
|
84
|
-
if (!isPlainRecord(cursor)) return undefined;
|
|
85
|
-
cursor = cursor[segment.key];
|
|
86
|
-
break;
|
|
87
|
-
case "select":
|
|
88
|
-
if (!Array.isArray(cursor)) return undefined;
|
|
89
|
-
cursor = selectElement(cursor, segment);
|
|
90
|
-
break;
|
|
91
|
-
default:
|
|
92
|
-
return assertNever(segment);
|
|
93
|
-
}
|
|
94
|
-
if (cursor === undefined) return undefined;
|
|
95
|
-
}
|
|
96
|
-
return cursor;
|
|
97
|
-
}
|
|
98
|
+
export { readPath };
|
|
98
99
|
|
|
99
100
|
/** Does the document carry any fragment we would write? */
|
|
100
101
|
export function hasOurFragments(doc: unknown, contribution: ManagedContribution): boolean {
|
|
@@ -136,7 +137,7 @@ export function blockedContainerPath(
|
|
|
136
137
|
case "key":
|
|
137
138
|
return (value as Record<string, unknown>)[segment.key];
|
|
138
139
|
case "select":
|
|
139
|
-
return
|
|
140
|
+
return (value as readonly unknown[])[selectIndex(value as readonly unknown[], segment.criteria)];
|
|
140
141
|
default:
|
|
141
142
|
return assertNever(segment);
|
|
142
143
|
}
|
|
@@ -285,6 +286,14 @@ export function classifyIntegration(input: {
|
|
|
285
286
|
*/
|
|
286
287
|
configPath?: string;
|
|
287
288
|
clientId?: IntegrationClientId;
|
|
289
|
+
/**
|
|
290
|
+
* Text format of the file being classified, which is not always the client's
|
|
291
|
+
* config format: a client that moved its providers keeps a second document
|
|
292
|
+
* whose format is declared with the store. Only the comment-capability of the
|
|
293
|
+
* format is read here, and reading the wrong one would decide a sibling edit
|
|
294
|
+
* the wrong way.
|
|
295
|
+
*/
|
|
296
|
+
format?: ConfigFormat;
|
|
288
297
|
}): { state: IntegrationState; reason?: StateReason } {
|
|
289
298
|
if (input.fileText !== null && !input.fileIsRegular) {
|
|
290
299
|
return { state: "unsafe", reason: "not-regular-file" };
|
|
@@ -310,8 +319,13 @@ export function classifyIntegration(input: {
|
|
|
310
319
|
}
|
|
311
320
|
}
|
|
312
321
|
} catch (error) {
|
|
313
|
-
if (
|
|
314
|
-
|
|
322
|
+
if (error instanceof AmbiguousSelectorError) {
|
|
323
|
+
return { state: "unsafe", reason: "ambiguous-selector" };
|
|
324
|
+
}
|
|
325
|
+
if (error instanceof InvalidSelectorError) {
|
|
326
|
+
return { state: "unsafe", reason: "unparseable" };
|
|
327
|
+
}
|
|
328
|
+
throw error;
|
|
315
329
|
}
|
|
316
330
|
if (!hasOurFragments(input.parsed, input.contribution)) return { state: "absent" };
|
|
317
331
|
|
|
@@ -393,7 +407,7 @@ export function classifyIntegration(input: {
|
|
|
393
407
|
* stands and re-owns the file. This also lets disable proceed on a
|
|
394
408
|
* drifted file — removal still touches only the recorded fragment paths.
|
|
395
409
|
*/
|
|
396
|
-
if (EXPORT_CLIENTS[clientId].format !== "json") {
|
|
410
|
+
if ((input.format ?? EXPORT_CLIENTS[clientId].format) !== "json") {
|
|
397
411
|
return { state: "conflict", reason: "foreign-edit" };
|
|
398
412
|
}
|
|
399
413
|
return { state: "stale" };
|
|
@@ -491,7 +505,6 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
491
505
|
retryPendingPrunesOnce(store);
|
|
492
506
|
let io = input.io ?? store.io();
|
|
493
507
|
const spec = INTEGRATION_CLIENTS[input.clientId];
|
|
494
|
-
const exportSpec = EXPORT_CLIENTS[input.clientId];
|
|
495
508
|
const retention = retentionOf(input.clientId, store);
|
|
496
509
|
/*
|
|
497
510
|
* Resolution can refuse — a relative OPENCLAW_* selector names a file whose
|
|
@@ -499,14 +512,25 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
499
512
|
* every client for its state, so letting that escape would answer 500 for
|
|
500
513
|
* the whole Integrations page because one client is misconfigured.
|
|
501
514
|
*/
|
|
502
|
-
let configPath: string;
|
|
503
515
|
let installed: boolean;
|
|
516
|
+
let effective: IntegrationTarget;
|
|
517
|
+
let record: OwnershipRecord | null;
|
|
504
518
|
try {
|
|
505
519
|
// One resolution for both, so a client whose paths come from mutable state
|
|
506
520
|
// cannot report one account's install beside another account's config path.
|
|
507
521
|
const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home);
|
|
508
|
-
configPath = paths.configPath;
|
|
509
522
|
installed = io.statKind(paths.detectDir) === "dir";
|
|
523
|
+
if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
|
|
524
|
+
/*
|
|
525
|
+
* The record is one of the inputs the target is chosen from, so it is read
|
|
526
|
+
* here rather than after the file. The status this function reports is about
|
|
527
|
+
* whichever file the next mutation would act on; reading a different one
|
|
528
|
+
* would let the badge and the switch disagree.
|
|
529
|
+
*/
|
|
530
|
+
record = store.readRecords()[input.clientId] ?? null;
|
|
531
|
+
effective = resolveIntegrationTarget({
|
|
532
|
+
clientId: input.clientId, configPath: paths.configPath, io, record, env: input.env, home: input.home,
|
|
533
|
+
});
|
|
510
534
|
} catch (error) {
|
|
511
535
|
if (!(error instanceof ClientPathError)) throw error;
|
|
512
536
|
/*
|
|
@@ -531,30 +555,32 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
531
555
|
};
|
|
532
556
|
}
|
|
533
557
|
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
if (!
|
|
558
|
+
const configPath = effective.configPath;
|
|
559
|
+
const loaded = loadTarget(io, configPath);
|
|
560
|
+
if (!loaded.ok) {
|
|
537
561
|
return {
|
|
538
562
|
clientId: input.clientId,
|
|
539
563
|
state: "unsafe",
|
|
540
564
|
installed,
|
|
541
565
|
configPath,
|
|
542
|
-
reason:
|
|
566
|
+
reason: loaded.why === "read-failed" ? "unparseable" : "not-regular-file",
|
|
543
567
|
...retention,
|
|
544
568
|
};
|
|
545
569
|
}
|
|
546
570
|
|
|
547
|
-
const parsed = input.clientId === "cline"
|
|
548
|
-
|
|
549
|
-
|
|
571
|
+
const parsed = input.clientId === "cline"
|
|
572
|
+
? parseClineDocument(loaded.before)
|
|
573
|
+
: parseConfig(loaded.before, effective.format);
|
|
574
|
+
const contribution = effective.buildContribution(exportContextOf(input));
|
|
550
575
|
const { state, reason } = classifyIntegration({
|
|
551
|
-
fileText:
|
|
576
|
+
fileText: loaded.before,
|
|
552
577
|
fileIsRegular: true,
|
|
553
578
|
parsed,
|
|
554
579
|
record,
|
|
555
580
|
contribution,
|
|
556
581
|
configPath,
|
|
557
582
|
clientId: input.clientId,
|
|
583
|
+
format: effective.format,
|
|
558
584
|
});
|
|
559
585
|
|
|
560
586
|
return {
|
|
@@ -563,6 +589,13 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
563
589
|
installed,
|
|
564
590
|
configPath,
|
|
565
591
|
...(reason ? { reason } : {}),
|
|
592
|
+
/*
|
|
593
|
+
* Only when the client reads a DIFFERENT file than the one this status is
|
|
594
|
+
* about. A store we are writing needs no notice; the path already names it.
|
|
595
|
+
*/
|
|
596
|
+
...(effective.ineffective && effective.ineffective.store !== configPath
|
|
597
|
+
? { supersededBy: effective.ineffective.store }
|
|
598
|
+
: {}),
|
|
566
599
|
...(record ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
|
|
567
600
|
...retention,
|
|
568
601
|
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which file an integration operation reads, writes and records — and, when
|
|
3
|
+
* that is not the file the client reads, which file is.
|
|
4
|
+
*
|
|
5
|
+
* A client may move its provider list between releases and keep the old file
|
|
6
|
+
* reachable only through a one-shot import. Everything the integration checks
|
|
7
|
+
* still passes against the old file: it is writable, the block merges, the
|
|
8
|
+
* ownership record describes it correctly. The only thing that changed is
|
|
9
|
+
* whether anything reads it (#5348).
|
|
10
|
+
*
|
|
11
|
+
* So the target is chosen before anything is classified, and it is chosen by
|
|
12
|
+
* three facts in this order:
|
|
13
|
+
*
|
|
14
|
+
* 1. No declared store, or no store on disk — the config file, exactly as
|
|
15
|
+
* before. A client that has never run still imports what we write there.
|
|
16
|
+
* 2. Our own block already sits in the config file this record names. Stay
|
|
17
|
+
* there. Disable removes what we wrote from where we wrote it, and the
|
|
18
|
+
* caller reports the write as ineffective rather than orphaning a block in
|
|
19
|
+
* one file while writing another.
|
|
20
|
+
* 3. Otherwise the store, but only when its schema can be established. A store
|
|
21
|
+
* we cannot parse or whose version we have not observed is reported as the
|
|
22
|
+
* reason the write cannot reach the client, because merging an invented
|
|
23
|
+
* nesting into a file holding the user's other providers would trade a
|
|
24
|
+
* silent no-op for a silent loss.
|
|
25
|
+
*/
|
|
26
|
+
import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
27
|
+
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
28
|
+
import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
|
|
29
|
+
import type { OwnershipRecord } from "./ownership";
|
|
30
|
+
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
31
|
+
|
|
32
|
+
export interface IntegrationTarget {
|
|
33
|
+
/** The file this operation reads, writes, journals and records ownership for. */
|
|
34
|
+
readonly configPath: string;
|
|
35
|
+
/** Text format of that file, which is not always the client's config format. */
|
|
36
|
+
readonly format: ConfigFormat;
|
|
37
|
+
/** The contribution shape that file's reader understands. */
|
|
38
|
+
readonly buildContribution: BuildContribution;
|
|
39
|
+
/**
|
|
40
|
+
* Set when a write to the file above would not reach the client.
|
|
41
|
+
*
|
|
42
|
+
* `why` is carried rather than re-derived because the cases have different
|
|
43
|
+
* remedies and only this function knows which one it took. Disable is not
|
|
44
|
+
* gated on it: removing bytes this project wrote from the file it wrote them
|
|
45
|
+
* to is as effective as it ever was.
|
|
46
|
+
*/
|
|
47
|
+
readonly ineffective: IneffectiveWrite | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface IneffectiveWrite {
|
|
51
|
+
/** The provider store the client reads. */
|
|
52
|
+
readonly store: string;
|
|
53
|
+
/**
|
|
54
|
+
* `owned-config-file` — this project's block is still in the config file, so
|
|
55
|
+
* the operation stays there and the remedy is to remove it first, rather than
|
|
56
|
+
* leave a block in one file while writing another.
|
|
57
|
+
*
|
|
58
|
+
* `unestablished-schema` — the store is not a document whose shape has been
|
|
59
|
+
* observed, so there is nothing safe to merge into it. This one is reachable
|
|
60
|
+
* with the store itself as the target: a client that bumps its schema after
|
|
61
|
+
* we wrote the store leaves our block there, removable, and the file no
|
|
62
|
+
* longer one we may merge into.
|
|
63
|
+
*/
|
|
64
|
+
readonly why: "owned-config-file" | "unestablished-schema";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function configFileTarget(
|
|
68
|
+
clientId: IntegrationClientId,
|
|
69
|
+
configPath: string,
|
|
70
|
+
ineffective: IneffectiveWrite | null,
|
|
71
|
+
): IntegrationTarget {
|
|
72
|
+
const exportSpec = EXPORT_CLIENTS[clientId];
|
|
73
|
+
return {
|
|
74
|
+
configPath,
|
|
75
|
+
format: exportSpec.format,
|
|
76
|
+
buildContribution: exportSpec.buildContribution,
|
|
77
|
+
ineffective,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function storeTarget(
|
|
82
|
+
declared: NonNullable<(typeof INTEGRATION_CLIENTS)[IntegrationClientId]["currentStore"]>,
|
|
83
|
+
configPath: string,
|
|
84
|
+
ineffective: IneffectiveWrite | null = null,
|
|
85
|
+
): IntegrationTarget {
|
|
86
|
+
return {
|
|
87
|
+
configPath,
|
|
88
|
+
format: declared.format,
|
|
89
|
+
buildContribution: declared.buildContribution,
|
|
90
|
+
ineffective,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Does the file this record names still hold any fragment it claims?
|
|
96
|
+
*
|
|
97
|
+
* Uncertainty answers yes. An unreadable or unparseable config file is a state
|
|
98
|
+
* the classifier is about to refuse on, and it must refuse on the file our
|
|
99
|
+
* record is about rather than silently move the operation to a different one.
|
|
100
|
+
* An ambiguous or invalid selector is the same kind of answer.
|
|
101
|
+
*/
|
|
102
|
+
function recordedBlockStillPresent(
|
|
103
|
+
io: IntegrationIO,
|
|
104
|
+
format: ConfigFormat,
|
|
105
|
+
configPath: string,
|
|
106
|
+
record: OwnershipRecord,
|
|
107
|
+
): boolean {
|
|
108
|
+
const loaded = loadTarget(io, configPath);
|
|
109
|
+
if (!loaded.ok) return true;
|
|
110
|
+
const parsed = parseConfig(loaded.before, format);
|
|
111
|
+
if (parsed === PARSE_FAILED) return true;
|
|
112
|
+
try {
|
|
113
|
+
return record.fragmentPaths.some(path => readPath(parsed, path) !== undefined);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof AmbiguousSelectorError || error instanceof InvalidSelectorError) return true;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the target for one operation.
|
|
122
|
+
*
|
|
123
|
+
* `configPath` is the client's config file as the registry resolves it now, and
|
|
124
|
+
* `record` is the stored ownership as read, before any path filtering — the
|
|
125
|
+
* filtering depends on the answer this function returns.
|
|
126
|
+
*
|
|
127
|
+
* The store is observed through the caller's own `IntegrationIO`, never the
|
|
128
|
+
* real filesystem directly, so status and mutation cannot disagree about
|
|
129
|
+
* whether a write can land.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveIntegrationTarget(args: {
|
|
132
|
+
clientId: IntegrationClientId;
|
|
133
|
+
configPath: string;
|
|
134
|
+
io: IntegrationIO;
|
|
135
|
+
record: OwnershipRecord | null;
|
|
136
|
+
env?: NodeJS.ProcessEnv;
|
|
137
|
+
home?: string;
|
|
138
|
+
}): IntegrationTarget {
|
|
139
|
+
const { clientId, configPath, io, record } = args;
|
|
140
|
+
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
141
|
+
if (!declared) return configFileTarget(clientId, configPath, null);
|
|
142
|
+
const storePath = declared.path(args.env, args.home);
|
|
143
|
+
const kind = io.statKind(storePath);
|
|
144
|
+
if (kind === "missing") return configFileTarget(clientId, configPath, null);
|
|
145
|
+
// Only proven absence permits a legacy write. Unreadable or non-file stores
|
|
146
|
+
// cannot establish what the client reads; preserve the recorded removal target.
|
|
147
|
+
if (kind !== "file") {
|
|
148
|
+
const ineffective: IneffectiveWrite = { store: storePath, why: "unestablished-schema" };
|
|
149
|
+
return record?.clientId === clientId && record.configPath === storePath
|
|
150
|
+
? storeTarget(declared, storePath, ineffective)
|
|
151
|
+
: configFileTarget(clientId, configPath, ineffective);
|
|
152
|
+
}
|
|
153
|
+
const loaded = loadTarget(io, storePath);
|
|
154
|
+
const parsed = loaded.ok ? parseConfig(loaded.before, declared.format) : PARSE_FAILED;
|
|
155
|
+
const established = parsed !== PARSE_FAILED && declared.establishes(parsed);
|
|
156
|
+
const unestablished: IneffectiveWrite = { store: storePath, why: "unestablished-schema" };
|
|
157
|
+
/*
|
|
158
|
+
* Our own block decides the target before the store's schema does, whichever
|
|
159
|
+
* file holds it. Disable has to remove what we wrote from where we wrote it,
|
|
160
|
+
* and an apply must never leave a block in one file while writing another.
|
|
161
|
+
*/
|
|
162
|
+
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
163
|
+
if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
|
|
164
|
+
return storeTarget(declared, storePath, established ? null : unestablished);
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
owned?.configPath === configPath
|
|
168
|
+
&& recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)
|
|
169
|
+
) {
|
|
170
|
+
return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
|
|
171
|
+
}
|
|
172
|
+
if (!established) {
|
|
173
|
+
return configFileTarget(clientId, configPath, unestablished);
|
|
174
|
+
}
|
|
175
|
+
return storeTarget(declared, storePath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The target a path NAMES, or null when this client may not write there.
|
|
180
|
+
*
|
|
181
|
+
* Undo acts on the file its journal row recorded, so it needs the meaning of a
|
|
182
|
+
* historical path rather than the choice above: which contribution shape
|
|
183
|
+
* describes those bytes, and whether this client still resolves that location
|
|
184
|
+
* at all. Resolving a path the client no longer names is what would let a row
|
|
185
|
+
* recorded for one home delete a file in another, so an unrecognised path is
|
|
186
|
+
* null and the caller refuses.
|
|
187
|
+
*/
|
|
188
|
+
export function declaredIntegrationTarget(args: {
|
|
189
|
+
clientId: IntegrationClientId;
|
|
190
|
+
configPath: string;
|
|
191
|
+
resolvedConfigPath: string;
|
|
192
|
+
env?: NodeJS.ProcessEnv;
|
|
193
|
+
home?: string;
|
|
194
|
+
}): IntegrationTarget | null {
|
|
195
|
+
const { clientId, configPath, resolvedConfigPath } = args;
|
|
196
|
+
if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
|
|
197
|
+
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
198
|
+
if (!declared) return null;
|
|
199
|
+
try {
|
|
200
|
+
if (declared.path(args.env, args.home) !== configPath) return null;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// A store the operator relocated with a path we cannot resolve proves
|
|
203
|
+
// nothing about the row, so the row is not a legal target.
|
|
204
|
+
if (error instanceof ClientPathError) return null;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
return storeTarget(declared, configPath);
|
|
208
|
+
}
|
|
@@ -13,7 +13,7 @@ import { homedir } from "node:os";
|
|
|
13
13
|
import { createClineIO, ClineTransactionError } from "./cline-io";
|
|
14
14
|
import { serializeClineDocument, preserveClineSelection } from "./cline-document";
|
|
15
15
|
import { dirname } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import type { ExportModel, ManagedContribution } from "../clients/config-export";
|
|
17
17
|
import { shouldInjectApiAuthHeader } from "../codex/inject";
|
|
18
18
|
import { detachedConfigSnapshot } from "../config/admitted-identity";
|
|
19
19
|
import { copyPlainData } from "../lib/plain-data";
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./ownership-policy";
|
|
34
34
|
import { AmbiguousSelectorError, createdContainerPaths, mergeContribution, removeFragments } from "./merge";
|
|
35
35
|
import { INTEGRATION_CLIENTS, isLoopbackOnly, resolveIntegrationPaths, type IntegrationClientId } from "./registry";
|
|
36
|
+
import { declaredIntegrationTarget } from "./target";
|
|
36
37
|
import { exportContextOf } from "./state";
|
|
37
38
|
import type { IntegrationState } from "./state";
|
|
38
39
|
import { serializeDocument, UnserializableValueError } from "./serialize";
|
|
@@ -251,7 +252,7 @@ function applyOrRefreshIntegration(
|
|
|
251
252
|
): WriteOutcome {
|
|
252
253
|
const pre = preflight(input);
|
|
253
254
|
if (pre.failed) return pre.failed;
|
|
254
|
-
const { store, io, clientId, spec,
|
|
255
|
+
const { store, io, clientId, spec, target, configPath, detectDir, before, parsed, contribution, record, classified } = pre;
|
|
255
256
|
|
|
256
257
|
// The detect directory preflight already resolved, so it cannot name a
|
|
257
258
|
// different account than the config path this operation is about to write.
|
|
@@ -262,6 +263,35 @@ function applyOrRefreshIntegration(
|
|
|
262
263
|
return refuse(clientId, "non_loopback", classified.state,
|
|
263
264
|
`The generated ${clientId} integration is loopback-only and does not emit the admission header a non-loopback bind requires. Give it loopback access instead, through a tunnel or a local forwarder.`);
|
|
264
265
|
}
|
|
266
|
+
/*
|
|
267
|
+
* The write would land, and nothing would read it.
|
|
268
|
+
*
|
|
269
|
+
* This is deliberately a refusal rather than a warning attached to a success.
|
|
270
|
+
* The file is writable, our block merges cleanly, and the ownership record
|
|
271
|
+
* that follows would describe a real state of a real file — which is exactly
|
|
272
|
+
* how the original defect stayed invisible: every check the integration runs
|
|
273
|
+
* passed, the journal recorded a correct apply, and no model ever appeared in
|
|
274
|
+
* the client (#5348). Reporting the operation as done is the part that is
|
|
275
|
+
* wrong, so the operation does not report at all.
|
|
276
|
+
*
|
|
277
|
+
* Reached only when the store cannot be written: either this project's block
|
|
278
|
+
* is still in the config file, where removing it is the way forward, or the
|
|
279
|
+
* store is not a document whose shape has been observed. Where the store can
|
|
280
|
+
* be written the operation targets it and never arrives here.
|
|
281
|
+
*
|
|
282
|
+
* Apply, overwrite and refresh only. Disable removes bytes this project put
|
|
283
|
+
* in this file, and that removal is as effective as it ever was.
|
|
284
|
+
*/
|
|
285
|
+
if (target.ineffective !== null) {
|
|
286
|
+
const readsFrom = target.ineffective.store;
|
|
287
|
+
const fallback = `Add the proxy as a provider in ${clientId}'s own settings instead; \`ocx export --client ${clientId}\` prints the model list to copy.`;
|
|
288
|
+
return refuse(clientId, "superseded_store", classified.state,
|
|
289
|
+
target.ineffective.why === "owned-config-file"
|
|
290
|
+
? `${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}.`
|
|
291
|
+
: readsFrom === configPath
|
|
292
|
+
? `opencodex does not recognise the schema of ${readsFrom}, the file ${clientId} reads its providers from, so it will not merge into it. ${fallback}`
|
|
293
|
+
: `${clientId} now reads its providers from ${readsFrom}, whose schema opencodex does not recognise, so writing ${configPath} would change nothing it loads. ${fallback}`);
|
|
294
|
+
}
|
|
265
295
|
if (classified.state === "conflict") {
|
|
266
296
|
if (conflictPolicy === "refuse") {
|
|
267
297
|
return refuse(clientId, "conflict", "conflict",
|
|
@@ -372,7 +402,7 @@ function applyOrRefreshIntegration(
|
|
|
372
402
|
}
|
|
373
403
|
text = patched;
|
|
374
404
|
} else {
|
|
375
|
-
text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument,
|
|
405
|
+
text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument, target.format);
|
|
376
406
|
}
|
|
377
407
|
} catch (error) {
|
|
378
408
|
if (error instanceof AmbiguousSelectorError) {
|
|
@@ -460,7 +490,7 @@ export function refreshIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
460
490
|
export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
461
491
|
const pre = preflight(input);
|
|
462
492
|
if (pre.failed) return pre.failed;
|
|
463
|
-
const { store, io, clientId, spec,
|
|
493
|
+
const { store, io, clientId, spec, target, configPath, before, parsed, record, classified } = pre;
|
|
464
494
|
|
|
465
495
|
if (classified.state === "absent") {
|
|
466
496
|
return { ok: true, changed: false, state: "absent", clientId, message: "not applied" };
|
|
@@ -532,7 +562,7 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
|
|
|
532
562
|
}
|
|
533
563
|
text = patched;
|
|
534
564
|
} else {
|
|
535
|
-
text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc,
|
|
565
|
+
text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc, target.format);
|
|
536
566
|
}
|
|
537
567
|
} catch (error) {
|
|
538
568
|
if (!(error instanceof UnserializableValueError)) throw error;
|
|
@@ -570,13 +600,21 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
570
600
|
const clientId = entry.clientId;
|
|
571
601
|
const resolvedPath = input.resolvedPaths?.configPath
|
|
572
602
|
?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
|
|
573
|
-
// Restore acts on the path the operation was journaled against. Resolving a
|
|
574
|
-
// different path here would let an operation recorded for one home delete a
|
|
575
|
-
// file in another.
|
|
576
603
|
const configPath = entry.configPath;
|
|
577
|
-
|
|
604
|
+
/*
|
|
605
|
+
* Restore acts on the path the operation was journaled against. Resolving a
|
|
606
|
+
* different path here would let an operation recorded for one home delete a
|
|
607
|
+
* file in another — but a client may legally have written more than one file,
|
|
608
|
+
* so the question is whether this client still names that location, not
|
|
609
|
+
* whether it is the config file. The answer also carries the contribution
|
|
610
|
+
* shape those bytes are in, which is what the state below is measured against.
|
|
611
|
+
*/
|
|
612
|
+
const rowTarget = declaredIntegrationTarget({
|
|
613
|
+
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
|
|
614
|
+
});
|
|
615
|
+
if (rowTarget === null) {
|
|
578
616
|
return refuse(clientId, "conflict", "conflict",
|
|
579
|
-
`that operation was recorded for ${configPath},
|
|
617
|
+
`that operation was recorded for ${configPath}, which this client no longer writes; it now resolves to ${resolvedPath}`);
|
|
580
618
|
}
|
|
581
619
|
if (clientId === "cline") {
|
|
582
620
|
try { io = createClineIO(io, configPath, store, true); }
|
|
@@ -625,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
625
663
|
// exact bytes when the snapshot was taken. Re-deriving it from the file would
|
|
626
664
|
// mean guessing which entries are ours, and a wrong guess deletes a user's.
|
|
627
665
|
const restoredRecord = entry.priorRecord;
|
|
628
|
-
const fresh =
|
|
666
|
+
const fresh = rowTarget.buildContribution(exportContextOf(input));
|
|
629
667
|
/*
|
|
630
668
|
* Does the restored record actually describe the restored bytes?
|
|
631
669
|
*
|
|
@@ -12,6 +12,11 @@ export function fixtureProviderConfig(adapter: string): OcxProviderConfig {
|
|
|
12
12
|
models: ["fixture-model"],
|
|
13
13
|
defaultModel: "fixture-model",
|
|
14
14
|
liveModels: false,
|
|
15
|
+
// The wire role is no longer read from the hostname, and an undeclared destination folds
|
|
16
|
+
// `developer` to `system` because one that rejects the role answers 400 and the turn never
|
|
17
|
+
// starts. This fixture is the one place that asserts the forwarded role, so the destination
|
|
18
|
+
// it stands for records that it accepts it. Only the Chat adapter reaches that decision.
|
|
19
|
+
...(adapter === "openai-chat" ? { foldDeveloperRoleToSystem: false } : {}),
|
|
15
20
|
};
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { OpenUrlResult } from "./open-url";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one sentence a terminal login says when nothing opened.
|
|
5
|
+
*
|
|
6
|
+
* Stated once because it is now said from three places — the Codex account login, the generic
|
|
7
|
+
* OAuth login and the key login — and a sentence restated three times drifts three ways. Callers
|
|
8
|
+
* that have something more specific to add append to it rather than rewriting it, so the part a
|
|
9
|
+
* user learns to recognize stays identical everywhere.
|
|
10
|
+
*/
|
|
11
|
+
export const BROWSER_LAUNCH_FAILED_NOTICE =
|
|
12
|
+
"⚠️ No browser could be opened here — open the URL above yourself.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Report a browser launch whose answer arrives after the code that started it has moved on.
|
|
16
|
+
*
|
|
17
|
+
* The OAuth controller does not await `onAuth`, so a CLI login cannot simply await the launcher
|
|
18
|
+
* there: the flow continues, and on a callback-server provider the very next thing it does is
|
|
19
|
+
* draw a readline prompt. A warning written at that moment lands on the line the user is typing
|
|
20
|
+
* on, which is worse than not warning at all.
|
|
21
|
+
*
|
|
22
|
+
* So the launch reports itself as soon as it settles, and anything that would collide with it
|
|
23
|
+
* waits on {@link BrowserLaunchReport.settled} first. The launcher answers within its own settle
|
|
24
|
+
* window, so the wait costs a fraction of a second and buys a deterministic order.
|
|
25
|
+
*/
|
|
26
|
+
export interface BrowserLaunchReport {
|
|
27
|
+
/** Adopt a launch already in flight. Its failure is reported once, when it settles. */
|
|
28
|
+
track(launch: Promise<OpenUrlResult>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves once every launch tracked BEFORE this call has been reported, and immediately when
|
|
31
|
+
* none was. A login publishes one URL, so "before this call" and "at all" are the same set
|
|
32
|
+
* here; the narrower promise is the one this actually keeps.
|
|
33
|
+
*/
|
|
34
|
+
settled(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createBrowserLaunchReport(
|
|
38
|
+
warn: (message: string) => void = message => { console.warn(message); },
|
|
39
|
+
): BrowserLaunchReport {
|
|
40
|
+
let pending: Promise<void> = Promise.resolve();
|
|
41
|
+
return {
|
|
42
|
+
track(launch) {
|
|
43
|
+
pending = pending
|
|
44
|
+
.then(() => launch)
|
|
45
|
+
.then(
|
|
46
|
+
result => {
|
|
47
|
+
if (result.status !== "started") warn(`\n${BROWSER_LAUNCH_FAILED_NOTICE}`);
|
|
48
|
+
},
|
|
49
|
+
// openUrl is documented never to reject, and a launcher that did would mean the same
|
|
50
|
+
// thing as one that failed. Swallowing it here is not politeness: this chain is what
|
|
51
|
+
// `settled()` hands to a prompt, so a rejection would propagate out of a login that
|
|
52
|
+
// is still perfectly able to continue, and would go unhandled in the polling flows
|
|
53
|
+
// that do not reach that await until minutes later.
|
|
54
|
+
() => { warn(`\n${BROWSER_LAUNCH_FAILED_NOTICE}`); },
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
settled: () => pending,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/lib/bun-runtime.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { createRequire } from "node:module";
|
|
|
14
14
|
import { realpathSync } from "node:fs";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { isRealBunBinary } from "./bun-binary-validator.mjs";
|
|
17
|
+
import { isStandaloneBinary } from "./standalone";
|
|
17
18
|
|
|
18
19
|
export { isRealBunBinary };
|
|
19
20
|
|
|
@@ -38,10 +39,10 @@ export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
|
|
|
38
39
|
*/
|
|
39
40
|
export const BUN_RUNTIME_PATH_ENV = "OCX_BUN_RUNTIME_PATH";
|
|
40
41
|
|
|
41
|
-
export type BunRuntimeSource = "override" | "bundled" | "process";
|
|
42
|
+
export type BunRuntimeSource = "override" | "bundled" | "process" | "standalone";
|
|
42
43
|
|
|
43
44
|
/** The only provenance values any surface may accept off the wire or out of the env. */
|
|
44
|
-
export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process"];
|
|
45
|
+
export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process", "standalone"];
|
|
45
46
|
|
|
46
47
|
export type DurableBunRuntime = {
|
|
47
48
|
path: string;
|
|
@@ -165,6 +166,9 @@ function unmarkedDurableBunRuntime(): DurableBunRuntime {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export function durableBunRuntime(): DurableBunRuntime {
|
|
169
|
+
if (isStandaloneBinary()) {
|
|
170
|
+
return { path: process.execPath, source: "standalone", overrideEnv: BUN_OVERRIDE_ENV };
|
|
171
|
+
}
|
|
168
172
|
// A durable artifact must use the runtime selected BEFORE Bun auto-loaded a
|
|
169
173
|
// project dotenv. The Node launcher and owned service/shim launchers stamp the
|
|
170
174
|
// selected source/path pair; it is accepted only when it names this exact
|