@frockbot/plugin-provider-ollama-cloud 0.0.0 → 0.1.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/src/user.ts ADDED
@@ -0,0 +1,2444 @@
1
+ import {
2
+ MAX_CONNECTION_SETTINGS_V1,
3
+ decodeConnectionCommandReceiptV1,
4
+ decodeConnectionCommandV1,
5
+ decodeConnectionModelCatalogV1,
6
+ type ConnectionCommandReceiptV1,
7
+ type ConnectionCommandV1,
8
+ type ConnectionModelCatalogV1,
9
+ type ConnectionModelV1,
10
+ type ConnectionSettingsV1,
11
+ type CredentialLeaseV1,
12
+ } from "@frockbot/connection-core";
13
+ import type { ConnectionView } from "@frockbot/configuration-core";
14
+ import type {
15
+ CredentialStorage,
16
+ CredentialTransaction,
17
+ CredentialUserBackendContribution,
18
+ } from "@frockbot/plugin-credentials/user";
19
+ import type {
20
+ UserSettingsBackendContribution,
21
+ UserSettingsStorage,
22
+ UserSettingsTransaction,
23
+ } from "@frockbot/plugin-settings/user";
24
+ import type { Plugin } from "cordis";
25
+ import {
26
+ decodeOllamaApiBaseUrl,
27
+ OllamaCloudClient,
28
+ type OllamaCloudClientConfig,
29
+ } from "./client.js";
30
+ import { OLLAMA_CLOUD_PROVIDER } from "./runtime.js";
31
+
32
+ const PACKAGE_ID = "provider-ollama-cloud";
33
+ const CONNECTION_TYPE_ID = "ollama-cloud-account";
34
+ const COMMAND_PREFIX = "ollama-connection-command:";
35
+ const PENDING_KEY = "ollama-pending-connection-commands";
36
+ const RECEIPT_INDEX_KEY = "ollama-connection-receipt-index";
37
+ const COMMAND_TOMBSTONES_KEY = "ollama-connection-command-tombstones";
38
+ const MAX_MANUAL_RECEIPTS = 256;
39
+ const MAX_COMMAND_TOMBSTONES = 128;
40
+ const MAX_MANUAL_COMMANDS = MAX_MANUAL_RECEIPTS + MAX_COMMAND_TOMBSTONES;
41
+ const MAX_PENDING_COMMANDS = 64;
42
+ const MAX_PENDING_RECOVERIES_PER_ALARM = 1;
43
+ const MAX_CATALOG_REFRESHES_PER_ALARM = 1;
44
+ const ACCOUNT_KEY = "ollama-connection-account";
45
+ const AUTOMATIC_REFRESH_RECEIPT_PREFIX = "ollama-refresh-receipt:";
46
+
47
+ const AUTOMATIC_DEFAULT_MODEL_PREFERENCES = [
48
+ "gpt-oss:20b",
49
+ "glm-5.3-flash:cloud",
50
+ ] as const;
51
+
52
+ /** Choose the safest useful default from the catalog a new Connection proved. */
53
+ export function selectAutomaticOllamaModelV1(
54
+ catalog: ConnectionModelCatalogV1,
55
+ ): ConnectionModelV1 | undefined {
56
+ for (const providerModelId of AUTOMATIC_DEFAULT_MODEL_PREFERENCES) {
57
+ const preferred = catalog.models.find(
58
+ (model) => model.providerModelId === providerModelId,
59
+ );
60
+ if (preferred) return preferred;
61
+ }
62
+ return catalog.models[0];
63
+ }
64
+ const MUTATION_SEQUENCE_PREFIX = "ollama-mutation-sequence:";
65
+ const MODEL_RESOLUTION_PREFIX = "ollama-model-resolution:";
66
+ const REFRESH_INTERVAL_MS = 60 * 60 * 1_000;
67
+ const RECOVERY_DELAY_MS = 60_000;
68
+ const MODEL_LEASE_MS = 30 * 60 * 1_000;
69
+ const MAX_CONNECTION_MODELS = 100;
70
+ const MAX_DISCOVERED_MODELS = 90;
71
+
72
+ interface StoredCommand {
73
+ schemaVersion: 1;
74
+ commandId: string;
75
+ fingerprint: string;
76
+ accountId: string;
77
+ connectionId: string;
78
+ credentialGeneration?: string;
79
+ expectedGeneration?: string;
80
+ /**
81
+ * `connection/create` is not among them: an Ollama Cloud account is always
82
+ * a keyed Connection, so the keyless create command is refused on arrival
83
+ * and never reaches a durable record.
84
+ */
85
+ operation: Exclude<ConnectionCommandV1["type"], "connection/create">;
86
+ label?: string;
87
+ settings?: Record<string, string>;
88
+ enabled?: boolean;
89
+ revokeUpstream?: boolean;
90
+ receipt?: ConnectionCommandReceiptV1;
91
+ completedAt?: number;
92
+ automaticRefresh?: boolean;
93
+ mutationSequence?: number;
94
+ settlementEffectId?: string;
95
+ settlementStatus?: "applied" | "failed";
96
+ validationCatalog?: ConnectionModelCatalogV1;
97
+ validationFailure?: string;
98
+ validationStatus?: "applied" | "failed";
99
+ providerRetryPolicy?: "safe-metadata-read";
100
+ }
101
+
102
+ interface StoredModelResolution {
103
+ schemaVersion: 1;
104
+ effectId: string;
105
+ accountId: string;
106
+ connectionId: string;
107
+ connectionGeneration: string;
108
+ providerModelId: string;
109
+ retryPolicy: "safe-metadata-read";
110
+ status: "pending" | "applied" | "failed";
111
+ model?: ConnectionModelCatalogV1["models"][number];
112
+ failure?: string;
113
+ }
114
+
115
+ type StoredCommandTombstone = StoredCommand & {
116
+ receipt: ConnectionCommandReceiptV1;
117
+ completedAt: number;
118
+ };
119
+
120
+ type OllamaCredentialContribution = Omit<
121
+ CredentialUserBackendContribution,
122
+ "discardPending" | "lease" | "replayLease" | "settle"
123
+ > & {
124
+ discardPending(
125
+ connectionId: string,
126
+ generation: string,
127
+ storage?: CredentialTransaction,
128
+ ): Promise<void>;
129
+ replayLease(input: {
130
+ accountId: string;
131
+ connectionId: string;
132
+ packageId: string;
133
+ effectId: string;
134
+ }): Promise<CredentialLeaseV1 | undefined>;
135
+ lease(
136
+ input: {
137
+ accountId: string;
138
+ connectionId: string;
139
+ packageId: string;
140
+ effectId: string;
141
+ expiresAt: string;
142
+ expectedGeneration: string;
143
+ credentialState?: "active" | "pending";
144
+ },
145
+ storage?: CredentialTransaction,
146
+ ): Promise<CredentialLeaseV1>;
147
+ settle(input: {
148
+ accountId: string;
149
+ connectionId: string;
150
+ packageId: string;
151
+ effectId: string;
152
+ }): Promise<void>;
153
+ };
154
+
155
+ // Ollama Cloud names its models bare (`gpt-oss:20b`, `glm-5.1`), with no
156
+ // `:cloud` suffix. `gpt-oss:20b` is the smallest model that is routinely
157
+ // present, so a probe against it costs the least; otherwise the first
158
+ // discovered model has to do.
159
+ const PREFERRED_PROBE_MODEL_ID = "gpt-oss:20b";
160
+
161
+ function probeModelId(models: readonly ConnectionModelV1[]): string {
162
+ const preferred = models.find(
163
+ (model) => model.providerModelId === PREFERRED_PROBE_MODEL_ID,
164
+ );
165
+ const chosen = preferred ?? models[0];
166
+ if (!chosen) {
167
+ throw new Error(
168
+ "Ollama Cloud exposed no model to validate the key against",
169
+ );
170
+ }
171
+ return chosen.providerModelId;
172
+ }
173
+
174
+ // The only setting this Package's Connection Type declares (manifest v4). The
175
+ // Connection settings bag carries it under the same id, which the shared
176
+ // decoder requires to be lower-case kebab.
177
+ const API_BASE_URL_SETTING = "api-base-url";
178
+
179
+ /** The per-value bound the shared Connection settings decoder enforces. */
180
+ const MAX_CONNECTION_SETTING_VALUE = 2_048;
181
+
182
+ /**
183
+ * Decode the Connection settings a `connection/create-api-key` command carried.
184
+ *
185
+ * An unknown key or an unusable endpoint is a User-visible refusal, not a
186
+ * silently ignored field.
187
+ */
188
+ function decodeOllamaConnectionSettings(
189
+ input: ConnectionSettingsV1 | undefined,
190
+ ): Record<string, string> {
191
+ if (input === undefined) return {};
192
+ const entries = Object.entries(input);
193
+ if (entries.length > MAX_CONNECTION_SETTINGS_V1) {
194
+ throw new Error("Ollama Cloud Connection settings are too many");
195
+ }
196
+ const accepted: Record<string, string> = {};
197
+ for (const [key, value] of entries) {
198
+ if (key !== API_BASE_URL_SETTING) {
199
+ throw new Error(
200
+ `Ollama Cloud Connection setting "${key}" is not supported`,
201
+ );
202
+ }
203
+ accepted[key] = decodeOllamaApiBaseUrl(value);
204
+ }
205
+ return accepted;
206
+ }
207
+
208
+ function decodeStoredConnectionSettings(
209
+ input: unknown,
210
+ ): Record<string, string> {
211
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
212
+ throw new Error("Stored Ollama Connection settings are invalid");
213
+ }
214
+ const entries = Object.entries(input as Record<string, unknown>);
215
+ if (entries.length > MAX_CONNECTION_SETTINGS_V1) {
216
+ throw new Error("Stored Ollama Connection settings are invalid");
217
+ }
218
+ const raw: Record<string, string> = {};
219
+ for (const [key, value] of entries) {
220
+ raw[key] = storedText(
221
+ value,
222
+ `settings.${key}`,
223
+ MAX_CONNECTION_SETTING_VALUE,
224
+ );
225
+ }
226
+ try {
227
+ return decodeOllamaConnectionSettings(raw);
228
+ } catch {
229
+ throw new Error("Stored Ollama Connection settings are invalid");
230
+ }
231
+ }
232
+
233
+ /** Read a Connection's endpoint root off its durable projection. */
234
+ function connectionApiBaseUrl(
235
+ connection: ConnectionView | undefined,
236
+ ): string | undefined {
237
+ const candidate = connection?.settings?.[API_BASE_URL_SETTING];
238
+ return candidate === undefined
239
+ ? undefined
240
+ : decodeOllamaApiBaseUrl(candidate);
241
+ }
242
+
243
+ export interface OllamaUserBackendHost {
244
+ storage: UserSettingsStorage &
245
+ CredentialStorage & {
246
+ getAlarm?(): Promise<number | null>;
247
+ setAlarm(scheduledTime: number | Date): Promise<void>;
248
+ };
249
+ settings: UserSettingsBackendContribution;
250
+ credentials: OllamaCredentialContribution;
251
+ /** A client the host supplies for every Connection; wins when given. */
252
+ client?: OllamaCloudClient;
253
+ /** Build a client for one Connection's endpoint. */
254
+ createClient?(config: OllamaCloudClientConfig): OllamaCloudClient;
255
+ now?: () => number;
256
+ randomId?: () => string;
257
+ }
258
+
259
+ function commandKey(commandId: string): string {
260
+ return `${COMMAND_PREFIX}${commandId}`;
261
+ }
262
+
263
+ function modelResolutionKey(effectId: string): string {
264
+ return `${MODEL_RESOLUTION_PREFIX}${effectId}`;
265
+ }
266
+
267
+ function mutationSequenceKey(
268
+ connectionId: string,
269
+ operation: StoredCommand["operation"],
270
+ ): string {
271
+ return `${MUTATION_SEQUENCE_PREFIX}${connectionId}:${operation}`;
272
+ }
273
+
274
+ async function fingerprint(value: unknown): Promise<string> {
275
+ const digest = await crypto.subtle.digest(
276
+ "SHA-256",
277
+ new TextEncoder().encode(JSON.stringify(value)),
278
+ );
279
+ return Array.from(new Uint8Array(digest), (byte) =>
280
+ byte.toString(16).padStart(2, "0"),
281
+ ).join("");
282
+ }
283
+
284
+ function receipt(
285
+ record: StoredCommand,
286
+ status: ConnectionCommandReceiptV1["status"],
287
+ ): ConnectionCommandReceiptV1 {
288
+ return {
289
+ schemaVersion: 1,
290
+ commandId: record.commandId,
291
+ connectionId: record.connectionId,
292
+ status,
293
+ };
294
+ }
295
+
296
+ function storedRecord(
297
+ input: unknown,
298
+ label: string,
299
+ required: readonly string[],
300
+ optional: readonly string[] = [],
301
+ ): Record<string, unknown> {
302
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
303
+ throw new Error(`${label} is invalid`);
304
+ }
305
+ const value = input as Record<string, unknown>;
306
+ const allowed = new Set([...required, ...optional]);
307
+ if (
308
+ !required.every((key) => Object.hasOwn(value, key)) ||
309
+ Object.keys(value).some((key) => !allowed.has(key))
310
+ ) {
311
+ throw new Error(`${label} is invalid`);
312
+ }
313
+ return value;
314
+ }
315
+
316
+ function storedText(value: unknown, label: string, maximum: number): string {
317
+ if (
318
+ typeof value !== "string" ||
319
+ value.length === 0 ||
320
+ value.length > maximum
321
+ ) {
322
+ throw new Error(`${label} is invalid`);
323
+ }
324
+ return value;
325
+ }
326
+
327
+ function decodeStoredAccount(input: unknown): string {
328
+ return storedText(input, "Stored Ollama account", 256);
329
+ }
330
+
331
+ function decodeCommandIdList(input: unknown, label: string): string[] {
332
+ if (!Array.isArray(input)) {
333
+ throw new Error(`Stored Ollama ${label} is invalid`);
334
+ }
335
+ return [
336
+ ...new Set(input.map((value) => storedText(value, "commandId", 128))),
337
+ ];
338
+ }
339
+
340
+ function decodePendingCommands(input: unknown): string[] {
341
+ const pending = decodeCommandIdList(input, "pending commands");
342
+ if (pending.length > MAX_PENDING_COMMANDS) {
343
+ throw new Error("Stored Ollama pending commands are invalid");
344
+ }
345
+ return pending;
346
+ }
347
+
348
+ function decodeReceiptIndex(input: unknown): string[] {
349
+ const index = decodeCommandIdList(input, "receipt index");
350
+ if (index.length > MAX_MANUAL_RECEIPTS) {
351
+ throw new Error("Stored Ollama receipt index is invalid");
352
+ }
353
+ return index;
354
+ }
355
+
356
+ function decodeCommandTombstones(input: unknown): StoredCommandTombstone[] {
357
+ if (!Array.isArray(input) || input.length > MAX_COMMAND_TOMBSTONES) {
358
+ throw new Error("Stored Ollama command tombstones are invalid");
359
+ }
360
+ return input.map((value) => {
361
+ const command = decodeStoredCommand(value);
362
+ if (!command.receipt || command.completedAt === undefined) {
363
+ throw new Error("Stored Ollama command tombstone is invalid");
364
+ }
365
+ return command as StoredCommandTombstone;
366
+ });
367
+ }
368
+
369
+ function decodeStoredCommand(input: unknown): StoredCommand {
370
+ const value = storedRecord(
371
+ input,
372
+ "Stored Ollama command",
373
+ [
374
+ "schemaVersion",
375
+ "commandId",
376
+ "fingerprint",
377
+ "accountId",
378
+ "connectionId",
379
+ "operation",
380
+ ],
381
+ [
382
+ "credentialGeneration",
383
+ "expectedGeneration",
384
+ "label",
385
+ "settings",
386
+ "enabled",
387
+ "revokeUpstream",
388
+ "receipt",
389
+ "completedAt",
390
+ "automaticRefresh",
391
+ "mutationSequence",
392
+ "settlementEffectId",
393
+ "settlementStatus",
394
+ "validationCatalog",
395
+ "validationFailure",
396
+ "validationStatus",
397
+ "providerRetryPolicy",
398
+ ],
399
+ );
400
+ const operations: StoredCommand["operation"][] = [
401
+ "connection/create-api-key",
402
+ "connection/rotate-api-key",
403
+ "connection/update-label",
404
+ "connection/refresh-models",
405
+ "connection/set-enabled",
406
+ "connection/disconnect",
407
+ ];
408
+ if (
409
+ value.schemaVersion !== 1 ||
410
+ !operations.includes(value.operation as never) ||
411
+ (value.enabled !== undefined && typeof value.enabled !== "boolean") ||
412
+ (value.revokeUpstream !== undefined &&
413
+ typeof value.revokeUpstream !== "boolean") ||
414
+ (value.completedAt !== undefined &&
415
+ (typeof value.completedAt !== "number" ||
416
+ !Number.isSafeInteger(value.completedAt) ||
417
+ value.completedAt < 0)) ||
418
+ (value.automaticRefresh !== undefined &&
419
+ typeof value.automaticRefresh !== "boolean") ||
420
+ (value.mutationSequence !== undefined &&
421
+ (typeof value.mutationSequence !== "number" ||
422
+ !Number.isSafeInteger(value.mutationSequence) ||
423
+ value.mutationSequence <= 0)) ||
424
+ (value.settlementStatus !== undefined &&
425
+ value.settlementStatus !== "applied" &&
426
+ value.settlementStatus !== "failed") ||
427
+ Boolean(value.settlementEffectId) !== Boolean(value.settlementStatus) ||
428
+ (value.validationStatus !== undefined &&
429
+ value.validationStatus !== "applied" &&
430
+ value.validationStatus !== "failed") ||
431
+ (value.validationStatus === "applied") !==
432
+ (value.validationCatalog !== undefined) ||
433
+ (value.validationStatus === "failed") !==
434
+ (value.validationFailure !== undefined) ||
435
+ (value.providerRetryPolicy !== undefined &&
436
+ value.providerRetryPolicy !== "safe-metadata-read")
437
+ ) {
438
+ throw new Error("Stored Ollama command is invalid");
439
+ }
440
+ const commandId = storedText(value.commandId, "commandId", 128);
441
+ const connectionId = storedText(value.connectionId, "connectionId", 128);
442
+ const decodedReceipt =
443
+ value.receipt === undefined
444
+ ? undefined
445
+ : decodeConnectionCommandReceiptV1(value.receipt);
446
+ const validationCatalog =
447
+ value.validationCatalog === undefined
448
+ ? undefined
449
+ : decodeConnectionModelCatalogV1(value.validationCatalog);
450
+ if (
451
+ decodedReceipt &&
452
+ (decodedReceipt.commandId !== commandId ||
453
+ decodedReceipt.connectionId !== connectionId)
454
+ ) {
455
+ throw new Error("Stored Ollama command receipt is invalid");
456
+ }
457
+ return {
458
+ schemaVersion: 1,
459
+ commandId,
460
+ fingerprint: storedText(value.fingerprint, "fingerprint", 64),
461
+ accountId: storedText(value.accountId, "accountId", 256),
462
+ connectionId,
463
+ operation: value.operation as StoredCommand["operation"],
464
+ ...(value.credentialGeneration === undefined
465
+ ? {}
466
+ : {
467
+ credentialGeneration: storedText(
468
+ value.credentialGeneration,
469
+ "credentialGeneration",
470
+ 128,
471
+ ),
472
+ }),
473
+ ...(value.expectedGeneration === undefined
474
+ ? {}
475
+ : {
476
+ expectedGeneration: storedText(
477
+ value.expectedGeneration,
478
+ "expectedGeneration",
479
+ 128,
480
+ ),
481
+ }),
482
+ ...(value.label === undefined
483
+ ? {}
484
+ : { label: storedText(value.label, "label", 120) }),
485
+ ...(value.settings === undefined
486
+ ? {}
487
+ : { settings: decodeStoredConnectionSettings(value.settings) }),
488
+ ...(value.enabled === undefined
489
+ ? {}
490
+ : { enabled: value.enabled as boolean }),
491
+ ...(value.revokeUpstream === undefined
492
+ ? {}
493
+ : { revokeUpstream: value.revokeUpstream as boolean }),
494
+ ...(decodedReceipt === undefined ? {} : { receipt: decodedReceipt }),
495
+ ...(value.completedAt === undefined
496
+ ? {}
497
+ : { completedAt: value.completedAt as number }),
498
+ ...(value.automaticRefresh === undefined
499
+ ? {}
500
+ : { automaticRefresh: value.automaticRefresh as boolean }),
501
+ ...(value.mutationSequence === undefined
502
+ ? {}
503
+ : { mutationSequence: value.mutationSequence as number }),
504
+ ...(value.settlementEffectId === undefined
505
+ ? {}
506
+ : {
507
+ settlementEffectId: storedText(
508
+ value.settlementEffectId,
509
+ "settlementEffectId",
510
+ 256,
511
+ ),
512
+ settlementStatus: value.settlementStatus as "applied" | "failed",
513
+ }),
514
+ ...(value.validationStatus === undefined
515
+ ? {}
516
+ : {
517
+ validationStatus: value.validationStatus as "applied" | "failed",
518
+ ...(validationCatalog ? { validationCatalog } : {}),
519
+ ...(value.validationFailure === undefined
520
+ ? {}
521
+ : {
522
+ validationFailure: storedText(
523
+ value.validationFailure,
524
+ "validationFailure",
525
+ 500,
526
+ ),
527
+ }),
528
+ }),
529
+ ...(value.providerRetryPolicy === undefined
530
+ ? {}
531
+ : { providerRetryPolicy: "safe-metadata-read" as const }),
532
+ };
533
+ }
534
+
535
+ function decodeStoredModelResolution(input: unknown): StoredModelResolution {
536
+ const value = storedRecord(
537
+ input,
538
+ "Stored Ollama model resolution",
539
+ [
540
+ "schemaVersion",
541
+ "effectId",
542
+ "accountId",
543
+ "connectionId",
544
+ "connectionGeneration",
545
+ "providerModelId",
546
+ "retryPolicy",
547
+ "status",
548
+ ],
549
+ ["model", "failure"],
550
+ );
551
+ const status = value.status;
552
+ if (
553
+ value.schemaVersion !== 1 ||
554
+ value.retryPolicy !== "safe-metadata-read" ||
555
+ (status !== "pending" && status !== "applied" && status !== "failed") ||
556
+ (status === "applied") !== (value.model !== undefined) ||
557
+ (status === "failed") !== (value.failure !== undefined)
558
+ ) {
559
+ throw new Error("Stored Ollama model resolution is invalid");
560
+ }
561
+ const model =
562
+ value.model === undefined
563
+ ? undefined
564
+ : decodeConnectionModelCatalogV1({
565
+ schemaVersion: 1,
566
+ generation: "resolution-journal",
567
+ state: "fresh",
568
+ models: [value.model],
569
+ }).models[0];
570
+ return {
571
+ schemaVersion: 1,
572
+ effectId: storedText(value.effectId, "effectId", 256),
573
+ accountId: storedText(value.accountId, "accountId", 256),
574
+ connectionId: storedText(value.connectionId, "connectionId", 128),
575
+ connectionGeneration: storedText(
576
+ value.connectionGeneration,
577
+ "connectionGeneration",
578
+ 128,
579
+ ),
580
+ providerModelId: storedText(value.providerModelId, "providerModelId", 256),
581
+ retryPolicy: "safe-metadata-read",
582
+ status,
583
+ ...(model ? { model } : {}),
584
+ ...(value.failure === undefined
585
+ ? {}
586
+ : { failure: storedText(value.failure, "failure", 500) }),
587
+ };
588
+ }
589
+
590
+ function decodeMutationSequence(input: unknown): {
591
+ next: number;
592
+ applied: number;
593
+ } {
594
+ const value = storedRecord(input, "Stored Ollama mutation sequence", [
595
+ "schemaVersion",
596
+ "next",
597
+ "applied",
598
+ ]);
599
+ if (
600
+ value.schemaVersion !== 1 ||
601
+ typeof value.next !== "number" ||
602
+ !Number.isSafeInteger(value.next) ||
603
+ value.next < 0 ||
604
+ typeof value.applied !== "number" ||
605
+ !Number.isSafeInteger(value.applied) ||
606
+ value.applied < 0 ||
607
+ value.applied > value.next
608
+ ) {
609
+ throw new Error("Stored Ollama mutation sequence is invalid");
610
+ }
611
+ return { next: value.next, applied: value.applied };
612
+ }
613
+
614
+ function compactCompletedCommand(
615
+ record: StoredCommand,
616
+ ): StoredCommandTombstone {
617
+ if (!record.receipt || record.completedAt === undefined) {
618
+ throw new Error("Completed Ollama command receipt is unavailable");
619
+ }
620
+ return {
621
+ schemaVersion: 1,
622
+ commandId: record.commandId,
623
+ fingerprint: record.fingerprint,
624
+ accountId: record.accountId,
625
+ connectionId: record.connectionId,
626
+ operation: record.operation,
627
+ receipt: record.receipt,
628
+ completedAt: record.completedAt,
629
+ };
630
+ }
631
+
632
+ function isSequencedMutation(operation: StoredCommand["operation"]): boolean {
633
+ return (
634
+ operation === "connection/update-label" ||
635
+ operation === "connection/set-enabled" ||
636
+ operation === "connection/refresh-models"
637
+ );
638
+ }
639
+
640
+ function retainResolvedModel(
641
+ catalog: ConnectionModelCatalogV1,
642
+ resolved: ConnectionModelCatalogV1["models"][number],
643
+ ): ConnectionModelCatalogV1["models"] {
644
+ const discovered = catalog.models
645
+ .filter((model) => model.source === "discovered")
646
+ .slice(0, MAX_DISCOVERED_MODELS);
647
+ const available = MAX_CONNECTION_MODELS - discovered.length;
648
+ if (available === 0) return discovered;
649
+ const exact = [
650
+ ...catalog.models.filter(
651
+ (model) =>
652
+ model.source === "exact-resolution" &&
653
+ model.providerModelId !== resolved.providerModelId,
654
+ ),
655
+ resolved,
656
+ ];
657
+ return [...discovered, ...exact.slice(-available)];
658
+ }
659
+
660
+ export class OllamaCloudUserBackendContribution {
661
+ readonly packageId = PACKAGE_ID;
662
+ private readonly now: () => number;
663
+ private readonly randomId: () => string;
664
+ private readonly resumptions = new Map<
665
+ string,
666
+ { fingerprint: string; promise: Promise<ConnectionCommandReceiptV1> }
667
+ >();
668
+
669
+ constructor(private readonly host: OllamaUserBackendHost) {
670
+ this.now = host.now ?? Date.now;
671
+ this.randomId = host.randomId ?? crypto.randomUUID.bind(crypto);
672
+ }
673
+
674
+ async executeConnection(
675
+ accountId: string,
676
+ input: unknown,
677
+ ): Promise<ConnectionCommandReceiptV1> {
678
+ return this.executeCommand(
679
+ accountId,
680
+ decodeConnectionCommandV1(input),
681
+ false,
682
+ );
683
+ }
684
+
685
+ async lookupConnectionCommand(
686
+ accountId: string,
687
+ commandId: string,
688
+ ): Promise<ConnectionCommandReceiptV1 | undefined> {
689
+ await this.host.settings.read(accountId);
690
+ const value = await this.host.storage.get<unknown>(commandKey(commandId));
691
+ const tombstonesValue = await this.host.storage.get<unknown>(
692
+ COMMAND_TOMBSTONES_KEY,
693
+ );
694
+ const record =
695
+ value === undefined
696
+ ? (tombstonesValue === undefined
697
+ ? []
698
+ : decodeCommandTombstones(tombstonesValue)
699
+ ).find((candidate) => candidate.commandId === commandId)
700
+ : decodeStoredCommand(value);
701
+ if (!record) return undefined;
702
+ if (record.accountId !== accountId) {
703
+ throw new Error("Connection command authority does not match");
704
+ }
705
+ return record.receipt;
706
+ }
707
+
708
+ private async executeCommand(
709
+ accountId: string,
710
+ command: ConnectionCommandV1,
711
+ automaticRefresh: boolean,
712
+ ): Promise<ConnectionCommandReceiptV1> {
713
+ const storedAccountValue =
714
+ await this.host.storage.get<unknown>(ACCOUNT_KEY);
715
+ const storedAccount =
716
+ storedAccountValue === undefined
717
+ ? undefined
718
+ : decodeStoredAccount(storedAccountValue);
719
+ if (storedAccount && storedAccount !== accountId) {
720
+ throw new Error("Ollama Connection authority does not match");
721
+ }
722
+ if (!storedAccount) await this.host.storage.put(ACCOUNT_KEY, accountId);
723
+ const commandFingerprint = await fingerprint(command);
724
+ const existingValue = await this.host.storage.get<unknown>(
725
+ commandKey(command.commandId),
726
+ );
727
+ const existing =
728
+ existingValue === undefined
729
+ ? undefined
730
+ : decodeStoredCommand(existingValue);
731
+ if (existing) {
732
+ if (
733
+ existing.accountId !== accountId ||
734
+ existing.fingerprint !== commandFingerprint
735
+ ) {
736
+ throw new Error("Connection command idempotency key was reused");
737
+ }
738
+ if (existing.receipt) return existing.receipt;
739
+ return this.resumeOnce(
740
+ existing,
741
+ command.type === "connection/create-api-key" ||
742
+ command.type === "connection/rotate-api-key"
743
+ ? command.apiKey
744
+ : undefined,
745
+ );
746
+ }
747
+ const tombstonesValue = await this.host.storage.get<unknown>(
748
+ COMMAND_TOMBSTONES_KEY,
749
+ );
750
+ const tombstone = (
751
+ tombstonesValue === undefined
752
+ ? []
753
+ : decodeCommandTombstones(tombstonesValue)
754
+ ).find((candidate) => candidate.commandId === command.commandId);
755
+ if (tombstone) {
756
+ if (
757
+ tombstone.accountId !== accountId ||
758
+ tombstone.fingerprint !== commandFingerprint
759
+ ) {
760
+ throw new Error("Connection command idempotency key was reused");
761
+ }
762
+ return tombstone.receipt;
763
+ }
764
+
765
+ const record = await this.admit(
766
+ accountId,
767
+ command,
768
+ commandFingerprint,
769
+ automaticRefresh,
770
+ );
771
+ return record.receipt ?? this.resumeOnce(record);
772
+ }
773
+
774
+ private async admit(
775
+ accountId: string,
776
+ command: ConnectionCommandV1,
777
+ commandFingerprint: string,
778
+ automaticRefresh: boolean,
779
+ ): Promise<StoredCommand> {
780
+ if (command.type === "connection/create-api-key") {
781
+ if (
782
+ command.packageId !== PACKAGE_ID ||
783
+ command.connectionTypeId !== CONNECTION_TYPE_ID
784
+ ) {
785
+ throw new Error("Ollama Cloud Connection type is invalid");
786
+ }
787
+ if (
788
+ !(await this.host.settings.isPackageInstalled(accountId, PACKAGE_ID))
789
+ ) {
790
+ throw new Error("Ollama Cloud Package is not installed and enabled");
791
+ }
792
+ const connectionId = `connection-${this.randomId()}`;
793
+ const generation = this.randomId();
794
+ // An unusable endpoint is admitted and then refused, so the User sees a
795
+ // failed receipt and a failed Connection carrying the reason rather than
796
+ // an unhandled throw. No provider request is made for it.
797
+ let accepted: Record<string, string> = {};
798
+ let settingsFailure: string | undefined;
799
+ try {
800
+ accepted = decodeOllamaConnectionSettings(command.settings);
801
+ } catch (error) {
802
+ settingsFailure = (
803
+ error instanceof Error && error.message
804
+ ? error.message
805
+ : "Ollama Cloud Connection settings are invalid"
806
+ ).slice(0, 500);
807
+ }
808
+ const record: StoredCommand = {
809
+ schemaVersion: 1,
810
+ commandId: command.commandId,
811
+ fingerprint: commandFingerprint,
812
+ accountId,
813
+ connectionId,
814
+ credentialGeneration: generation,
815
+ operation: command.type,
816
+ label: command.label,
817
+ ...(Object.keys(accepted).length > 0 ? { settings: accepted } : {}),
818
+ providerRetryPolicy: "safe-metadata-read",
819
+ ...(settingsFailure === undefined
820
+ ? {}
821
+ : {
822
+ validationFailure: settingsFailure,
823
+ validationStatus: "failed" as const,
824
+ }),
825
+ };
826
+ const prepared = await this.host.credentials.prepareApiKey({
827
+ accountId,
828
+ connectionId,
829
+ packageId: PACKAGE_ID,
830
+ generation,
831
+ apiKey: command.apiKey,
832
+ });
833
+ const admitted = await this.host.storage.transaction(
834
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
835
+ const admission = await this.admitRecord(record, storage);
836
+ if (!admission.created) return admission.record;
837
+ const settings = await this.host.settings.readSnapshot(storage);
838
+ if (
839
+ !settings.packages.some(
840
+ (pkg) =>
841
+ pkg.packageId === PACKAGE_ID && pkg.state === "installed",
842
+ )
843
+ ) {
844
+ throw new Error(
845
+ "Ollama Cloud Package is not installed and enabled",
846
+ );
847
+ }
848
+ await this.host.settings.createConnection(
849
+ accountId,
850
+ this.authorizingConnection(record, command.label),
851
+ storage,
852
+ );
853
+ await this.host.credentials.stagePreparedApiKey(prepared, storage);
854
+ return record;
855
+ },
856
+ );
857
+ await this.host.storage.setAlarm(this.now() + RECOVERY_DELAY_MS);
858
+ return admitted;
859
+ }
860
+
861
+ if (command.type === "connection/create") {
862
+ throw new Error("Ollama Cloud Connections require an API key");
863
+ }
864
+
865
+ const connection = await this.requireConnection(
866
+ accountId,
867
+ command.connectionId,
868
+ );
869
+ if (command.type === "connection/rotate-api-key") {
870
+ if (connection.state !== "ready" && connection.state !== "disabled") {
871
+ throw new Error(`Connection is ${connection.state}`);
872
+ }
873
+ const generation = this.randomId();
874
+ const record: StoredCommand = {
875
+ schemaVersion: 1,
876
+ commandId: command.commandId,
877
+ fingerprint: commandFingerprint,
878
+ accountId,
879
+ connectionId: connection.connectionId,
880
+ credentialGeneration: generation,
881
+ expectedGeneration: connection.generation,
882
+ operation: command.type,
883
+ providerRetryPolicy: "safe-metadata-read",
884
+ };
885
+ const prepared = await this.host.credentials.prepareApiKey({
886
+ accountId,
887
+ connectionId: connection.connectionId,
888
+ packageId: PACKAGE_ID,
889
+ generation,
890
+ apiKey: command.apiKey,
891
+ });
892
+ const admitted = await this.host.storage.transaction(
893
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
894
+ const admission = await this.admitRecord(record, storage);
895
+ if (!admission.created) return admission.record;
896
+ const settings = await this.host.settings.readSnapshot(storage);
897
+ const current = settings.connections.find(
898
+ (candidate) => candidate.connectionId === connection.connectionId,
899
+ );
900
+ if (
901
+ !settings.packages.some(
902
+ (pkg) =>
903
+ pkg.packageId === PACKAGE_ID && pkg.state === "installed",
904
+ ) ||
905
+ !current ||
906
+ current.generation !== connection.generation ||
907
+ (current.state !== "ready" && current.state !== "disabled")
908
+ ) {
909
+ throw new Error("Connection changed before credential rotation");
910
+ }
911
+ await this.host.credentials.stagePreparedApiKey(prepared, storage);
912
+ return record;
913
+ },
914
+ );
915
+ await this.host.storage.setAlarm(this.now() + RECOVERY_DELAY_MS);
916
+ return admitted;
917
+ }
918
+
919
+ const record: StoredCommand = {
920
+ schemaVersion: 1,
921
+ commandId: command.commandId,
922
+ fingerprint: commandFingerprint,
923
+ accountId,
924
+ connectionId: connection.connectionId,
925
+ expectedGeneration: connection.generation,
926
+ operation: command.type,
927
+ ...(command.type === "connection/refresh-models"
928
+ ? { providerRetryPolicy: "safe-metadata-read" as const }
929
+ : {}),
930
+ ...(command.type === "connection/update-label"
931
+ ? { label: command.label }
932
+ : {}),
933
+ ...(command.type === "connection/set-enabled"
934
+ ? { enabled: command.enabled }
935
+ : {}),
936
+ ...(command.type === "connection/disconnect"
937
+ ? { revokeUpstream: command.revokeUpstream }
938
+ : {}),
939
+ ...(automaticRefresh ? { automaticRefresh: true } : {}),
940
+ };
941
+ const admitted = await this.admitRecord(record);
942
+ await this.host.storage.setAlarm(this.now() + RECOVERY_DELAY_MS);
943
+ return admitted.record;
944
+ }
945
+
946
+ private async ensureAdmittedState(
947
+ record: StoredCommand,
948
+ apiKey?: string,
949
+ ): Promise<void> {
950
+ if (record.operation === "connection/create-api-key") {
951
+ const current = await this.host.settings.getConnection(
952
+ record.accountId,
953
+ record.connectionId,
954
+ );
955
+ if (!current) {
956
+ await this.host.settings.createConnection(
957
+ record.accountId,
958
+ this.authorizingConnection(record, record.label ?? "Ollama Cloud"),
959
+ );
960
+ }
961
+ }
962
+ if (apiKey && record.credentialGeneration) {
963
+ await this.host.credentials.stageApiKey({
964
+ accountId: record.accountId,
965
+ connectionId: record.connectionId,
966
+ packageId: PACKAGE_ID,
967
+ generation: record.credentialGeneration,
968
+ apiKey,
969
+ });
970
+ }
971
+ }
972
+
973
+ private async admitRecord(
974
+ record: StoredCommand,
975
+ storage?: CredentialTransaction,
976
+ ): Promise<{ record: StoredCommand; created: boolean }> {
977
+ const admit = async (transaction: CredentialTransaction) => {
978
+ const existingValue = await transaction.get<unknown>(
979
+ commandKey(record.commandId),
980
+ );
981
+ const tombstonesValue = await transaction.get<unknown>(
982
+ COMMAND_TOMBSTONES_KEY,
983
+ );
984
+ const tombstones =
985
+ tombstonesValue === undefined
986
+ ? []
987
+ : decodeCommandTombstones(tombstonesValue);
988
+ const tombstone = tombstones.find(
989
+ (candidate) => candidate.commandId === record.commandId,
990
+ );
991
+ if (tombstone) {
992
+ if (
993
+ tombstone.accountId !== record.accountId ||
994
+ tombstone.fingerprint !== record.fingerprint
995
+ ) {
996
+ throw new Error("Connection command idempotency key was reused");
997
+ }
998
+ return { record: tombstone, created: false };
999
+ }
1000
+ const existing =
1001
+ existingValue === undefined
1002
+ ? undefined
1003
+ : decodeStoredCommand(existingValue);
1004
+ if (existing) {
1005
+ if (
1006
+ existing.accountId !== record.accountId ||
1007
+ existing.fingerprint !== record.fingerprint
1008
+ ) {
1009
+ throw new Error("Connection command idempotency key was reused");
1010
+ }
1011
+ return { record: existing, created: false };
1012
+ }
1013
+ const pendingValue = await transaction.get<unknown>(PENDING_KEY);
1014
+ const pending = (
1015
+ pendingValue === undefined ? [] : decodePendingCommands(pendingValue)
1016
+ ).filter((id) => id !== record.commandId);
1017
+ if (pending.length >= MAX_PENDING_COMMANDS) {
1018
+ throw new Error("Ollama Connection command capacity reached");
1019
+ }
1020
+ if (!record.automaticRefresh) {
1021
+ const receiptIndexValue =
1022
+ await transaction.get<unknown>(RECEIPT_INDEX_KEY);
1023
+ const receiptIndex =
1024
+ receiptIndexValue === undefined
1025
+ ? []
1026
+ : decodeReceiptIndex(receiptIndexValue);
1027
+ if (
1028
+ receiptIndex.length + tombstones.length + pending.length >=
1029
+ MAX_MANUAL_COMMANDS
1030
+ ) {
1031
+ throw new Error("Ollama Connection command history capacity reached");
1032
+ }
1033
+ }
1034
+ let admitted = record;
1035
+ let sequenceEntry: Record<string, unknown> = {};
1036
+ if (isSequencedMutation(record.operation)) {
1037
+ const sequenceValue = await transaction.get<unknown>(
1038
+ mutationSequenceKey(record.connectionId, record.operation),
1039
+ );
1040
+ const sequence =
1041
+ sequenceValue === undefined
1042
+ ? { next: 0, applied: 0 }
1043
+ : decodeMutationSequence(sequenceValue);
1044
+ const next = sequence.next + 1;
1045
+ admitted = { ...record, mutationSequence: next };
1046
+ sequenceEntry = {
1047
+ [mutationSequenceKey(record.connectionId, record.operation)]: {
1048
+ schemaVersion: 1,
1049
+ next,
1050
+ applied: sequence.applied,
1051
+ },
1052
+ };
1053
+ }
1054
+ await transaction.put({
1055
+ [commandKey(record.commandId)]: admitted,
1056
+ [PENDING_KEY]: [...pending, record.commandId],
1057
+ ...sequenceEntry,
1058
+ });
1059
+ await transaction.setAlarm?.(this.now() + RECOVERY_DELAY_MS);
1060
+ return { record: admitted, created: true };
1061
+ };
1062
+ return storage ? admit(storage) : this.host.storage.transaction(admit);
1063
+ }
1064
+
1065
+ private authorizingConnection(
1066
+ record: StoredCommand,
1067
+ displayName: string,
1068
+ ): ConnectionView {
1069
+ const updatedAt = new Date(this.now()).toISOString();
1070
+ return {
1071
+ connectionId: record.connectionId,
1072
+ packageId: PACKAGE_ID,
1073
+ connectionTypeId: CONNECTION_TYPE_ID,
1074
+ displayName,
1075
+ state: "authorizing",
1076
+ generation: record.credentialGeneration,
1077
+ providerType: OLLAMA_CLOUD_PROVIDER,
1078
+ authorization: {
1079
+ schemaVersion: 1,
1080
+ kind: "api-key",
1081
+ credential: {
1082
+ schemaVersion: 1,
1083
+ configured: true,
1084
+ source: "api-key",
1085
+ writable: true,
1086
+ generation: record.credentialGeneration,
1087
+ updatedAt,
1088
+ },
1089
+ },
1090
+ settings: { ...(record.settings ?? {}) },
1091
+ safeMetadata: { creationCommandId: record.commandId },
1092
+ };
1093
+ }
1094
+
1095
+ private async resumeOnce(
1096
+ record: StoredCommand,
1097
+ apiKey?: string,
1098
+ ): Promise<ConnectionCommandReceiptV1> {
1099
+ const active = this.resumptions.get(record.commandId);
1100
+ if (active) {
1101
+ if (active.fingerprint !== record.fingerprint) {
1102
+ throw new Error("Connection command idempotency key was reused");
1103
+ }
1104
+ return active.promise;
1105
+ }
1106
+ const promise = (async () => {
1107
+ try {
1108
+ const sequenced = await this.ensureMutationSequence(record);
1109
+ await this.ensureAdmittedState(sequenced, apiKey);
1110
+ return await this.resume(sequenced);
1111
+ } catch (error) {
1112
+ await this.host.storage.setAlarm(this.now() + RECOVERY_DELAY_MS);
1113
+ throw error;
1114
+ }
1115
+ })();
1116
+ this.resumptions.set(record.commandId, {
1117
+ fingerprint: record.fingerprint,
1118
+ promise,
1119
+ });
1120
+ const release = () => {
1121
+ if (this.resumptions.get(record.commandId)?.promise === promise) {
1122
+ this.resumptions.delete(record.commandId);
1123
+ }
1124
+ };
1125
+ void promise.then(release, release);
1126
+ return promise;
1127
+ }
1128
+
1129
+ private async ensureMutationSequence(
1130
+ record: StoredCommand,
1131
+ ): Promise<StoredCommand> {
1132
+ if (!isSequencedMutation(record.operation) || record.mutationSequence) {
1133
+ return record;
1134
+ }
1135
+ return this.host.storage.transaction(
1136
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1137
+ const storedValue = await storage.get<unknown>(
1138
+ commandKey(record.commandId),
1139
+ );
1140
+ if (storedValue === undefined) return record;
1141
+ const stored = decodeStoredCommand(storedValue);
1142
+ if (stored.mutationSequence) return stored;
1143
+ const sequenceValue = await storage.get<unknown>(
1144
+ mutationSequenceKey(record.connectionId, record.operation),
1145
+ );
1146
+ const sequence =
1147
+ sequenceValue === undefined
1148
+ ? { next: 0, applied: 0 }
1149
+ : decodeMutationSequence(sequenceValue);
1150
+ const next = sequence.next + 1;
1151
+ const sequenced = { ...stored, mutationSequence: next };
1152
+ await storage.put({
1153
+ [commandKey(record.commandId)]: sequenced,
1154
+ [mutationSequenceKey(record.connectionId, record.operation)]: {
1155
+ schemaVersion: 1,
1156
+ next,
1157
+ applied: sequence.applied,
1158
+ },
1159
+ });
1160
+ return sequenced;
1161
+ },
1162
+ );
1163
+ }
1164
+
1165
+ private async resume(
1166
+ record: StoredCommand,
1167
+ ): Promise<ConnectionCommandReceiptV1> {
1168
+ switch (record.operation) {
1169
+ case "connection/create-api-key":
1170
+ case "connection/rotate-api-key":
1171
+ return this.validateAndActivate(record);
1172
+ case "connection/update-label":
1173
+ return this.updateLabel(record);
1174
+ case "connection/refresh-models":
1175
+ return this.refreshCatalog(record);
1176
+ case "connection/set-enabled":
1177
+ return this.setEnabled(record);
1178
+ case "connection/disconnect":
1179
+ return this.disconnect(record);
1180
+ }
1181
+ }
1182
+
1183
+ private catalog(
1184
+ models: ConnectionModelCatalogV1["models"],
1185
+ ): ConnectionModelCatalogV1 {
1186
+ const now = this.now();
1187
+ return {
1188
+ schemaVersion: 1,
1189
+ generation: this.randomId(),
1190
+ state: "fresh",
1191
+ models: models.slice(0, MAX_DISCOVERED_MODELS),
1192
+ refreshedAt: new Date(now).toISOString(),
1193
+ refreshAfter: new Date(now + REFRESH_INTERVAL_MS).toISOString(),
1194
+ };
1195
+ }
1196
+
1197
+ private async validateAndActivate(
1198
+ record: StoredCommand,
1199
+ ): Promise<ConnectionCommandReceiptV1> {
1200
+ const generation = record.credentialGeneration;
1201
+ if (!generation) throw new Error("Credential generation is unavailable");
1202
+ const effectId = `validation:${record.commandId}`;
1203
+ let outcome = record;
1204
+ const existingProjection = await this.requireConnection(
1205
+ record.accountId,
1206
+ record.connectionId,
1207
+ );
1208
+ if (
1209
+ !outcome.validationStatus &&
1210
+ existingProjection.generation === generation &&
1211
+ (existingProjection.state === "ready" ||
1212
+ existingProjection.state === "disabled")
1213
+ ) {
1214
+ return this.finishRecord(record, "applied");
1215
+ }
1216
+ if (!outcome.validationStatus) {
1217
+ try {
1218
+ if (outcome.providerRetryPolicy !== "safe-metadata-read") {
1219
+ throw new Error("Ollama validation retry policy is unavailable");
1220
+ }
1221
+ const lease = await this.host.credentials.lease({
1222
+ accountId: record.accountId,
1223
+ connectionId: record.connectionId,
1224
+ packageId: PACKAGE_ID,
1225
+ effectId,
1226
+ expiresAt: new Date(this.now() + MODEL_LEASE_MS).toISOString(),
1227
+ expectedGeneration: generation,
1228
+ credentialState: "pending",
1229
+ });
1230
+ const apiKey = await this.host.credentials.openLease({
1231
+ accountId: record.accountId,
1232
+ packageId: PACKAGE_ID,
1233
+ lease,
1234
+ });
1235
+ // A catalog read is not validation: measured against https://ollama.com
1236
+ // on 2026-08-31, `GET /api/tags`, `GET /v1/models`, and `POST
1237
+ // /api/show` all answer 200 for a valid key, a garbage key, and no key
1238
+ // at all (docs/research/ollama-cloud-auth.md), so `listModels` alone
1239
+ // promotes a bad key to `ready` and the User only learns it is bad when
1240
+ // a Turn ends `model-error`. `POST /api/chat` authenticates, so a
1241
+ // one-token completion is what proves the key.
1242
+ const client = this.clientFor(
1243
+ record.settings?.[API_BASE_URL_SETTING] ??
1244
+ connectionApiBaseUrl(existingProjection),
1245
+ );
1246
+ const models = await client.listModels(apiKey);
1247
+ await client.probeInference(apiKey, probeModelId(models));
1248
+ outcome = {
1249
+ ...record,
1250
+ validationCatalog: this.catalog(models),
1251
+ validationStatus: "applied",
1252
+ };
1253
+ } catch (error) {
1254
+ outcome = {
1255
+ ...record,
1256
+ validationFailure: (error instanceof Error && error.message
1257
+ ? error.message
1258
+ : "Ollama Cloud validation failed"
1259
+ ).slice(0, 500),
1260
+ validationStatus: "failed",
1261
+ };
1262
+ }
1263
+ await this.host.storage.transaction(async (storage) => {
1264
+ const storedValue = await storage.get<unknown>(
1265
+ commandKey(record.commandId),
1266
+ );
1267
+ if (storedValue === undefined) {
1268
+ throw new Error("Ollama Connection command is unavailable");
1269
+ }
1270
+ const stored = decodeStoredCommand(storedValue);
1271
+ if (!stored.receipt) {
1272
+ await storage.put(commandKey(record.commandId), outcome);
1273
+ }
1274
+ });
1275
+ }
1276
+ const settleValidation = () =>
1277
+ this.host.credentials.settle({
1278
+ accountId: record.accountId,
1279
+ connectionId: record.connectionId,
1280
+ packageId: PACKAGE_ID,
1281
+ effectId,
1282
+ });
1283
+ let projected: ConnectionView;
1284
+ try {
1285
+ projected = await this.requireConnection(
1286
+ record.accountId,
1287
+ record.connectionId,
1288
+ );
1289
+ } catch (error) {
1290
+ await settleValidation();
1291
+ await this.host.credentials.discardPending(
1292
+ record.connectionId,
1293
+ generation,
1294
+ );
1295
+ const settledValue = await this.host.storage.get<unknown>(
1296
+ commandKey(record.commandId),
1297
+ );
1298
+ const settled =
1299
+ settledValue === undefined
1300
+ ? undefined
1301
+ : decodeStoredCommand(settledValue).receipt;
1302
+ if (settled) return settled;
1303
+ throw error;
1304
+ }
1305
+ if (
1306
+ projected.generation === generation &&
1307
+ (projected.state === "ready" || projected.state === "disabled")
1308
+ ) {
1309
+ await settleValidation();
1310
+ return this.finishRecord(outcome, "applied");
1311
+ }
1312
+ if (outcome.validationStatus === "failed") {
1313
+ await settleValidation();
1314
+ await this.host.credentials.discardPending(
1315
+ record.connectionId,
1316
+ generation,
1317
+ );
1318
+ if (
1319
+ record.operation === "connection/create-api-key" &&
1320
+ projected.state === "authorizing" &&
1321
+ projected.generation === generation
1322
+ ) {
1323
+ await this.host.settings.replaceConnection(
1324
+ record.accountId,
1325
+ record.connectionId,
1326
+ projected.generation,
1327
+ {
1328
+ ...projected,
1329
+ state: "failed",
1330
+ authorization: {
1331
+ schemaVersion: 1,
1332
+ kind: "api-key",
1333
+ credential: {
1334
+ schemaVersion: 1,
1335
+ configured: false,
1336
+ source: "api-key",
1337
+ writable: true,
1338
+ },
1339
+ },
1340
+ failure: outcome.validationFailure,
1341
+ },
1342
+ );
1343
+ }
1344
+ return this.finishRecord(outcome, "failed");
1345
+ }
1346
+ try {
1347
+ await this.host.storage.transaction(
1348
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1349
+ const current = await this.host.settings.getConnection(
1350
+ record.accountId,
1351
+ record.connectionId,
1352
+ storage,
1353
+ );
1354
+ if (!current || current.packageId !== PACKAGE_ID) {
1355
+ throw new Error("Ollama Cloud Connection is unavailable");
1356
+ }
1357
+ if (
1358
+ record.operation === "connection/create-api-key" &&
1359
+ (current.state !== "authorizing" ||
1360
+ current.generation !== generation)
1361
+ ) {
1362
+ throw new Error(`Connection is ${current.state}`);
1363
+ }
1364
+ if (
1365
+ record.operation === "connection/rotate-api-key" &&
1366
+ current.state !== "ready" &&
1367
+ current.state !== "disabled"
1368
+ ) {
1369
+ throw new Error(`Connection is ${current.state}`);
1370
+ }
1371
+ await this.host.credentials.activate(
1372
+ {
1373
+ accountId: record.accountId,
1374
+ connectionId: record.connectionId,
1375
+ packageId: PACKAGE_ID,
1376
+ generation,
1377
+ },
1378
+ storage,
1379
+ );
1380
+ await this.host.settings.replaceConnection(
1381
+ record.accountId,
1382
+ record.connectionId,
1383
+ record.expectedGeneration ?? current.generation,
1384
+ {
1385
+ ...current,
1386
+ state: current.state === "disabled" ? "disabled" : "ready",
1387
+ generation,
1388
+ modelCatalog: outcome.validationCatalog,
1389
+ authorization: {
1390
+ schemaVersion: 1,
1391
+ kind: "api-key",
1392
+ credential: {
1393
+ schemaVersion: 1,
1394
+ configured: true,
1395
+ source: "api-key",
1396
+ writable: true,
1397
+ generation,
1398
+ updatedAt: new Date(this.now()).toISOString(),
1399
+ },
1400
+ },
1401
+ failure: undefined,
1402
+ },
1403
+ storage,
1404
+ );
1405
+ if (
1406
+ record.operation === "connection/create-api-key" &&
1407
+ outcome.validationCatalog
1408
+ ) {
1409
+ const user = await this.host.settings.read(
1410
+ record.accountId,
1411
+ storage,
1412
+ );
1413
+ const selected = selectAutomaticOllamaModelV1(
1414
+ outcome.validationCatalog,
1415
+ );
1416
+ if (selected && user.newBotModelTemplateSource !== "user") {
1417
+ await this.host.settings.executeConfigurationCommand(
1418
+ record.accountId,
1419
+ {
1420
+ schemaVersion: 1,
1421
+ type: "user/set-new-bot-model",
1422
+ commandId: record.commandId,
1423
+ expectedRevision: user.revision,
1424
+ model: {
1425
+ connectionId: record.connectionId,
1426
+ providerModelId: selected.providerModelId,
1427
+ },
1428
+ source: "auto",
1429
+ },
1430
+ storage,
1431
+ );
1432
+ }
1433
+ }
1434
+ },
1435
+ );
1436
+ } catch (error) {
1437
+ await settleValidation();
1438
+ await this.host.credentials.discardPending(
1439
+ record.connectionId,
1440
+ generation,
1441
+ );
1442
+ const settledValue = await this.host.storage.get<unknown>(
1443
+ commandKey(record.commandId),
1444
+ );
1445
+ const settled =
1446
+ settledValue === undefined
1447
+ ? undefined
1448
+ : decodeStoredCommand(settledValue).receipt;
1449
+ if (settled) return settled;
1450
+ if (record.operation === "connection/create-api-key") {
1451
+ const current = await this.requireConnection(
1452
+ record.accountId,
1453
+ record.connectionId,
1454
+ );
1455
+ if (
1456
+ current.state === "authorizing" &&
1457
+ current.generation === generation
1458
+ ) {
1459
+ await this.host.settings.replaceConnection(
1460
+ record.accountId,
1461
+ record.connectionId,
1462
+ current.generation,
1463
+ {
1464
+ ...current,
1465
+ state: "failed",
1466
+ authorization: {
1467
+ schemaVersion: 1,
1468
+ kind: "api-key",
1469
+ credential: {
1470
+ schemaVersion: 1,
1471
+ configured: false,
1472
+ source: "api-key",
1473
+ writable: true,
1474
+ },
1475
+ },
1476
+ failure:
1477
+ error instanceof Error
1478
+ ? error.message
1479
+ : "Ollama Cloud validation failed",
1480
+ },
1481
+ );
1482
+ }
1483
+ }
1484
+ return this.finishRecord(outcome, "failed");
1485
+ }
1486
+ await settleValidation();
1487
+ return this.finishRecord(outcome, "applied");
1488
+ }
1489
+
1490
+ private async updateLabel(
1491
+ record: StoredCommand,
1492
+ ): Promise<ConnectionCommandReceiptV1> {
1493
+ const label = record.label;
1494
+ if (!label) return this.finishRecord(record, "failed");
1495
+ const applied = await this.applySequencedMutation(record, (current) => ({
1496
+ ...current,
1497
+ displayName: label,
1498
+ }));
1499
+ return this.finishRecord(record, applied ? "applied" : "failed");
1500
+ }
1501
+
1502
+ private async refreshCatalog(
1503
+ record: StoredCommand,
1504
+ ): Promise<ConnectionCommandReceiptV1> {
1505
+ const expectedGeneration = record.expectedGeneration;
1506
+ if (!expectedGeneration) return this.finishRecord(record, "failed");
1507
+ if (record.settlementEffectId && record.settlementStatus) {
1508
+ await this.host.credentials.settle({
1509
+ accountId: record.accountId,
1510
+ connectionId: record.connectionId,
1511
+ packageId: PACKAGE_ID,
1512
+ effectId: record.settlementEffectId,
1513
+ });
1514
+ return this.finishRecord(record, record.settlementStatus);
1515
+ }
1516
+ if (record.providerRetryPolicy !== "safe-metadata-read") {
1517
+ return this.finishRecord(record, "failed");
1518
+ }
1519
+ const effectId = `catalog:${record.commandId}`;
1520
+ let lease: CredentialLeaseV1 | undefined;
1521
+ let models: ConnectionModelCatalogV1["models"] | undefined;
1522
+ let failure: unknown;
1523
+ let refreshEndpoint: string | undefined;
1524
+ try {
1525
+ lease = await this.host.storage.transaction(
1526
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1527
+ const authority = await this.requireModelAuthority(
1528
+ {
1529
+ accountId: record.accountId,
1530
+ connectionId: record.connectionId,
1531
+ connectionGeneration: expectedGeneration,
1532
+ },
1533
+ storage,
1534
+ );
1535
+ refreshEndpoint = connectionApiBaseUrl(authority);
1536
+ return this.host.credentials.lease(
1537
+ {
1538
+ accountId: record.accountId,
1539
+ connectionId: record.connectionId,
1540
+ packageId: PACKAGE_ID,
1541
+ effectId,
1542
+ expiresAt: new Date(this.now() + MODEL_LEASE_MS).toISOString(),
1543
+ expectedGeneration,
1544
+ },
1545
+ storage,
1546
+ );
1547
+ },
1548
+ );
1549
+ const apiKey = await this.host.credentials.openLease({
1550
+ accountId: record.accountId,
1551
+ packageId: PACKAGE_ID,
1552
+ lease,
1553
+ });
1554
+ models = await this.clientFor(refreshEndpoint).listModels(apiKey);
1555
+ } catch (error) {
1556
+ failure = error;
1557
+ }
1558
+ const outcome = await this.host.storage.transaction(
1559
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1560
+ let outcomeModels = models;
1561
+ let outcomeFailure = failure;
1562
+ let authorized: ConnectionView | undefined;
1563
+ const mutationSequenceValue = record.mutationSequence
1564
+ ? await storage.get<unknown>(
1565
+ mutationSequenceKey(record.connectionId, record.operation),
1566
+ )
1567
+ : undefined;
1568
+ const mutationSequence =
1569
+ mutationSequenceValue === undefined
1570
+ ? undefined
1571
+ : decodeMutationSequence(mutationSequenceValue);
1572
+ const appliesProjection =
1573
+ !record.mutationSequence ||
1574
+ !mutationSequence ||
1575
+ record.mutationSequence === mutationSequence.next;
1576
+ if (outcomeModels) {
1577
+ try {
1578
+ authorized = await this.requireModelAuthority(
1579
+ {
1580
+ accountId: record.accountId,
1581
+ connectionId: record.connectionId,
1582
+ connectionGeneration: expectedGeneration,
1583
+ },
1584
+ storage,
1585
+ );
1586
+ } catch (error) {
1587
+ outcomeModels = undefined;
1588
+ outcomeFailure = error;
1589
+ }
1590
+ }
1591
+ const status: ConnectionCommandReceiptV1["status"] =
1592
+ appliesProjection && outcomeFailure === undefined
1593
+ ? "applied"
1594
+ : "failed";
1595
+ const pendingSettlement: StoredCommand | undefined = lease
1596
+ ? {
1597
+ ...record,
1598
+ settlementEffectId: effectId,
1599
+ settlementStatus: status,
1600
+ }
1601
+ : undefined;
1602
+ if (appliesProjection && outcomeModels && authorized) {
1603
+ await this.host.settings.replaceConnection(
1604
+ record.accountId,
1605
+ record.connectionId,
1606
+ expectedGeneration,
1607
+ {
1608
+ ...authorized,
1609
+ modelCatalog: this.catalog(outcomeModels),
1610
+ failure: undefined,
1611
+ },
1612
+ storage,
1613
+ );
1614
+ } else if (appliesProjection) {
1615
+ const settings = await this.host.settings.readSnapshot(storage);
1616
+ const current = settings.connections.find(
1617
+ (connection) => connection.connectionId === record.connectionId,
1618
+ );
1619
+ if (
1620
+ settings.packages.some(
1621
+ (pkg) =>
1622
+ pkg.packageId === PACKAGE_ID && pkg.state === "installed",
1623
+ ) &&
1624
+ current?.packageId === PACKAGE_ID &&
1625
+ current.generation === expectedGeneration &&
1626
+ current.state === "ready" &&
1627
+ current.modelCatalog
1628
+ ) {
1629
+ await this.host.settings.replaceConnection(
1630
+ record.accountId,
1631
+ record.connectionId,
1632
+ expectedGeneration,
1633
+ {
1634
+ ...current,
1635
+ modelCatalog: {
1636
+ ...current.modelCatalog,
1637
+ state: "stale",
1638
+ refreshAfter: new Date(
1639
+ this.now() + REFRESH_INTERVAL_MS,
1640
+ ).toISOString(),
1641
+ failure:
1642
+ outcomeFailure instanceof Error
1643
+ ? outcomeFailure.message
1644
+ : "Ollama Cloud catalog refresh failed",
1645
+ },
1646
+ },
1647
+ storage,
1648
+ );
1649
+ }
1650
+ }
1651
+ if (appliesProjection && record.mutationSequence && mutationSequence) {
1652
+ await storage.put(
1653
+ mutationSequenceKey(record.connectionId, record.operation),
1654
+ {
1655
+ schemaVersion: 1,
1656
+ next: mutationSequence.next,
1657
+ applied: Math.max(
1658
+ mutationSequence.applied,
1659
+ record.mutationSequence,
1660
+ ),
1661
+ },
1662
+ );
1663
+ }
1664
+ if (pendingSettlement) {
1665
+ const storedValue = await storage.get<unknown>(
1666
+ commandKey(record.commandId),
1667
+ );
1668
+ if (storedValue === undefined) {
1669
+ throw new Error("Ollama Connection command is unavailable");
1670
+ }
1671
+ const stored = decodeStoredCommand(storedValue);
1672
+ if (!stored.receipt) {
1673
+ await storage.put(commandKey(record.commandId), pendingSettlement);
1674
+ }
1675
+ }
1676
+ return { status, pendingSettlement };
1677
+ },
1678
+ );
1679
+ if (!outcome.pendingSettlement) {
1680
+ return this.finishRecord(record, outcome.status);
1681
+ }
1682
+ await this.host.credentials.settle({
1683
+ accountId: record.accountId,
1684
+ connectionId: record.connectionId,
1685
+ packageId: PACKAGE_ID,
1686
+ effectId,
1687
+ });
1688
+ return this.finishRecord(outcome.pendingSettlement, outcome.status);
1689
+ }
1690
+
1691
+ private async setEnabled(
1692
+ record: StoredCommand,
1693
+ ): Promise<ConnectionCommandReceiptV1> {
1694
+ const applied = await this.applySequencedMutation(record, (current) => {
1695
+ if (current.state !== "ready" && current.state !== "disabled") {
1696
+ return undefined;
1697
+ }
1698
+ return { ...current, state: record.enabled ? "ready" : "disabled" };
1699
+ });
1700
+ return this.finishRecord(record, applied ? "applied" : "failed");
1701
+ }
1702
+
1703
+ private async applySequencedMutation(
1704
+ record: StoredCommand,
1705
+ update: (current: ConnectionView) => ConnectionView | undefined,
1706
+ ): Promise<boolean> {
1707
+ const mutationSequence = record.mutationSequence;
1708
+ if (!mutationSequence) return false;
1709
+ return this.host.storage.transaction(
1710
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1711
+ const sequenceValue = await storage.get<unknown>(
1712
+ mutationSequenceKey(record.connectionId, record.operation),
1713
+ );
1714
+ const sequence =
1715
+ sequenceValue === undefined
1716
+ ? { next: mutationSequence, applied: 0 }
1717
+ : decodeMutationSequence(sequenceValue);
1718
+ if (mutationSequence < sequence.applied) return false;
1719
+ if (mutationSequence === sequence.applied) return true;
1720
+ const current = await this.host.settings.getConnection(
1721
+ record.accountId,
1722
+ record.connectionId,
1723
+ storage,
1724
+ );
1725
+ if (
1726
+ !current ||
1727
+ current.packageId !== PACKAGE_ID ||
1728
+ current.generation !== record.expectedGeneration
1729
+ ) {
1730
+ return false;
1731
+ }
1732
+ const updated = update(current);
1733
+ if (!updated) return false;
1734
+ await this.host.settings.replaceConnection(
1735
+ record.accountId,
1736
+ record.connectionId,
1737
+ current.generation,
1738
+ updated,
1739
+ storage,
1740
+ );
1741
+ await storage.put(
1742
+ mutationSequenceKey(record.connectionId, record.operation),
1743
+ {
1744
+ schemaVersion: 1,
1745
+ next: Math.max(sequence.next, mutationSequence),
1746
+ applied: mutationSequence,
1747
+ },
1748
+ );
1749
+ return true;
1750
+ },
1751
+ );
1752
+ }
1753
+
1754
+ private async cancelPendingCredentialMutations(
1755
+ record: StoredCommand,
1756
+ storage: UserSettingsTransaction & CredentialTransaction,
1757
+ ): Promise<void> {
1758
+ const pendingValue = await storage.get<unknown>(PENDING_KEY);
1759
+ const pending =
1760
+ pendingValue === undefined ? [] : decodePendingCommands(pendingValue);
1761
+ const retained: string[] = [];
1762
+ const completedIds: string[] = [];
1763
+ const completed: Record<string, unknown> = {};
1764
+ for (const commandId of pending) {
1765
+ const value = await storage.get<unknown>(commandKey(commandId));
1766
+ if (value === undefined) continue;
1767
+ const candidate = decodeStoredCommand(value);
1768
+ if (
1769
+ (candidate.operation === "connection/create-api-key" ||
1770
+ candidate.operation === "connection/rotate-api-key") &&
1771
+ candidate.connectionId === record.connectionId &&
1772
+ !candidate.receipt
1773
+ ) {
1774
+ completed[commandKey(commandId)] = compactCompletedCommand({
1775
+ ...candidate,
1776
+ receipt: receipt(candidate, "failed"),
1777
+ completedAt: this.now(),
1778
+ });
1779
+ completedIds.push(commandId);
1780
+ if (candidate.credentialGeneration) {
1781
+ await this.host.credentials.discardPending(
1782
+ candidate.connectionId,
1783
+ candidate.credentialGeneration,
1784
+ storage,
1785
+ );
1786
+ }
1787
+ } else {
1788
+ retained.push(commandId);
1789
+ }
1790
+ }
1791
+ const receiptIndexValue = await storage.get<unknown>(RECEIPT_INDEX_KEY);
1792
+ const receiptIndex =
1793
+ receiptIndexValue === undefined
1794
+ ? []
1795
+ : decodeReceiptIndex(receiptIndexValue);
1796
+ const ordered = [
1797
+ ...receiptIndex.filter((commandId) => !completedIds.includes(commandId)),
1798
+ ...completedIds,
1799
+ ];
1800
+ const tombstonesValue = await storage.get<unknown>(COMMAND_TOMBSTONES_KEY);
1801
+ let tombstones =
1802
+ tombstonesValue === undefined
1803
+ ? []
1804
+ : decodeCommandTombstones(tombstonesValue);
1805
+ for (const commandId of ordered.slice(0, -MAX_MANUAL_RECEIPTS)) {
1806
+ const key = commandKey(commandId);
1807
+ const value = completed[key] ?? (await storage.get<unknown>(key));
1808
+ if (value === undefined) continue;
1809
+ const compacted = compactCompletedCommand(decodeStoredCommand(value));
1810
+ tombstones = [
1811
+ ...tombstones.filter((candidate) => candidate.commandId !== commandId),
1812
+ compacted,
1813
+ ];
1814
+ delete completed[key];
1815
+ await storage.delete(key);
1816
+ }
1817
+ if (tombstones.length > MAX_COMMAND_TOMBSTONES) {
1818
+ throw new Error("Ollama Connection command history capacity reached");
1819
+ }
1820
+ await storage.put({
1821
+ ...completed,
1822
+ [COMMAND_TOMBSTONES_KEY]: tombstones,
1823
+ [PENDING_KEY]: retained,
1824
+ [RECEIPT_INDEX_KEY]: ordered.slice(-MAX_MANUAL_RECEIPTS),
1825
+ });
1826
+ }
1827
+
1828
+ private async disconnect(
1829
+ record: StoredCommand,
1830
+ ): Promise<ConnectionCommandReceiptV1> {
1831
+ const expectedGeneration = record.expectedGeneration;
1832
+ if (!expectedGeneration) return this.finishRecord(record, "failed");
1833
+ const transition = await this.host.storage.transaction(
1834
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1835
+ const current = await this.host.settings.getConnection(
1836
+ record.accountId,
1837
+ record.connectionId,
1838
+ storage,
1839
+ );
1840
+ if (!current || current.packageId !== PACKAGE_ID) return "stale";
1841
+ if (current.generation !== expectedGeneration) return "stale";
1842
+ if (current.state === "revoked") {
1843
+ if (!record.revokeUpstream) return "revoked";
1844
+ await this.host.settings.replaceConnection(
1845
+ record.accountId,
1846
+ record.connectionId,
1847
+ expectedGeneration,
1848
+ {
1849
+ ...current,
1850
+ state: "reconciliation-required",
1851
+ failure:
1852
+ "Ollama Cloud does not expose upstream API-key revocation",
1853
+ },
1854
+ storage,
1855
+ );
1856
+ return "reconciliation-required";
1857
+ }
1858
+ if (current.state === "reconciliation-required") {
1859
+ return "reconciliation-required";
1860
+ }
1861
+ if (
1862
+ Array.isArray(current.safeMetadata.dependentAssignments) &&
1863
+ current.safeMetadata.dependentAssignments.length > 0
1864
+ ) {
1865
+ return "dependent";
1866
+ }
1867
+ await this.cancelPendingCredentialMutations(record, storage);
1868
+ if (current.state !== "revoking") {
1869
+ await this.host.settings.replaceConnection(
1870
+ record.accountId,
1871
+ record.connectionId,
1872
+ expectedGeneration,
1873
+ { ...current, state: "revoking" },
1874
+ storage,
1875
+ );
1876
+ }
1877
+ return "revoking";
1878
+ },
1879
+ );
1880
+ if (transition === "stale" || transition === "dependent") {
1881
+ return this.finishRecord(record, "failed");
1882
+ }
1883
+ if (transition === "revoked") return this.finishRecord(record, "applied");
1884
+ if (transition === "reconciliation-required") {
1885
+ return this.finishRecord(record, "reconciliation-required");
1886
+ }
1887
+
1888
+ await this.host.credentials.disconnect(record.connectionId);
1889
+ const terminalStatus = await this.host.storage.transaction(
1890
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1891
+ const current = await this.host.settings.getConnection(
1892
+ record.accountId,
1893
+ record.connectionId,
1894
+ storage,
1895
+ );
1896
+ if (
1897
+ !current ||
1898
+ current.packageId !== PACKAGE_ID ||
1899
+ current.generation !== expectedGeneration
1900
+ ) {
1901
+ return "failed" as const;
1902
+ }
1903
+ if (current.state === "reconciliation-required") {
1904
+ return "reconciliation-required" as const;
1905
+ }
1906
+ const unsupportedUpstreamRevoke = record.revokeUpstream === true;
1907
+ if (current.state === "revoked" && !unsupportedUpstreamRevoke) {
1908
+ return "applied" as const;
1909
+ }
1910
+ if (current.state !== "revoking" && current.state !== "revoked") {
1911
+ return "failed" as const;
1912
+ }
1913
+ await this.host.settings.replaceConnection(
1914
+ record.accountId,
1915
+ record.connectionId,
1916
+ expectedGeneration,
1917
+ {
1918
+ ...current,
1919
+ state: unsupportedUpstreamRevoke
1920
+ ? "reconciliation-required"
1921
+ : "revoked",
1922
+ authorization: {
1923
+ schemaVersion: 1,
1924
+ kind: "api-key",
1925
+ credential: {
1926
+ schemaVersion: 1,
1927
+ configured: false,
1928
+ source: "api-key",
1929
+ writable: true,
1930
+ },
1931
+ },
1932
+ ...(unsupportedUpstreamRevoke
1933
+ ? {
1934
+ failure:
1935
+ "Ollama Cloud does not expose upstream API-key revocation",
1936
+ }
1937
+ : {}),
1938
+ },
1939
+ storage,
1940
+ );
1941
+ return unsupportedUpstreamRevoke
1942
+ ? ("reconciliation-required" as const)
1943
+ : ("applied" as const);
1944
+ },
1945
+ );
1946
+ return this.finishRecord(record, terminalStatus);
1947
+ }
1948
+
1949
+ private async finishRecord(
1950
+ record: StoredCommand,
1951
+ status: ConnectionCommandReceiptV1["status"],
1952
+ ): Promise<ConnectionCommandReceiptV1> {
1953
+ const proposed = receipt(record, status);
1954
+ const result = await this.host.storage.transaction(
1955
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
1956
+ const storedValue = await storage.get<unknown>(
1957
+ commandKey(record.commandId),
1958
+ );
1959
+ const stored =
1960
+ storedValue === undefined
1961
+ ? undefined
1962
+ : decodeStoredCommand(storedValue);
1963
+ if (stored?.receipt) return stored.receipt;
1964
+ const pendingValue = await storage.get<unknown>(PENDING_KEY);
1965
+ const pending = (
1966
+ pendingValue === undefined ? [] : decodePendingCommands(pendingValue)
1967
+ ).filter((id) => id !== record.commandId);
1968
+ if (record.automaticRefresh) {
1969
+ await storage.delete(commandKey(record.commandId));
1970
+ await storage.put({
1971
+ [`${AUTOMATIC_REFRESH_RECEIPT_PREFIX}${record.connectionId}`]: {
1972
+ schemaVersion: 1,
1973
+ commandId: record.commandId,
1974
+ connectionId: record.connectionId,
1975
+ status: proposed.status,
1976
+ completedAt: new Date(this.now()).toISOString(),
1977
+ },
1978
+ [PENDING_KEY]: pending,
1979
+ });
1980
+ } else {
1981
+ const receiptIndexValue =
1982
+ await storage.get<unknown>(RECEIPT_INDEX_KEY);
1983
+ const receiptIndex =
1984
+ receiptIndexValue === undefined
1985
+ ? []
1986
+ : decodeReceiptIndex(receiptIndexValue);
1987
+ const ordered = [
1988
+ ...receiptIndex.filter(
1989
+ (commandId) => commandId !== record.commandId,
1990
+ ),
1991
+ record.commandId,
1992
+ ];
1993
+ const retained = ordered.slice(-MAX_MANUAL_RECEIPTS);
1994
+ const {
1995
+ validationCatalog: _validationCatalog,
1996
+ validationFailure: _validationFailure,
1997
+ validationStatus: _validationStatus,
1998
+ ...durableRecord
1999
+ } = stored ?? record;
2000
+ const completed = {
2001
+ ...durableRecord,
2002
+ receipt: proposed,
2003
+ completedAt: this.now(),
2004
+ };
2005
+ const tombstonesValue = await storage.get<unknown>(
2006
+ COMMAND_TOMBSTONES_KEY,
2007
+ );
2008
+ let tombstones =
2009
+ tombstonesValue === undefined
2010
+ ? []
2011
+ : decodeCommandTombstones(tombstonesValue);
2012
+ for (const commandId of ordered.slice(0, -MAX_MANUAL_RECEIPTS)) {
2013
+ const value = await storage.get<unknown>(commandKey(commandId));
2014
+ if (value === undefined) continue;
2015
+ const compacted = compactCompletedCommand(
2016
+ decodeStoredCommand(value),
2017
+ );
2018
+ tombstones = [
2019
+ ...tombstones.filter(
2020
+ (candidate) => candidate.commandId !== commandId,
2021
+ ),
2022
+ compacted,
2023
+ ];
2024
+ await storage.delete(commandKey(commandId));
2025
+ }
2026
+ if (tombstones.length > MAX_COMMAND_TOMBSTONES) {
2027
+ throw new Error(
2028
+ "Ollama Connection command history capacity reached",
2029
+ );
2030
+ }
2031
+ await storage.put({
2032
+ [commandKey(record.commandId)]: completed,
2033
+ [COMMAND_TOMBSTONES_KEY]: tombstones,
2034
+ [RECEIPT_INDEX_KEY]: retained,
2035
+ [PENDING_KEY]: pending,
2036
+ });
2037
+ }
2038
+ return proposed;
2039
+ },
2040
+ );
2041
+ await this.scheduleNextAlarm(record.accountId);
2042
+ return result;
2043
+ }
2044
+
2045
+ private async scheduleNextAlarm(accountId: string): Promise<void> {
2046
+ const pendingValue = await this.host.storage.get<unknown>(PENDING_KEY);
2047
+ const pending =
2048
+ pendingValue === undefined ? [] : decodePendingCommands(pendingValue);
2049
+ const settings = await this.host.settings.read(accountId);
2050
+ const packageInstalled = settings.packages.some(
2051
+ (pkg) => pkg.packageId === PACKAGE_ID && pkg.state === "installed",
2052
+ );
2053
+ const catalogDeadlines = packageInstalled
2054
+ ? settings.connections.flatMap((connection) => {
2055
+ const refreshAfter = connection.modelCatalog?.refreshAfter;
2056
+ if (
2057
+ connection.packageId !== PACKAGE_ID ||
2058
+ connection.state !== "ready" ||
2059
+ !refreshAfter
2060
+ ) {
2061
+ return [];
2062
+ }
2063
+ const deadline = Date.parse(refreshAfter);
2064
+ return Number.isFinite(deadline)
2065
+ ? [Math.max(deadline, this.now() + RECOVERY_DELAY_MS)]
2066
+ : [];
2067
+ })
2068
+ : [];
2069
+ const credentialExpiry = await this.host.credentials.nextLeaseExpiry();
2070
+ const deadlines = [
2071
+ ...(pending.length > 0 ? [this.now() + RECOVERY_DELAY_MS] : []),
2072
+ ...catalogDeadlines,
2073
+ ...(credentialExpiry === undefined ? [] : [credentialExpiry]),
2074
+ ];
2075
+ if (deadlines.length > 0) {
2076
+ await this.host.storage.setAlarm(Math.min(...deadlines));
2077
+ }
2078
+ }
2079
+
2080
+ async leaseModelCredential(input: {
2081
+ accountId: string;
2082
+ connectionId: string;
2083
+ providerModelId: string;
2084
+ effectId: string;
2085
+ connectionGeneration: string;
2086
+ }): Promise<CredentialLeaseV1> {
2087
+ await this.host.credentials.expireLeases();
2088
+ const replay = await this.host.credentials.replayLease({
2089
+ accountId: input.accountId,
2090
+ connectionId: input.connectionId,
2091
+ packageId: PACKAGE_ID,
2092
+ effectId: input.effectId,
2093
+ });
2094
+ if (replay) return replay;
2095
+
2096
+ const admittedGeneration = input.connectionGeneration;
2097
+ const connection = await this.host.storage.transaction(
2098
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
2099
+ return this.requireModelAuthority(input, storage);
2100
+ },
2101
+ );
2102
+ const known = connection.modelCatalog?.models.some(
2103
+ (model) => model.providerModelId === input.providerModelId,
2104
+ );
2105
+ let resolvedModel: ConnectionModelCatalogV1["models"][number] | undefined;
2106
+ const resolutionKey = modelResolutionKey(input.effectId);
2107
+ if (!known) {
2108
+ const discoveryEffectId = `resolve:${input.effectId}`;
2109
+ const resolution = await this.host.storage.transaction(
2110
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
2111
+ await this.requireModelAuthority(input, storage);
2112
+ const storedValue = await storage.get<unknown>(resolutionKey);
2113
+ const stored =
2114
+ storedValue === undefined
2115
+ ? undefined
2116
+ : decodeStoredModelResolution(storedValue);
2117
+ if (
2118
+ stored &&
2119
+ (stored.effectId !== input.effectId ||
2120
+ stored.accountId !== input.accountId ||
2121
+ stored.connectionId !== input.connectionId ||
2122
+ stored.connectionGeneration !== admittedGeneration ||
2123
+ stored.providerModelId !== input.providerModelId)
2124
+ ) {
2125
+ throw new Error("Stored Ollama model resolution authority changed");
2126
+ }
2127
+ if (stored?.status === "applied" || stored?.status === "failed") {
2128
+ return { journal: stored };
2129
+ }
2130
+ const lease = await this.host.credentials.lease(
2131
+ {
2132
+ accountId: input.accountId,
2133
+ connectionId: input.connectionId,
2134
+ packageId: PACKAGE_ID,
2135
+ effectId: discoveryEffectId,
2136
+ expiresAt: new Date(this.now() + MODEL_LEASE_MS).toISOString(),
2137
+ expectedGeneration: admittedGeneration,
2138
+ },
2139
+ storage,
2140
+ );
2141
+ const journal: StoredModelResolution = stored ?? {
2142
+ schemaVersion: 1,
2143
+ effectId: input.effectId,
2144
+ accountId: input.accountId,
2145
+ connectionId: input.connectionId,
2146
+ connectionGeneration: admittedGeneration,
2147
+ providerModelId: input.providerModelId,
2148
+ retryPolicy: "safe-metadata-read",
2149
+ status: "pending",
2150
+ };
2151
+ if (!stored) await storage.put(resolutionKey, journal);
2152
+ return { journal, lease };
2153
+ },
2154
+ );
2155
+ let journal = resolution.journal;
2156
+ if (journal.status === "pending") {
2157
+ if (!resolution.lease) {
2158
+ throw new Error("Ollama model resolution lease is unavailable");
2159
+ }
2160
+ let outcome: StoredModelResolution;
2161
+ try {
2162
+ const apiKey = await this.host.credentials.openLease({
2163
+ accountId: input.accountId,
2164
+ packageId: PACKAGE_ID,
2165
+ lease: resolution.lease,
2166
+ });
2167
+ const model = await this.clientFor(
2168
+ connectionApiBaseUrl(connection),
2169
+ ).resolveModel(apiKey, input.providerModelId);
2170
+ outcome = { ...journal, status: "applied", model };
2171
+ } catch (error) {
2172
+ outcome = {
2173
+ ...journal,
2174
+ status: "failed",
2175
+ failure: (error instanceof Error && error.message
2176
+ ? error.message
2177
+ : "Ollama Cloud model resolution failed"
2178
+ ).slice(0, 500),
2179
+ };
2180
+ }
2181
+ journal = await this.host.storage.transaction(async (storage) => {
2182
+ const storedValue = await storage.get<unknown>(resolutionKey);
2183
+ if (storedValue === undefined) {
2184
+ throw new Error("Ollama model resolution journal is unavailable");
2185
+ }
2186
+ const stored = decodeStoredModelResolution(storedValue);
2187
+ if (stored.status === "pending") {
2188
+ await storage.put(resolutionKey, outcome);
2189
+ return outcome;
2190
+ }
2191
+ return stored;
2192
+ });
2193
+ }
2194
+ await this.host.credentials.settle({
2195
+ accountId: input.accountId,
2196
+ connectionId: input.connectionId,
2197
+ packageId: PACKAGE_ID,
2198
+ effectId: discoveryEffectId,
2199
+ });
2200
+ if (journal.status === "failed") {
2201
+ throw new Error(journal.failure);
2202
+ }
2203
+ resolvedModel = journal.model;
2204
+ }
2205
+
2206
+ return this.host.storage.transaction(
2207
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
2208
+ const current = await this.requireModelAuthority(input, storage);
2209
+ if (
2210
+ resolvedModel &&
2211
+ !current.modelCatalog?.models.some(
2212
+ (model) => model.providerModelId === input.providerModelId,
2213
+ )
2214
+ ) {
2215
+ const catalog = current.modelCatalog ?? this.catalog([]);
2216
+ await this.host.settings.replaceConnection(
2217
+ input.accountId,
2218
+ input.connectionId,
2219
+ admittedGeneration,
2220
+ {
2221
+ ...current,
2222
+ modelCatalog: {
2223
+ ...catalog,
2224
+ generation: this.randomId(),
2225
+ models: retainResolvedModel(catalog, resolvedModel),
2226
+ },
2227
+ },
2228
+ storage,
2229
+ );
2230
+ }
2231
+ const lease = await this.host.credentials.lease(
2232
+ {
2233
+ accountId: input.accountId,
2234
+ connectionId: input.connectionId,
2235
+ packageId: PACKAGE_ID,
2236
+ effectId: input.effectId,
2237
+ expiresAt: new Date(this.now() + MODEL_LEASE_MS).toISOString(),
2238
+ expectedGeneration: admittedGeneration,
2239
+ },
2240
+ storage,
2241
+ );
2242
+ if (!known) await storage.delete(resolutionKey);
2243
+ return lease;
2244
+ },
2245
+ );
2246
+ }
2247
+
2248
+ /**
2249
+ * Lease this Connection's key for a *tool* effect — today the
2250
+ * `ollama-cloud-web-search` Capability. It is the model lease minus the
2251
+ * model: `/api/web_search` resolves nothing and bills no inference, so there
2252
+ * is no catalog to reconcile and no discovery lease to settle.
2253
+ *
2254
+ * The authority check is the same one the model path runs, and the lease is
2255
+ * keyed by the tool call's durable `effectId`, so a Turn resumed after
2256
+ * eviction replays the same lease rather than minting a second one.
2257
+ */
2258
+ async leaseToolCredential(input: {
2259
+ accountId: string;
2260
+ connectionId: string;
2261
+ effectId: string;
2262
+ connectionGeneration: string;
2263
+ }): Promise<CredentialLeaseV1> {
2264
+ await this.host.credentials.expireLeases();
2265
+ const replay = await this.host.credentials.replayLease({
2266
+ accountId: input.accountId,
2267
+ connectionId: input.connectionId,
2268
+ packageId: PACKAGE_ID,
2269
+ effectId: input.effectId,
2270
+ });
2271
+ if (replay) return replay;
2272
+ return this.host.storage.transaction(
2273
+ async (storage: UserSettingsTransaction & CredentialTransaction) => {
2274
+ await this.requireModelAuthority(input, storage);
2275
+ return this.host.credentials.lease(
2276
+ {
2277
+ accountId: input.accountId,
2278
+ connectionId: input.connectionId,
2279
+ packageId: PACKAGE_ID,
2280
+ effectId: input.effectId,
2281
+ expiresAt: new Date(this.now() + MODEL_LEASE_MS).toISOString(),
2282
+ expectedGeneration: input.connectionGeneration,
2283
+ },
2284
+ storage,
2285
+ );
2286
+ },
2287
+ );
2288
+ }
2289
+
2290
+ private async requireModelAuthority(
2291
+ input: {
2292
+ accountId: string;
2293
+ connectionId: string;
2294
+ connectionGeneration: string;
2295
+ },
2296
+ storage: UserSettingsTransaction,
2297
+ ): Promise<ConnectionView> {
2298
+ const settings = await this.host.settings.readSnapshot(storage);
2299
+ if (
2300
+ !settings.packages.some(
2301
+ (pkg) => pkg.packageId === PACKAGE_ID && pkg.state === "installed",
2302
+ )
2303
+ ) {
2304
+ throw new Error("Ollama Cloud Package is not installed and enabled");
2305
+ }
2306
+ const connection = settings.connections.find(
2307
+ (candidate) => candidate.connectionId === input.connectionId,
2308
+ );
2309
+ if (
2310
+ !connection ||
2311
+ connection.packageId !== PACKAGE_ID ||
2312
+ connection.state !== "ready" ||
2313
+ connection.generation !== input.connectionGeneration
2314
+ ) {
2315
+ throw new Error("Connection changed before model authorization");
2316
+ }
2317
+ return connection;
2318
+ }
2319
+
2320
+ /**
2321
+ * Settle a tool effect's lease. Unlike the model path there is no `resolve:`
2322
+ * companion lease and no stored model resolution, because a tool effect
2323
+ * resolves no model.
2324
+ */
2325
+ async settleToolCredential(input: {
2326
+ accountId: string;
2327
+ connectionId: string;
2328
+ effectId: string;
2329
+ }): Promise<void> {
2330
+ await this.host.credentials.settle({ ...input, packageId: PACKAGE_ID });
2331
+ }
2332
+
2333
+ async settleModelCredential(input: {
2334
+ accountId: string;
2335
+ connectionId: string;
2336
+ effectId: string;
2337
+ }): Promise<void> {
2338
+ for (const effectId of [input.effectId, `resolve:${input.effectId}`]) {
2339
+ await this.host.credentials.settle({
2340
+ ...input,
2341
+ packageId: PACKAGE_ID,
2342
+ effectId,
2343
+ });
2344
+ }
2345
+ await this.host.storage.delete(modelResolutionKey(input.effectId));
2346
+ }
2347
+
2348
+ async alarm(): Promise<void> {
2349
+ const pendingValue = await this.host.storage.get<unknown>(PENDING_KEY);
2350
+ const pending =
2351
+ pendingValue === undefined ? [] : decodePendingCommands(pendingValue);
2352
+ for (const commandId of pending.slice(
2353
+ 0,
2354
+ MAX_PENDING_RECOVERIES_PER_ALARM,
2355
+ )) {
2356
+ const recordValue = await this.host.storage.get<unknown>(
2357
+ commandKey(commandId),
2358
+ );
2359
+ if (recordValue === undefined) continue;
2360
+ const record = decodeStoredCommand(recordValue);
2361
+ if (!record.receipt) await this.resumeOnce(record);
2362
+ }
2363
+ const accountValue = await this.host.storage.get<unknown>(ACCOUNT_KEY);
2364
+ if (accountValue === undefined) return;
2365
+ const accountId = decodeStoredAccount(accountValue);
2366
+ const settings = await this.host.settings.read(accountId);
2367
+ const packageInstalled = settings.packages.some(
2368
+ (pkg) => pkg.packageId === PACKAGE_ID && pkg.state === "installed",
2369
+ );
2370
+ if (!packageInstalled) {
2371
+ await this.scheduleNextAlarm(accountId);
2372
+ return;
2373
+ }
2374
+ const dueConnections = settings.connections
2375
+ .filter((connection) => {
2376
+ const refreshAfter = connection.modelCatalog?.refreshAfter;
2377
+ return (
2378
+ connection.packageId === PACKAGE_ID &&
2379
+ connection.state === "ready" &&
2380
+ refreshAfter !== undefined &&
2381
+ Date.parse(refreshAfter) <= this.now()
2382
+ );
2383
+ })
2384
+ .slice(0, MAX_CATALOG_REFRESHES_PER_ALARM);
2385
+ for (const connection of dueConnections) {
2386
+ const refreshAfter = connection.modelCatalog?.refreshAfter;
2387
+ if (!refreshAfter) continue;
2388
+ await this.executeCommand(
2389
+ accountId,
2390
+ decodeConnectionCommandV1({
2391
+ schemaVersion: 1,
2392
+ type: "connection/refresh-models",
2393
+ commandId: `refresh-${connection.connectionId}-${Date.parse(refreshAfter)}`,
2394
+ connectionId: connection.connectionId,
2395
+ }),
2396
+ true,
2397
+ );
2398
+ }
2399
+ await this.scheduleNextAlarm(accountId);
2400
+ }
2401
+
2402
+ /**
2403
+ * The provider client for one Connection's endpoint.
2404
+ *
2405
+ * A host-supplied client wins, so a test or an embedding host can serve every
2406
+ * Connection from one stub; otherwise the host's factory, or the Package
2407
+ * default pointing at https://ollama.com, builds one per endpoint.
2408
+ */
2409
+ private clientFor(apiBaseUrl?: string): OllamaCloudClient {
2410
+ if (this.host.client) return this.host.client;
2411
+ const config: OllamaCloudClientConfig =
2412
+ apiBaseUrl === undefined ? {} : { apiBaseUrl };
2413
+ return this.host.createClient
2414
+ ? this.host.createClient(config)
2415
+ : new OllamaCloudClient(config);
2416
+ }
2417
+
2418
+ private async requireConnection(
2419
+ accountId: string,
2420
+ connectionId: string,
2421
+ ): Promise<ConnectionView> {
2422
+ const connection = await this.host.settings.getConnection(
2423
+ accountId,
2424
+ connectionId,
2425
+ );
2426
+ if (!connection || connection.packageId !== PACKAGE_ID) {
2427
+ throw new Error("Ollama Cloud Connection is unavailable");
2428
+ }
2429
+ return connection;
2430
+ }
2431
+ }
2432
+
2433
+ export function createOllamaCloudUserBackendContribution(
2434
+ host: OllamaUserBackendHost,
2435
+ ): OllamaCloudUserBackendContribution {
2436
+ return new OllamaCloudUserBackendContribution(host);
2437
+ }
2438
+
2439
+ export function createOllamaCloudUserBackendPlugin(
2440
+ host: OllamaUserBackendHost,
2441
+ lifecycle: { mount(value: OllamaCloudUserBackendContribution): () => void },
2442
+ ): Plugin {
2443
+ return () => lifecycle.mount(createOllamaCloudUserBackendContribution(host));
2444
+ }