@bitkyc08/opencodex 2.83.0 → 2.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/ollama-native.ts +29 -1
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +3 -0
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +19 -0
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +54 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +26 -2
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export.ts +46 -16
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +16 -2
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +311 -171
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/reset-coupon-ledger.ts +75 -5
- package/src/grok/reset-coupons.ts +4 -2
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +24 -15
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +2 -0
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +14 -14
- package/src/providers/registry/entries-extended.ts +0 -1
- package/src/providers/registry/model-seeds.ts +17 -0
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +13 -0
- package/src/server/lifecycle.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +78 -27
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +82 -402
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +5 -3
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +2 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +63 -6
- package/src/server/responses/adapter-dispatch.ts +232 -79
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +126 -48
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -4
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +6 -8
- package/src/server/responses/sidecar-execution.ts +11 -2
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/usage/expected-prices.ts +4 -0
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ba8uG696.js +0 -54
- package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
- package/gui/dist/assets/index-BHRZdJuv.js +0 -94
package/src/grok/grpc-web.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
export interface GrpcWebTrailer {
|
|
7
7
|
status: number;
|
|
8
|
+
statusValues: string[];
|
|
8
9
|
statusMessage?: string;
|
|
9
10
|
metadata: Record<string, string>;
|
|
10
11
|
}
|
|
@@ -12,6 +13,8 @@ export interface GrpcWebTrailer {
|
|
|
12
13
|
export interface DecodedGrpcWebResponse {
|
|
13
14
|
messages: Uint8Array[];
|
|
14
15
|
status: number;
|
|
16
|
+
statusValues: string[];
|
|
17
|
+
framingComplete: boolean;
|
|
15
18
|
statusMessage?: string;
|
|
16
19
|
trailers?: GrpcWebTrailer;
|
|
17
20
|
}
|
|
@@ -51,6 +54,7 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
|
|
|
51
54
|
const text = new TextDecoder("utf-8").decode(bytes);
|
|
52
55
|
const lines = text.split(/\r?\n/);
|
|
53
56
|
const metadata: Record<string, string> = {};
|
|
57
|
+
const statusValues: string[] = [];
|
|
54
58
|
let status = 0;
|
|
55
59
|
let statusMessage: string | undefined;
|
|
56
60
|
|
|
@@ -62,6 +66,7 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
|
|
|
62
66
|
if (!key) continue;
|
|
63
67
|
metadata[key] = value;
|
|
64
68
|
if (key === "grpc-status") {
|
|
69
|
+
statusValues.push(value);
|
|
65
70
|
const parsed = parseInt(value, 10);
|
|
66
71
|
if (!Number.isNaN(parsed)) {
|
|
67
72
|
status = parsed;
|
|
@@ -75,7 +80,7 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
|
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
return { status, statusMessage, metadata };
|
|
83
|
+
return { status, statusValues, statusMessage, metadata };
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/**
|
|
@@ -83,8 +88,11 @@ export function parseGrpcWebTrailers(bytes: Uint8Array): GrpcWebTrailer {
|
|
|
83
88
|
*/
|
|
84
89
|
export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse {
|
|
85
90
|
const messages: Uint8Array[] = [];
|
|
91
|
+
const statusValues: string[] = [];
|
|
86
92
|
let offset = 0;
|
|
87
93
|
let trailer: GrpcWebTrailer | undefined;
|
|
94
|
+
let firstTrailerPayload: Uint8Array | undefined;
|
|
95
|
+
let framingComplete = true;
|
|
88
96
|
|
|
89
97
|
while (offset + HEADER_SIZE <= bytes.length) {
|
|
90
98
|
const flag = bytes[offset];
|
|
@@ -99,10 +107,22 @@ export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse
|
|
|
99
107
|
|
|
100
108
|
const payload = bytes.subarray(frameStart, frameEnd);
|
|
101
109
|
|
|
110
|
+
// Preserve legacy decoding; callers needing confirmation inspect framingComplete.
|
|
102
111
|
if (flag === FRAME_DATA) {
|
|
112
|
+
if (firstTrailerPayload !== undefined) framingComplete = false;
|
|
103
113
|
messages.push(payload);
|
|
104
114
|
} else if (flag === FRAME_TRAILER) {
|
|
115
|
+
const firstTrailer = firstTrailerPayload;
|
|
116
|
+
if (firstTrailer === undefined) {
|
|
117
|
+
firstTrailerPayload = payload;
|
|
118
|
+
} else if (payload.length !== firstTrailer.length
|
|
119
|
+
|| payload.some((byte, index) => byte !== firstTrailer[index])) {
|
|
120
|
+
framingComplete = false;
|
|
121
|
+
}
|
|
105
122
|
trailer = parseGrpcWebTrailers(payload);
|
|
123
|
+
for (const value of trailer.statusValues) statusValues.push(value);
|
|
124
|
+
} else {
|
|
125
|
+
framingComplete = false;
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
offset = frameEnd;
|
|
@@ -114,6 +134,8 @@ export function decodeGrpcWebResponse(bytes: Uint8Array): DecodedGrpcWebResponse
|
|
|
114
134
|
return {
|
|
115
135
|
messages,
|
|
116
136
|
status: finalStatus,
|
|
137
|
+
statusValues,
|
|
138
|
+
framingComplete: framingComplete && offset === bytes.length,
|
|
117
139
|
statusMessage: finalMessage,
|
|
118
140
|
trailers: trailer,
|
|
119
141
|
};
|
|
@@ -25,6 +25,8 @@ export interface GrokResetCouponOperationRecord {
|
|
|
25
25
|
attemptedAt?: number;
|
|
26
26
|
code?: string;
|
|
27
27
|
settledAt?: number;
|
|
28
|
+
/** Durable status of a replayed record, so callers apply the terminal-replay predicate. */
|
|
29
|
+
status?: "attempted" | "settled" | "failed";
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
interface GrokResetCouponOperationState {
|
|
@@ -77,8 +79,15 @@ function readGrokCouponLedger(filePath: string): GrokResetCouponLedger {
|
|
|
77
79
|
op.status = "attempted";
|
|
78
80
|
delete op.code;
|
|
79
81
|
}
|
|
82
|
+
// A v1 attempted record never carried a confirmed outcome.
|
|
83
|
+
if (op.status === "attempted") delete op.code;
|
|
80
84
|
}
|
|
81
85
|
}
|
|
86
|
+
// Only a settled or failed record carries an outcome. An open or attempted
|
|
87
|
+
// record with a code would replay an unconfirmed result as definitive.
|
|
88
|
+
for (const op of Object.values(parsed.operations)) {
|
|
89
|
+
if ((op.status === "open" || op.status === "attempted") && op.code !== undefined) throw new Error();
|
|
90
|
+
}
|
|
82
91
|
return { version: 2, operations: parsed.operations };
|
|
83
92
|
} catch {
|
|
84
93
|
throw new Error("Grok coupon ledger is unreadable; existing records were preserved.");
|
|
@@ -163,6 +172,7 @@ export function openGrokResetCouponOperation(
|
|
|
163
172
|
attemptedAt: existing.status === "attempted" ? existing.updatedAt : undefined,
|
|
164
173
|
code: existing.code,
|
|
165
174
|
settledAt: existing.updatedAt,
|
|
175
|
+
status: existing.status,
|
|
166
176
|
};
|
|
167
177
|
}
|
|
168
178
|
return {
|
|
@@ -232,17 +242,37 @@ type GrokResetCouponPreflightResult =
|
|
|
232
242
|
| { kind: "recorded" | "changed" }
|
|
233
243
|
| { kind: "replay"; code: string; tokenId?: string; settledAt: number };
|
|
234
244
|
|
|
235
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Codes that never prove an outcome. A record carrying one is not replayed as
|
|
247
|
+
* definitive, even if a restored or hand-edited ledger stores it as settled/failed.
|
|
248
|
+
*/
|
|
249
|
+
const UNRESOLVED_COUPON_CODES = new Set([
|
|
250
|
+
"redeem_failed", "attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed",
|
|
251
|
+
"attempt_mark_failed", "operation_token_mismatch", "ledger_unavailable", "capacity",
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Whether a durable record is a definitive outcome this account and token may replay:
|
|
256
|
+
* settled or failed, same account, same token when both are known, and a code that
|
|
257
|
+
* does not itself mean "unconfirmed".
|
|
258
|
+
*/
|
|
259
|
+
export function isTerminalGrokCouponReplay(
|
|
260
|
+
record: { status?: string; accountId?: string; tokenId?: string; code?: string } | undefined,
|
|
261
|
+
accountId: string, tokenId: string | undefined,
|
|
262
|
+
): boolean {
|
|
263
|
+
if (!record || record.accountId !== accountId) return false;
|
|
264
|
+
if (tokenId !== undefined && record.tokenId !== undefined && record.tokenId !== tokenId) return false;
|
|
265
|
+
return (record.status === "settled" || record.status === "failed")
|
|
266
|
+
&& !!record.code && !UNRESOLVED_COUPON_CODES.has(record.code);
|
|
267
|
+
}
|
|
236
268
|
|
|
237
269
|
/** The durable definitive outcome for this account and token, if the operation has one. */
|
|
238
270
|
function terminalReplay(
|
|
239
271
|
ledger: GrokResetCouponLedger, operationId: string, accountId: string, tokenId: string | undefined,
|
|
240
272
|
): Extract<GrokResetCouponPreflightResult, { kind: "replay" }> | null {
|
|
241
273
|
const existing = ledger.operations[operationId];
|
|
242
|
-
if (!existing || existing
|
|
243
|
-
|
|
244
|
-
if ((existing.status !== "settled" && existing.status !== "failed") || !existing.code || UNRESOLVED_COUPON_CODES.has(existing.code)) return null;
|
|
245
|
-
return { kind: "replay", code: existing.code, tokenId: existing.tokenId, settledAt: existing.updatedAt };
|
|
274
|
+
if (!existing || !isTerminalGrokCouponReplay(existing, accountId, tokenId)) return null;
|
|
275
|
+
return { kind: "replay", code: existing.code!, tokenId: existing.tokenId, settledAt: existing.updatedAt };
|
|
246
276
|
}
|
|
247
277
|
|
|
248
278
|
/** Settle a refusal or inspect its winner without releasing the mutation transaction. */
|
|
@@ -259,6 +289,46 @@ export function settleGrokResetCouponPreflightRefusal(
|
|
|
259
289
|
});
|
|
260
290
|
}
|
|
261
291
|
|
|
292
|
+
export type GrokResetCouponPreOpenRefusalResult =
|
|
293
|
+
| GrokResetCouponPreflightResult
|
|
294
|
+
| { kind: "identity-mismatch" | "capacity" };
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Refuse a request that never opened its supplied operationId. Under the mutation lock
|
|
298
|
+
* an absent id is reserved as a failed record, so a later open of the same id replays
|
|
299
|
+
* the refusal and can never be claimed (within the 30-day retention window). An id
|
|
300
|
+
* that already exists is never overwritten: a definitive outcome replays, anything
|
|
301
|
+
* else reports "changed" so the caller keeps its hold.
|
|
302
|
+
*/
|
|
303
|
+
export function refuseGrokResetCouponBeforeOpen(
|
|
304
|
+
refusal: { operationId: string; accountId: string; tokenId?: string; code: string },
|
|
305
|
+
now = Date.now(),
|
|
306
|
+
journalPath?: string,
|
|
307
|
+
): GrokResetCouponPreOpenRefusalResult {
|
|
308
|
+
return withConfigMutationLockSync(() => {
|
|
309
|
+
const filePath = journalPath ?? grokCouponJournalPath();
|
|
310
|
+
const ledger = readGrokCouponLedger(filePath);
|
|
311
|
+
const retentionCutoff = now - 30 * 24 * 60 * 60_000;
|
|
312
|
+
ledger.operations = Object.fromEntries(
|
|
313
|
+
Object.entries(ledger.operations).filter(([, op]) => op.updatedAt > retentionCutoff),
|
|
314
|
+
);
|
|
315
|
+
const existing = ledger.operations[refusal.operationId];
|
|
316
|
+
if (existing) {
|
|
317
|
+
if (existing.accountId !== refusal.accountId) return { kind: "identity-mismatch" };
|
|
318
|
+
// Same token binding as a normal replay: a different requested coupon never
|
|
319
|
+
// replays another coupon's outcome.
|
|
320
|
+
return terminalReplay(ledger, refusal.operationId, refusal.accountId, refusal.tokenId) ?? { kind: "changed" };
|
|
321
|
+
}
|
|
322
|
+
if (Object.keys(ledger.operations).length >= MAX_GROK_RESET_COUPON_OPERATION_IDS) return { kind: "capacity" };
|
|
323
|
+
ledger.operations[refusal.operationId] = {
|
|
324
|
+
accountId: refusal.accountId, ...(refusal.tokenId !== undefined ? { tokenId: refusal.tokenId } : {}),
|
|
325
|
+
status: "failed", code: refusal.code, createdAt: now, updatedAt: now,
|
|
326
|
+
};
|
|
327
|
+
writeGrokCouponLedger(filePath, ledger, now);
|
|
328
|
+
return { kind: "recorded" };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
262
332
|
/**
|
|
263
333
|
* After a lost claim: the definitive winner to replay, or null while the
|
|
264
334
|
* operation is still unresolved. Terminal states never revert, so reading after
|
|
@@ -231,10 +231,12 @@ function buildGrokHeaders(accessToken: string): Record<string, string> {
|
|
|
231
231
|
* Reads available Grok reset tokens for the authenticated xAI account.
|
|
232
232
|
*/
|
|
233
233
|
function assertConfirmedCouponStatus(decoded: ReturnType<typeof decodeGrpcWebResponse>): void {
|
|
234
|
-
const rawStatus = decoded.
|
|
234
|
+
const rawStatus = decoded.statusValues[0];
|
|
235
235
|
// The shared decoder defaults a missing/malformed status to zero. Coupon
|
|
236
236
|
// availability and an irreversible redemption need affirmative confirmation.
|
|
237
|
-
if (
|
|
237
|
+
if (decoded.framingComplete !== true || rawStatus === undefined
|
|
238
|
+
|| decoded.statusValues.some((value) => !/^\d+$/.test(value)
|
|
239
|
+
|| !Number.isSafeInteger(Number(value)) || value !== rawStatus)) {
|
|
238
240
|
throw new Error("Grok coupon response has no confirmed gRPC status");
|
|
239
241
|
}
|
|
240
242
|
const status = Number(rawStatus);
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
assertDroidRecordedSettingsUnambiguous,
|
|
33
33
|
isLoopbackOnly,
|
|
34
34
|
resolveIntegrationPaths,
|
|
35
|
+
type ResolvedIntegrationPaths,
|
|
35
36
|
restoreOwnershipCollision,
|
|
36
37
|
type IntegrationClientId,
|
|
37
38
|
} from "./registry";
|
|
@@ -567,7 +568,7 @@ export function restoreStoreDirectoryRefusal(
|
|
|
567
568
|
if (!declared?.lockFile) return null;
|
|
568
569
|
let storePath: string;
|
|
569
570
|
try {
|
|
570
|
-
storePath = declared.path(input.env, input.home);
|
|
571
|
+
storePath = declared.path(input.env, input.home, io);
|
|
571
572
|
} catch (error) {
|
|
572
573
|
if (error instanceof ClientPathError) return null;
|
|
573
574
|
throw error;
|
|
@@ -602,10 +603,11 @@ export function observeRestore(
|
|
|
602
603
|
const store = selectedOperation?.store ?? input.store ?? createIntegrationStateStore();
|
|
603
604
|
let io = input.io ?? defaultIntegrationIO(store);
|
|
604
605
|
const clientId = input.clientId;
|
|
605
|
-
let resolved:
|
|
606
|
+
let resolved: ResolvedIntegrationPaths;
|
|
606
607
|
try {
|
|
607
608
|
const context = exportContextOf(input);
|
|
608
|
-
resolved =
|
|
609
|
+
resolved = clientId === "zcode" || !input.resolvedPaths
|
|
610
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, context, io) : input.resolvedPaths;
|
|
609
611
|
if (clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(resolved.detectDir, context);
|
|
610
612
|
} catch (error) {
|
|
611
613
|
if (!(error instanceof ClientPathError)) throw error;
|
|
@@ -630,7 +632,7 @@ export function observeRestore(
|
|
|
630
632
|
* config file. The answer also carries the document shape those bytes are in.
|
|
631
633
|
*/
|
|
632
634
|
const rowTarget = declaredIntegrationTarget({
|
|
633
|
-
clientId, configPath, resolvedConfigPath: resolved.configPath, env: input.env, home: input.home,
|
|
635
|
+
clientId, configPath, resolvedConfigPath: resolved.configPath, env: input.env, home: input.home, io,
|
|
634
636
|
});
|
|
635
637
|
if (rowTarget === null) {
|
|
636
638
|
return {
|
|
@@ -848,7 +850,7 @@ export interface IntegrationWriteInput {
|
|
|
848
850
|
store?: IntegrationStateStore;
|
|
849
851
|
io?: IntegrationIO;
|
|
850
852
|
/** Frozen once by the async coordinator; synchronous callers may omit it. */
|
|
851
|
-
resolvedPaths?:
|
|
853
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
852
854
|
}
|
|
853
855
|
|
|
854
856
|
/** A refusal in the planner's own vocabulary, so observation does not depend on the writer's result type. */
|
|
@@ -922,7 +924,8 @@ export function observeIntegration(
|
|
|
922
924
|
* verify account 1 was installed and then write account 0's catalog.
|
|
923
925
|
*/
|
|
924
926
|
const context = exportContextOf(input);
|
|
925
|
-
const resolved =
|
|
927
|
+
const resolved = clientId === "zcode" || !input.resolvedPaths
|
|
928
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, context, io) : input.resolvedPaths;
|
|
926
929
|
if (clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(resolved.detectDir, context);
|
|
927
930
|
detectDir = resolved.detectDir;
|
|
928
931
|
if (clientId === "cline") io = createClineIO(io, resolved.configPath, store, effects.recover);
|
|
@@ -945,9 +948,12 @@ export function observeIntegration(
|
|
|
945
948
|
* collection route.
|
|
946
949
|
*/
|
|
947
950
|
effective = resolveIntegrationTarget({
|
|
948
|
-
clientId, configPath: recordedPath, io, record: stored, env: input.env, home: input.home,
|
|
951
|
+
clientId, configPath: recordedPath, io, record: stored, env: input.env, home: input.home, resolvedPaths: resolved,
|
|
949
952
|
});
|
|
950
953
|
configPath = effective.configPath;
|
|
954
|
+
if (effective.pathError && operation !== "disable") {
|
|
955
|
+
return { failed: observationFailure("unsafe", "unsafe", effective.pathError) } as const;
|
|
956
|
+
}
|
|
951
957
|
} catch (error) {
|
|
952
958
|
if (error instanceof ClineTransactionError) {
|
|
953
959
|
return { failed: { ...observationFailure("unsafe", "unsafe", error.message, error.snapshotPath), residual: true } } as const;
|
|
@@ -12,6 +12,7 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { assertDroidSettingsUnambiguous } from "./droid-settings";
|
|
13
13
|
import { readPath } from "./merge";
|
|
14
14
|
import type { OwnershipRecord } from "./ownership";
|
|
15
|
+
import type { IntegrationIO } from "./config-io";
|
|
15
16
|
import { dirname, join } from "node:path";
|
|
16
17
|
import {
|
|
17
18
|
ClientPathError,
|
|
@@ -49,6 +50,8 @@ import {
|
|
|
49
50
|
raycastConfigPath,
|
|
50
51
|
zcodeConfigPath,
|
|
51
52
|
zcodeHomeDir,
|
|
53
|
+
zcodeLegacyHomeDir,
|
|
54
|
+
ZcodeSettingsPathError,
|
|
52
55
|
zcodeProviderStorePath,
|
|
53
56
|
buildZcodeStoreContribution,
|
|
54
57
|
zcodeStoreSchemaEstablished,
|
|
@@ -75,12 +78,21 @@ import {
|
|
|
75
78
|
*/
|
|
76
79
|
export type IntegrationClientId = ExportClientId;
|
|
77
80
|
|
|
81
|
+
export interface ResolvedIntegrationPaths {
|
|
82
|
+
configPath: string;
|
|
83
|
+
detectDir: string;
|
|
84
|
+
/** Snapshot of a mutable store resolution from the same bootstrap read. */
|
|
85
|
+
storePath?: string;
|
|
86
|
+
/** The default config path is only a recovery hint when the active root is unknown. */
|
|
87
|
+
pathError?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
export interface IntegrationClientSpec {
|
|
79
91
|
id: IntegrationClientId;
|
|
80
92
|
/** The client's config file, honoring that client's own environment override. */
|
|
81
|
-
configPath: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
93
|
+
configPath: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
|
|
82
94
|
/** Directory whose existence is the cheap "is it installed?" signal. */
|
|
83
|
-
detectDir: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
95
|
+
detectDir: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
|
|
84
96
|
/**
|
|
85
97
|
* The provider store this client reads INSTEAD of `configPath`.
|
|
86
98
|
*
|
|
@@ -98,7 +110,7 @@ export interface IntegrationClientSpec {
|
|
|
98
110
|
* client, never merged into on a guess.
|
|
99
111
|
*/
|
|
100
112
|
currentStore?: {
|
|
101
|
-
path: (env?: NodeJS.ProcessEnv, home?: string) => string;
|
|
113
|
+
path: (env?: NodeJS.ProcessEnv, home?: string, io?: Pick<IntegrationIO, "readText">) => string;
|
|
102
114
|
format: ConfigFormat;
|
|
103
115
|
establishes: (parsed: unknown) => boolean;
|
|
104
116
|
buildContribution: BuildContribution;
|
|
@@ -139,7 +151,7 @@ export interface IntegrationClientSpec {
|
|
|
139
151
|
* Droid uses the same seam to check competing settings against the export
|
|
140
152
|
* context before status, preview, or mutation proceeds.
|
|
141
153
|
*/
|
|
142
|
-
resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext
|
|
154
|
+
resolvePaths?: (env?: NodeJS.ProcessEnv, home?: string, exportContext?: ExportContext, io?: Pick<IntegrationIO, "readText">) => ResolvedIntegrationPaths;
|
|
143
155
|
/**
|
|
144
156
|
* Where the client's config WOULD live, for a client whose real path cannot
|
|
145
157
|
* be resolved yet.
|
|
@@ -183,10 +195,11 @@ export function resolveIntegrationPaths(
|
|
|
183
195
|
env: NodeJS.ProcessEnv = process.env,
|
|
184
196
|
home: string = homedir(),
|
|
185
197
|
exportContext?: ExportContext,
|
|
186
|
-
|
|
198
|
+
io?: Pick<IntegrationIO, "readText">,
|
|
199
|
+
): ResolvedIntegrationPaths {
|
|
187
200
|
const spec = INTEGRATION_CLIENTS[clientId];
|
|
188
|
-
if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext);
|
|
189
|
-
return { configPath: spec.configPath(env, home), detectDir: spec.detectDir(env, home) };
|
|
201
|
+
if (spec.resolvePaths) return spec.resolvePaths(env, home, exportContext, io);
|
|
202
|
+
return { configPath: spec.configPath(env, home, io), detectDir: spec.detectDir(env, home, io) };
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
export function assertDroidPathsUnambiguous(root: string, exportContext?: ExportContext): void {
|
|
@@ -357,15 +370,31 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
|
|
|
357
370
|
},
|
|
358
371
|
zcode: {
|
|
359
372
|
id: "zcode",
|
|
360
|
-
configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
|
|
361
|
-
detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
|
|
373
|
+
configPath: (env = process.env, home = homedir(), io) => zcodeConfigPath(env, home, io),
|
|
374
|
+
detectDir: (env = process.env, home = homedir(), io) => zcodeHomeDir(env, home, io),
|
|
375
|
+
resolvePaths: (env = process.env, home = homedir(), _context, io) => {
|
|
376
|
+
let root: string;
|
|
377
|
+
let pathError: string | undefined;
|
|
378
|
+
try { root = zcodeHomeDir(env, home, io); }
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (!(error instanceof ZcodeSettingsPathError)) throw error;
|
|
381
|
+
root = zcodeLegacyHomeDir(env, home);
|
|
382
|
+
pathError = error.message;
|
|
383
|
+
}
|
|
384
|
+
// Passing the resolved root prevents a second bootstrap read. An explicit
|
|
385
|
+
// provider-file override outranks both the setting and ZCODE_DATA_DIR.
|
|
386
|
+
const storePath = zcodeProviderStorePath({ ...env, ZCODE_DATA_DIR: root }, home, io);
|
|
387
|
+
return { configPath: join(root, "v2", "config.json"), detectDir: root, storePath, ...(pathError ? { pathError } : {}) };
|
|
388
|
+
},
|
|
389
|
+
bindsDriftedRecord: (path, env = process.env, home = homedir()) =>
|
|
390
|
+
path === join(zcodeLegacyHomeDir(env, home), "v2", "config.json"),
|
|
362
391
|
/*
|
|
363
392
|
* ZCode 3.14 reads its providers from `v2/provider_config.json` and reaches
|
|
364
393
|
* `v2/config.json` only through the import that seeded it. Where the new
|
|
365
394
|
* file exists the import is spent, so our write is read by nobody (#5348).
|
|
366
395
|
*/
|
|
367
396
|
currentStore: {
|
|
368
|
-
path: (env = process.env, home = homedir()) => zcodeProviderStorePath(env, home),
|
|
397
|
+
path: (env = process.env, home = homedir(), io) => zcodeProviderStorePath(env, home, io),
|
|
369
398
|
format: "json",
|
|
370
399
|
establishes: zcodeStoreSchemaEstablished,
|
|
371
400
|
buildContribution: buildZcodeStoreContribution,
|
|
@@ -496,7 +525,7 @@ export function boundIntegrationConfigPath(input: {
|
|
|
496
525
|
if (
|
|
497
526
|
record && record.clientId === input.clientId &&
|
|
498
527
|
record.configPath !== input.resolvedPath &&
|
|
499
|
-
input.statKind(record.configPath) === "file" &&
|
|
528
|
+
(input.clientId === "zcode" ? input.statKind(record.configPath) !== "missing" : input.statKind(record.configPath) === "file") &&
|
|
500
529
|
INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord?.(record.configPath, input.env, input.home) === true
|
|
501
530
|
) {
|
|
502
531
|
return record.configPath;
|
|
@@ -529,6 +558,11 @@ export function restoreOwnershipCollision(input: {
|
|
|
529
558
|
}): string | null {
|
|
530
559
|
const currentPath = input.currentPath;
|
|
531
560
|
if (currentPath === null || currentPath === input.journaledPath) return null;
|
|
561
|
+
// A relocated ZCode store must not lose its single record to an older
|
|
562
|
+
// legacy undo, even when both journaled locations are legal for this home.
|
|
563
|
+
if (input.clientId === "zcode") {
|
|
564
|
+
return `that operation was recorded for ${input.journaledPath}, but ${currentPath} currently owns this integration`;
|
|
565
|
+
}
|
|
532
566
|
const binds = INTEGRATION_CLIENTS[input.clientId].bindsDriftedRecord;
|
|
533
567
|
if (!binds) return null;
|
|
534
568
|
if (binds(input.journaledPath, input.env, input.home) !== true) return null;
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
assertDroidPathsUnambiguous,
|
|
40
40
|
assertDroidRecordedSettingsUnambiguous,
|
|
41
41
|
resolveIntegrationPaths,
|
|
42
|
+
type ResolvedIntegrationPaths,
|
|
42
43
|
unresolvedPathHintFor,
|
|
43
44
|
type IntegrationClientId,
|
|
44
45
|
} from "./registry";
|
|
@@ -482,7 +483,7 @@ export interface IntegrationStateInput {
|
|
|
482
483
|
store?: IntegrationStateStore;
|
|
483
484
|
io?: IntegrationIO;
|
|
484
485
|
/** Internal explicit profile target; never accepted as a caller-provided path. */
|
|
485
|
-
resolvedPaths?:
|
|
486
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
486
487
|
}
|
|
487
488
|
|
|
488
489
|
export function exportContextOf(input: {
|
|
@@ -592,11 +593,11 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
592
593
|
*/
|
|
593
594
|
let installed: boolean;
|
|
594
595
|
let effective: IntegrationTarget;
|
|
595
|
-
let record: OwnershipRecord | null;
|
|
596
|
+
let record: OwnershipRecord | null = null;
|
|
596
597
|
try {
|
|
597
598
|
// One resolution for both, so a client whose paths come from mutable state
|
|
598
599
|
// cannot report one account's install beside another account's config path.
|
|
599
|
-
const paths = resolveStatePaths(input);
|
|
600
|
+
const paths = resolveStatePaths(input, io);
|
|
600
601
|
installed = io.statKind(paths.detectDir) === "dir";
|
|
601
602
|
if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
|
|
602
603
|
/*
|
|
@@ -611,7 +612,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
611
612
|
statKind: io.statKind, env: input.env, home: input.home,
|
|
612
613
|
});
|
|
613
614
|
effective = resolveIntegrationTarget({
|
|
614
|
-
clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home,
|
|
615
|
+
clientId: input.clientId, configPath: recordedPath, io, record, env: input.env, home: input.home, resolvedPaths: paths,
|
|
615
616
|
});
|
|
616
617
|
} catch (error) {
|
|
617
618
|
if (!(error instanceof ClientPathError)) throw error;
|
|
@@ -626,6 +627,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
626
627
|
* yet. A client that can name where its config would go gets `installed:
|
|
627
628
|
* false` and that location, which reads as "not installed" in the UI.
|
|
628
629
|
*/
|
|
630
|
+
if (input.clientId === "zcode" && record) return {
|
|
631
|
+
clientId: input.clientId, state: "unsafe", installed: false, configPath: record.configPath,
|
|
632
|
+
reason: "unresolvable-path", appliedAt: record.appliedAt, lastOpId: record.opId, ...retention,
|
|
633
|
+
};
|
|
629
634
|
const hint = unresolvedPathHintFor(input.clientId, input.env, input.home);
|
|
630
635
|
return {
|
|
631
636
|
clientId: input.clientId,
|
|
@@ -638,6 +643,12 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
638
643
|
}
|
|
639
644
|
|
|
640
645
|
const configPath = effective.configPath;
|
|
646
|
+
if (effective.pathError) return {
|
|
647
|
+
clientId: input.clientId, state: "unsafe", installed, configPath,
|
|
648
|
+
reason: "unresolvable-path",
|
|
649
|
+
...(record?.configPath === configPath ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
|
|
650
|
+
...retention,
|
|
651
|
+
};
|
|
641
652
|
if (input.clientId === "kilo") {
|
|
642
653
|
const candidates = inspectKiloCandidates({ io, selectedPath: configPath, env: input.env, home: input.home });
|
|
643
654
|
if (candidates.kind !== "ok") return {
|
|
@@ -715,9 +726,10 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
715
726
|
};
|
|
716
727
|
}
|
|
717
728
|
|
|
718
|
-
function resolveStatePaths(input: IntegrationStateInput
|
|
729
|
+
function resolveStatePaths(input: IntegrationStateInput, io: IntegrationIO): ResolvedIntegrationPaths {
|
|
719
730
|
const context = exportContextOf(input);
|
|
720
|
-
const paths = input.
|
|
731
|
+
const paths = input.clientId === "zcode" || !input.resolvedPaths
|
|
732
|
+
? resolveIntegrationPaths(input.clientId, input.env, input.home, context, io) : input.resolvedPaths;
|
|
721
733
|
if (input.clientId === "droid" && input.resolvedPaths) assertDroidPathsUnambiguous(paths.detectDir, context);
|
|
722
734
|
return paths;
|
|
723
735
|
}
|
|
@@ -727,7 +739,7 @@ export function readOwnedDroidReasoningDefaults(input: IntegrationStateInput): R
|
|
|
727
739
|
const store = input.store ?? createIntegrationStateStore();
|
|
728
740
|
const io = input.io ?? store.io();
|
|
729
741
|
try {
|
|
730
|
-
const paths = resolveStatePaths(input);
|
|
742
|
+
const paths = resolveStatePaths(input, io);
|
|
731
743
|
const record = store.readRecords().droid;
|
|
732
744
|
if (!record || record.clientId !== "droid" || record.configPath !== paths.configPath) return {};
|
|
733
745
|
const effective = resolveIntegrationTarget({
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* nesting into a file holding the user's other providers would trade a
|
|
24
24
|
* silent no-op for a silent loss.
|
|
25
25
|
*/
|
|
26
|
-
import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
26
|
+
import { EXPORT_CLIENTS, ClientPathError, zcodeLegacyHomeDir, zcodeProviderStorePath, type BuildContribution, type ConfigFormat } from "../clients/config-export";
|
|
27
27
|
import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
|
|
28
28
|
import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
|
|
29
29
|
import type { OwnershipRecord } from "./ownership";
|
|
30
|
-
import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
|
|
30
|
+
import { INTEGRATION_CLIENTS, resolveIntegrationPaths, type ResolvedIntegrationPaths, type IntegrationClientId } from "./registry";
|
|
31
31
|
|
|
32
32
|
export interface IntegrationTarget {
|
|
33
33
|
/** The file this operation reads, writes, journals and records ownership for. */
|
|
@@ -51,6 +51,8 @@ export interface IntegrationTarget {
|
|
|
51
51
|
* to is as effective as it ever was.
|
|
52
52
|
*/
|
|
53
53
|
readonly ineffective: IneffectiveWrite | null;
|
|
54
|
+
/** Active root unknown; the named config is only a historical removal target. */
|
|
55
|
+
readonly pathError?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface IneffectiveWrite {
|
|
@@ -153,12 +155,44 @@ export function resolveIntegrationTarget(args: {
|
|
|
153
155
|
record: OwnershipRecord | null;
|
|
154
156
|
env?: NodeJS.ProcessEnv;
|
|
155
157
|
home?: string;
|
|
158
|
+
resolvedPaths?: ResolvedIntegrationPaths;
|
|
156
159
|
}): IntegrationTarget {
|
|
157
160
|
const { clientId, configPath, io, record } = args;
|
|
158
161
|
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
159
162
|
if (!declared) return configFileTarget(clientId, configPath, null);
|
|
160
|
-
const
|
|
163
|
+
const paths = clientId === "zcode"
|
|
164
|
+
? args.resolvedPaths ?? resolveIntegrationPaths(clientId, args.env, args.home, undefined, io)
|
|
165
|
+
: undefined;
|
|
166
|
+
const storePath = paths?.storePath ?? declared.path(args.env, args.home, io);
|
|
161
167
|
const kind = io.statKind(storePath);
|
|
168
|
+
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
169
|
+
const historicalStore = clientId === "zcode"
|
|
170
|
+
? zcodeProviderStorePath({ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, io)
|
|
171
|
+
: null;
|
|
172
|
+
if (clientId === "zcode" && owned && owned.configPath !== configPath && owned.configPath !== storePath
|
|
173
|
+
&& owned.configPath !== historicalStore
|
|
174
|
+
&& INTEGRATION_CLIENTS[clientId].bindsDriftedRecord?.(owned.configPath, args.env, args.home) !== true
|
|
175
|
+
&& io.statKind(owned.configPath) !== "missing") {
|
|
176
|
+
throw new ClientPathError(`ZCode ownership remains at ${owned.configPath}. Its location cannot be bound to the current home or selectors; restore the original selectors and disable it before enabling a different data root. Ownership and recovery evidence are retained.`);
|
|
177
|
+
}
|
|
178
|
+
// An explicit provider-file selector can establish the active target even
|
|
179
|
+
// when the bootstrap is unreadable, but absence cannot justify a legacy write.
|
|
180
|
+
if (paths?.pathError && (!(args.env ?? process.env).ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim() || kind !== "file")) {
|
|
181
|
+
const removal = owned?.configPath === historicalStore
|
|
182
|
+
? storeTarget(declared, historicalStore)
|
|
183
|
+
: configFileTarget(clientId, configPath, null);
|
|
184
|
+
return { ...removal, pathError: paths.pathError };
|
|
185
|
+
}
|
|
186
|
+
if (historicalStore && owned?.configPath === historicalStore && historicalStore !== storePath
|
|
187
|
+
&& recordedBlockStillPresent(io, declared.format, historicalStore, owned)) {
|
|
188
|
+
return storeTarget(declared, historicalStore, { store: storePath, why: "owned-config-file" });
|
|
189
|
+
}
|
|
190
|
+
// Relocation must not orphan a block at the default historical path, even
|
|
191
|
+
// when the relocated store has not been created yet.
|
|
192
|
+
if (clientId === "zcode" && owned?.configPath === configPath && configPath !== paths?.configPath
|
|
193
|
+
&& recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)) {
|
|
194
|
+
return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
|
|
195
|
+
}
|
|
162
196
|
if (kind === "missing") {
|
|
163
197
|
const missing = declared.missingStore;
|
|
164
198
|
return missing?.readsStore(storePath, path => io.statKind(path)) === true
|
|
@@ -182,7 +216,6 @@ export function resolveIntegrationTarget(args: {
|
|
|
182
216
|
* file holds it. Disable has to remove what we wrote from where we wrote it,
|
|
183
217
|
* and an apply must never leave a block in one file while writing another.
|
|
184
218
|
*/
|
|
185
|
-
const owned = record !== null && record.clientId === clientId ? record : null;
|
|
186
219
|
if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
|
|
187
220
|
return storeTarget(declared, storePath, established ? null : unestablished);
|
|
188
221
|
}
|
|
@@ -214,6 +247,7 @@ export function declaredIntegrationTarget(args: {
|
|
|
214
247
|
resolvedConfigPath: string;
|
|
215
248
|
env?: NodeJS.ProcessEnv;
|
|
216
249
|
home?: string;
|
|
250
|
+
io?: Pick<IntegrationIO, "readText">;
|
|
217
251
|
}): IntegrationTarget | null {
|
|
218
252
|
const { clientId, configPath, resolvedConfigPath } = args;
|
|
219
253
|
if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
|
|
@@ -222,8 +256,11 @@ export function declaredIntegrationTarget(args: {
|
|
|
222
256
|
}
|
|
223
257
|
const declared = INTEGRATION_CLIENTS[clientId].currentStore;
|
|
224
258
|
if (!declared) return null;
|
|
259
|
+
if (clientId === "zcode" && configPath === zcodeProviderStorePath(
|
|
260
|
+
{ ...(args.env ?? process.env), ZCODE_DATA_DIR: zcodeLegacyHomeDir(args.env, args.home) }, args.home, args.io,
|
|
261
|
+
)) return storeTarget(declared, configPath);
|
|
225
262
|
try {
|
|
226
|
-
if (declared.path(args.env, args.home) !== configPath) return null;
|
|
263
|
+
if (declared.path(args.env, args.home, args.io) !== configPath) return null;
|
|
227
264
|
} catch (error) {
|
|
228
265
|
// A store the operator relocated with a path we cannot resolve proves
|
|
229
266
|
// nothing about the row, so the row is not a legal target.
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
assertDroidPathsUnambiguous,
|
|
40
40
|
isLoopbackOnly,
|
|
41
41
|
resolveIntegrationPaths,
|
|
42
|
+
type ResolvedIntegrationPaths,
|
|
42
43
|
restoreOwnershipCollision,
|
|
43
44
|
type IntegrationClientId,
|
|
44
45
|
} from "./registry";
|
|
@@ -643,8 +644,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
643
644
|
if (entry.clientId !== input.clientId) throw new Error("restore input names a different client than the operation");
|
|
644
645
|
|
|
645
646
|
const clientId = entry.clientId;
|
|
646
|
-
|
|
647
|
-
|
|
647
|
+
let resolvedPath: string;
|
|
648
|
+
try {
|
|
649
|
+
resolvedPath = clientId === "zcode" || !input.resolvedPaths
|
|
650
|
+
? resolveIntegrationPaths(clientId, input.env, input.home, undefined, io).configPath
|
|
651
|
+
: input.resolvedPaths.configPath;
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
654
|
+
return refuse(clientId, "unsafe", "unsafe", error.message);
|
|
655
|
+
}
|
|
648
656
|
const configPath = entry.configPath;
|
|
649
657
|
/*
|
|
650
658
|
* Restore acts on the path the operation was journaled against. Resolving a
|
|
@@ -655,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
|
|
|
655
663
|
* shape those bytes are in, which is what the state below is measured against.
|
|
656
664
|
*/
|
|
657
665
|
const rowTarget = declaredIntegrationTarget({
|
|
658
|
-
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
|
|
666
|
+
clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home, io,
|
|
659
667
|
});
|
|
660
668
|
if (rowTarget === null) {
|
|
661
669
|
return refuse(clientId, "conflict", "conflict",
|
|
@@ -814,7 +822,7 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
|
|
|
814
822
|
io: IntegrationIO;
|
|
815
823
|
env: NodeJS.ProcessEnv;
|
|
816
824
|
home: string;
|
|
817
|
-
resolvedPaths:
|
|
825
|
+
resolvedPaths: ResolvedIntegrationPaths;
|
|
818
826
|
};
|
|
819
827
|
|
|
820
828
|
/**
|
|
@@ -854,7 +862,7 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
854
862
|
*/
|
|
855
863
|
const resolvedPaths = input.resolvedPaths
|
|
856
864
|
? { ...input.resolvedPaths }
|
|
857
|
-
: resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }));
|
|
865
|
+
: resolveIntegrationPaths(input.clientId, env, home, exportContextOf({ ...input, config, models: models.value }), io);
|
|
858
866
|
/*
|
|
859
867
|
* The configuration and the roster are seams like the others, and they were the two still held
|
|
860
868
|
* by reference. A coordinated write plans from this input, awaits the writer lock and a
|
|
@@ -899,7 +907,7 @@ function storeLockFile(frozen: FrozenIntegrationInput): string | null {
|
|
|
899
907
|
if (!declared?.lockFile) return null;
|
|
900
908
|
let storePath: string;
|
|
901
909
|
try {
|
|
902
|
-
storePath = declared.path(frozen.env, frozen.home);
|
|
910
|
+
storePath = declared.path(frozen.env, frozen.home, frozen.io);
|
|
903
911
|
} catch (error) {
|
|
904
912
|
if (error instanceof ClientPathError) return null;
|
|
905
913
|
throw error;
|