@frockbot/plugin-shell 0.1.4 → 0.2.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/frockbot.json +4 -0
- package/package.json +30 -28
- package/src/agent.ts +10 -13
- package/src/backend-authoring.test.ts +356 -2
- package/src/backend-authoring.ts +585 -20
- package/src/backend-composition-input.test.ts +65 -0
- package/src/backend-composition-input.ts +58 -0
- package/src/backend-composition.ts +19 -1
- package/src/backend-configuration.test.ts +166 -0
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-isolate.test.ts +198 -203
- package/src/backend-isolate.ts +96 -231
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +947 -206
- package/src/client/FrockBotApp.vue +38 -0
- package/src/client/PackageIframeHost.vue +218 -0
- package/src/client/PackageIframeSettings.vue +52 -0
- package/src/client/SendPayloadView.vue +0 -60
- package/src/client/index.test.ts +119 -0
- package/src/client/index.ts +78 -1
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/composition-views.ts +54 -0
- package/src/shared.ts +10 -0
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
// Bot Durable Object authority for Catalog tools. The external User settings
|
|
3
|
+
// mutation is fenced by a Bot-owned intent and an idempotent command id; the
|
|
4
|
+
// pending Composition generation is then appended and pinned for the next Turn.
|
|
5
|
+
import {
|
|
6
|
+
MAX_CATALOG_DOCUMENT_BYTES_V1,
|
|
7
|
+
assertCatalogEntryMatchesIndexV1,
|
|
8
|
+
assertCatalogPackageBundleV1,
|
|
9
|
+
catalogContentHashV1,
|
|
10
|
+
catalogEntryKeyV1,
|
|
11
|
+
catalogIndexKeyV1,
|
|
12
|
+
catalogPackageArtifactKeyV1,
|
|
13
|
+
catalogPackageSourceKeyV1,
|
|
14
|
+
catalogPackageUiArtifactKeyV1,
|
|
15
|
+
decodeCatalogIndexDocumentV1,
|
|
16
|
+
parseCatalogEntryDocumentV1,
|
|
17
|
+
type CatalogEntryV1,
|
|
18
|
+
type CatalogIndexV1,
|
|
19
|
+
} from "@frockbot/catalog-core";
|
|
20
|
+
import {
|
|
21
|
+
type OperationReceiptV1,
|
|
22
|
+
type UserConfigurationCommandV1,
|
|
23
|
+
type UserSettingsViewV1,
|
|
24
|
+
} from "@frockbot/configuration-core";
|
|
25
|
+
import { isClientIframeContribution } from "@frockbot/kernel-composition";
|
|
26
|
+
import { canonicalJson } from "@frockbot/kernel-composition/compiler";
|
|
27
|
+
import {
|
|
28
|
+
compositionArtifactSetHashV1,
|
|
29
|
+
compositionGenerationIdV1,
|
|
30
|
+
decodeCompositionGenerationV1,
|
|
31
|
+
type CompositionGenerationV1,
|
|
32
|
+
type CompositionMemberV1,
|
|
33
|
+
} from "@frockbot/kernel-composition/generation";
|
|
34
|
+
import {
|
|
35
|
+
authorshipFailureKey,
|
|
36
|
+
authorshipUndoOutcomeKey,
|
|
37
|
+
authorshipManifestKey,
|
|
38
|
+
type AuthoringFailureRecordV1,
|
|
39
|
+
type AuthoredManifestRecordV1,
|
|
40
|
+
type PackageUndoRecordV1,
|
|
41
|
+
} from "@frockbot/plugin-authoring/records";
|
|
42
|
+
import type { PackageUndoRequestV1 } from "@frockbot/plugin-authoring/agent";
|
|
43
|
+
import type { PackageUndoOutcomeV1 } from "@frockbot/plugin-authoring/shared";
|
|
44
|
+
import type {
|
|
45
|
+
PackageCatalogChangeInputV1,
|
|
46
|
+
PackageCatalogChangeRequestV1,
|
|
47
|
+
PackageCatalogHost,
|
|
48
|
+
} from "@frockbot/plugin-package-catalog/agent";
|
|
49
|
+
import {
|
|
50
|
+
PACKAGE_CATALOG_RESULT_MAX_V1,
|
|
51
|
+
packageCatalogEffectIdV1,
|
|
52
|
+
type PackageCatalogChangeOutcomeV1,
|
|
53
|
+
type PackageCatalogInspectOutcomeV1,
|
|
54
|
+
} from "@frockbot/plugin-package-catalog/shared";
|
|
55
|
+
import {
|
|
56
|
+
packageCatalogChangeIntentKey,
|
|
57
|
+
packageCatalogChangeOutcomeKey,
|
|
58
|
+
packageCatalogUndoIntentKey,
|
|
59
|
+
type PackageCatalogChangeIntentV1,
|
|
60
|
+
type PackageCatalogChangeOutcomeRecordV1,
|
|
61
|
+
type PackageCatalogUndoIntentV1,
|
|
62
|
+
} from "@frockbot/plugin-package-catalog/records";
|
|
63
|
+
|
|
64
|
+
export interface PackageCatalogStorage {
|
|
65
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
66
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PackageCatalogCompositionStore {
|
|
70
|
+
current(): Promise<CompositionGenerationV1>;
|
|
71
|
+
lastKnownGood(): Promise<CompositionGenerationV1>;
|
|
72
|
+
read(generationId: string): Promise<CompositionGenerationV1 | undefined>;
|
|
73
|
+
propose(
|
|
74
|
+
generation: CompositionGenerationV1,
|
|
75
|
+
options?: { pin?: boolean },
|
|
76
|
+
): Promise<void>;
|
|
77
|
+
revert(
|
|
78
|
+
toGenerationId: string,
|
|
79
|
+
origin: {
|
|
80
|
+
kind: "revert";
|
|
81
|
+
revertsTo: string;
|
|
82
|
+
botId: string;
|
|
83
|
+
runId: string;
|
|
84
|
+
turnId: string;
|
|
85
|
+
},
|
|
86
|
+
options?: { createdAt?: string },
|
|
87
|
+
): Promise<CompositionGenerationV1>;
|
|
88
|
+
list(query: {
|
|
89
|
+
limit: number;
|
|
90
|
+
cursor?: string;
|
|
91
|
+
}): Promise<{ generations: CompositionGenerationV1[]; cursor?: string }>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface PackageCatalogUserAuthority {
|
|
95
|
+
read(): Promise<UserSettingsViewV1>;
|
|
96
|
+
execute(command: UserConfigurationCommandV1): Promise<OperationReceiptV1>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface BotPackageCatalogReader {
|
|
100
|
+
readIndex(generation: string, expectedHash: string): Promise<CatalogIndexV1>;
|
|
101
|
+
readEntry(input: {
|
|
102
|
+
generation: string;
|
|
103
|
+
index: CatalogIndexV1;
|
|
104
|
+
catalogId: string;
|
|
105
|
+
}): Promise<CatalogEntryV1 | undefined>;
|
|
106
|
+
readSource(sourceHash: string): Promise<string | undefined>;
|
|
107
|
+
headArtifact(contentHash: string): Promise<{ size: number } | undefined>;
|
|
108
|
+
headUiArtifact(contentHash: string): Promise<{ size: number } | undefined>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PackageCatalogHostOptions {
|
|
112
|
+
storage: PackageCatalogStorage;
|
|
113
|
+
composition: PackageCatalogCompositionStore;
|
|
114
|
+
catalog: BotPackageCatalogReader;
|
|
115
|
+
user: PackageCatalogUserAuthority;
|
|
116
|
+
userId: string;
|
|
117
|
+
botId: string;
|
|
118
|
+
runId: string;
|
|
119
|
+
turnId: string;
|
|
120
|
+
now?(): Date;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CatalogAwarePackageCatalogHost extends PackageCatalogHost {
|
|
124
|
+
/** `undefined` delegates a non-Catalog undo to the authoring host. */
|
|
125
|
+
undoCatalogChange(
|
|
126
|
+
request: PackageUndoRequestV1,
|
|
127
|
+
): Promise<PackageUndoOutcomeV1 | undefined>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function summary(
|
|
131
|
+
action: "install" | "update" | "remove",
|
|
132
|
+
name: string,
|
|
133
|
+
): string {
|
|
134
|
+
const verb =
|
|
135
|
+
action === "install"
|
|
136
|
+
? "Added"
|
|
137
|
+
: action === "update"
|
|
138
|
+
? "Updated"
|
|
139
|
+
: "Removed";
|
|
140
|
+
return `${verb} ${name}`.slice(0, 160);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function requiredCatalogPin(user: UserSettingsViewV1): {
|
|
144
|
+
generation: string;
|
|
145
|
+
indexHash: string;
|
|
146
|
+
} {
|
|
147
|
+
if (!user.catalogGeneration || !user.catalogIndexHash) {
|
|
148
|
+
throw new Error("this User has no pinned Package Catalog generation");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
generation: user.catalogGeneration,
|
|
152
|
+
indexHash: user.catalogIndexHash,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function connectionState(
|
|
157
|
+
entry: CatalogEntryV1,
|
|
158
|
+
user: UserSettingsViewV1,
|
|
159
|
+
): PackageCatalogInspectOutcomeV1["connectionTypes"] {
|
|
160
|
+
return (entry.bundle?.manifest.configuration?.connectionTypes ?? []).map(
|
|
161
|
+
(definition) => ({
|
|
162
|
+
id: definition.id,
|
|
163
|
+
displayName: definition.displayName,
|
|
164
|
+
connected: user.connections.some(
|
|
165
|
+
(connection) =>
|
|
166
|
+
connection.packageId === entry.packageId &&
|
|
167
|
+
connection.connectionTypeId === definition.id &&
|
|
168
|
+
connection.state === "ready",
|
|
169
|
+
),
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function recordedOutcome(
|
|
175
|
+
record: PackageCatalogChangeOutcomeRecordV1,
|
|
176
|
+
): PackageCatalogChangeOutcomeV1 {
|
|
177
|
+
return record.status === "refused"
|
|
178
|
+
? {
|
|
179
|
+
status: "refused",
|
|
180
|
+
action: record.action,
|
|
181
|
+
effectId: record.effectId,
|
|
182
|
+
reason: record.reason,
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
status: "recorded",
|
|
186
|
+
action: record.action,
|
|
187
|
+
effectId: record.effectId,
|
|
188
|
+
packageId: record.packageId,
|
|
189
|
+
displayName: record.displayName,
|
|
190
|
+
...(record.version ? { version: record.version } : {}),
|
|
191
|
+
...(record.contentHash ? { contentHash: record.contentHash } : {}),
|
|
192
|
+
generationId: record.generationId,
|
|
193
|
+
missingConnectionTypes: record.missingConnectionTypes,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** One admitted Turn's authority-bearing Catalog host. */
|
|
198
|
+
export function createPackageCatalogHost(
|
|
199
|
+
options: PackageCatalogHostOptions,
|
|
200
|
+
): CatalogAwarePackageCatalogHost {
|
|
201
|
+
const now = options.now ?? (() => new Date());
|
|
202
|
+
|
|
203
|
+
async function pinnedEntry(catalogId: string): Promise<{
|
|
204
|
+
user: UserSettingsViewV1;
|
|
205
|
+
entry: CatalogEntryV1;
|
|
206
|
+
generation: string;
|
|
207
|
+
}> {
|
|
208
|
+
const user = await options.user.read();
|
|
209
|
+
const pin = requiredCatalogPin(user);
|
|
210
|
+
const index = await options.catalog.readIndex(
|
|
211
|
+
pin.generation,
|
|
212
|
+
pin.indexHash,
|
|
213
|
+
);
|
|
214
|
+
const entry = await options.catalog.readEntry({
|
|
215
|
+
generation: pin.generation,
|
|
216
|
+
index,
|
|
217
|
+
catalogId,
|
|
218
|
+
});
|
|
219
|
+
if (!entry) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`Catalog entry "${catalogId}" is not in pinned generation "${pin.generation}"`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return { user, entry, generation: pin.generation };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function refuse(
|
|
228
|
+
action: PackageCatalogChangeInputV1["action"],
|
|
229
|
+
effectId: string,
|
|
230
|
+
reason: string,
|
|
231
|
+
): Promise<PackageCatalogChangeOutcomeV1> {
|
|
232
|
+
const record: PackageCatalogChangeOutcomeRecordV1 = {
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
status: "refused",
|
|
235
|
+
effectId,
|
|
236
|
+
action,
|
|
237
|
+
reason,
|
|
238
|
+
recordedAt: now().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
await options.storage.put({
|
|
241
|
+
[packageCatalogChangeOutcomeKey(effectId)]: record,
|
|
242
|
+
});
|
|
243
|
+
return recordedOutcome(record);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function verifyArtifact(entry: CatalogEntryV1): Promise<void> {
|
|
247
|
+
if (!entry.bundle) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Catalog entry "${entry.catalogId}" names reviewed first-party code and has no Bot-isolate bundle`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const artifact = await options.catalog.headArtifact(
|
|
253
|
+
entry.bundle.contentHash,
|
|
254
|
+
);
|
|
255
|
+
if (!artifact || artifact.size !== entry.bundle.size) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Catalog bundle "${entry.bundle.contentHash}" is absent or has the wrong size`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const client = entry.bundle.manifest.contributions.client;
|
|
261
|
+
if (client && isClientIframeContribution(client)) {
|
|
262
|
+
const uiArtifact = await options.catalog.headUiArtifact(
|
|
263
|
+
client.artifact.contentHash,
|
|
264
|
+
);
|
|
265
|
+
if (!uiArtifact || uiArtifact.size !== client.artifact.size) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Catalog iframe artifact "${client.artifact.contentHash}" is absent or has the wrong size`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function undoRefused(
|
|
274
|
+
request: PackageUndoRequestV1,
|
|
275
|
+
reason: string,
|
|
276
|
+
): Promise<PackageUndoOutcomeV1> {
|
|
277
|
+
const recordedAt = now().toISOString();
|
|
278
|
+
const failureId = `authoring-failure-${crypto.randomUUID()}`;
|
|
279
|
+
const failure: AuthoringFailureRecordV1 = {
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
failureId,
|
|
282
|
+
effectId: request.effectId,
|
|
283
|
+
botId: options.botId,
|
|
284
|
+
packageId: "composition",
|
|
285
|
+
runId: options.runId,
|
|
286
|
+
phase: "compose",
|
|
287
|
+
reason,
|
|
288
|
+
diagnostics: [],
|
|
289
|
+
recordedAt,
|
|
290
|
+
};
|
|
291
|
+
const outcome: PackageUndoRecordV1 = {
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
status: "refused",
|
|
294
|
+
effectId: request.effectId,
|
|
295
|
+
failureId,
|
|
296
|
+
reason,
|
|
297
|
+
recordedAt,
|
|
298
|
+
};
|
|
299
|
+
await options.storage.put({
|
|
300
|
+
[authorshipFailureKey(failureId)]: failure,
|
|
301
|
+
[authorshipUndoOutcomeKey(request.effectId)]: outcome,
|
|
302
|
+
});
|
|
303
|
+
return { status: "refused", reason, failureId };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
effectIdFor: (change) =>
|
|
308
|
+
packageCatalogEffectIdV1({
|
|
309
|
+
runId: options.runId,
|
|
310
|
+
action: change.action,
|
|
311
|
+
identifier:
|
|
312
|
+
change.action === "remove"
|
|
313
|
+
? change.input.packageId
|
|
314
|
+
: change.input.catalogId,
|
|
315
|
+
...(change.action === "remove"
|
|
316
|
+
? {}
|
|
317
|
+
: { contentHash: change.input.contentHash }),
|
|
318
|
+
}),
|
|
319
|
+
|
|
320
|
+
async search(input) {
|
|
321
|
+
const user = await options.user.read();
|
|
322
|
+
const pin = requiredCatalogPin(user);
|
|
323
|
+
const index = await options.catalog.readIndex(
|
|
324
|
+
pin.generation,
|
|
325
|
+
pin.indexHash,
|
|
326
|
+
);
|
|
327
|
+
const query = input.query.trim().toLocaleLowerCase();
|
|
328
|
+
const entries = index.entries
|
|
329
|
+
.filter((entry) => {
|
|
330
|
+
const haystack = [
|
|
331
|
+
entry.packageId,
|
|
332
|
+
entry.displayName,
|
|
333
|
+
entry.description,
|
|
334
|
+
...(entry.tags ?? []),
|
|
335
|
+
]
|
|
336
|
+
.join("\n")
|
|
337
|
+
.toLocaleLowerCase();
|
|
338
|
+
return query.length === 0 || haystack.includes(query);
|
|
339
|
+
})
|
|
340
|
+
.slice(0, PACKAGE_CATALOG_RESULT_MAX_V1);
|
|
341
|
+
return { generation: pin.generation, entries };
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
async inspect(input) {
|
|
345
|
+
const { user, entry, generation } = await pinnedEntry(input.catalogId);
|
|
346
|
+
const connections = connectionState(entry, user);
|
|
347
|
+
const missingConnectionTypes = connections
|
|
348
|
+
.filter((connection) => !connection.connected)
|
|
349
|
+
.map((connection) => connection.id);
|
|
350
|
+
const source = entry.bundle?.sourceHash
|
|
351
|
+
? await options.catalog.readSource(entry.bundle.sourceHash)
|
|
352
|
+
: undefined;
|
|
353
|
+
return {
|
|
354
|
+
generation,
|
|
355
|
+
entry,
|
|
356
|
+
declaredTools: (entry.bundle?.manifest.tools ?? []).map(
|
|
357
|
+
(tool) => tool.name,
|
|
358
|
+
),
|
|
359
|
+
connectionTypes: connections,
|
|
360
|
+
missingConnectionTypes,
|
|
361
|
+
inert: missingConnectionTypes.length > 0,
|
|
362
|
+
...(source === undefined ? {} : { source }),
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
async undoCatalogChange(request) {
|
|
367
|
+
const replay = await options.storage.get<PackageUndoRecordV1>(
|
|
368
|
+
authorshipUndoOutcomeKey(request.effectId),
|
|
369
|
+
);
|
|
370
|
+
if (replay) {
|
|
371
|
+
return replay.status === "recorded"
|
|
372
|
+
? {
|
|
373
|
+
status: "recorded",
|
|
374
|
+
effectId: replay.effectId,
|
|
375
|
+
generationId: replay.generationId,
|
|
376
|
+
targetGenerationId: replay.targetGenerationId,
|
|
377
|
+
}
|
|
378
|
+
: {
|
|
379
|
+
status: "refused",
|
|
380
|
+
reason: replay.reason,
|
|
381
|
+
failureId: replay.failureId,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const current = await options.composition.current();
|
|
386
|
+
const history = (await options.composition.list({ limit: 100 }))
|
|
387
|
+
.generations;
|
|
388
|
+
const latestCatalog = history.find(
|
|
389
|
+
(generation) => generation.origin.kind === "bot-catalog",
|
|
390
|
+
);
|
|
391
|
+
if (!request.input.generationId && !latestCatalog) return undefined;
|
|
392
|
+
const targetId =
|
|
393
|
+
request.input.generationId ?? latestCatalog?.parentGenerationId;
|
|
394
|
+
if (!targetId) {
|
|
395
|
+
return undoRefused(
|
|
396
|
+
request,
|
|
397
|
+
"There is no earlier Catalog Package setup generation to undo",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const target = await options.composition.read(targetId);
|
|
401
|
+
if (!target) {
|
|
402
|
+
return undoRefused(
|
|
403
|
+
request,
|
|
404
|
+
`Composition generation "${targetId}" is unavailable`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (target.status !== "active" && target.status !== "superseded") {
|
|
408
|
+
return undoRefused(
|
|
409
|
+
request,
|
|
410
|
+
`Composition generation "${targetId}" was never successfully mounted and cannot be an undo target`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (target.generationId === current.generationId) {
|
|
414
|
+
return undoRefused(
|
|
415
|
+
request,
|
|
416
|
+
`Package setup already matches Composition generation "${targetId}"`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const protectedMembers = (generation: CompositionGenerationV1) =>
|
|
420
|
+
generation.members
|
|
421
|
+
.filter(
|
|
422
|
+
(member) =>
|
|
423
|
+
member.provenance.kind !== "bot" &&
|
|
424
|
+
member.provenance.kind !== "catalog",
|
|
425
|
+
)
|
|
426
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId));
|
|
427
|
+
if (
|
|
428
|
+
canonicalJson(protectedMembers(current)) !==
|
|
429
|
+
canonicalJson(protectedMembers(target))
|
|
430
|
+
) {
|
|
431
|
+
return undoRefused(
|
|
432
|
+
request,
|
|
433
|
+
"That generation changes required first-party or User Package setup",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const catalogIds = new Set(
|
|
437
|
+
[...current.members, ...target.members]
|
|
438
|
+
.filter((member) => member.provenance.kind === "catalog")
|
|
439
|
+
.map((member) => member.packageId),
|
|
440
|
+
);
|
|
441
|
+
const changed = [...catalogIds].filter((packageId) => {
|
|
442
|
+
const from = current.members.find(
|
|
443
|
+
(member) => member.packageId === packageId,
|
|
444
|
+
);
|
|
445
|
+
const to = target.members.find(
|
|
446
|
+
(member) => member.packageId === packageId,
|
|
447
|
+
);
|
|
448
|
+
return canonicalJson(from ?? null) !== canonicalJson(to ?? null);
|
|
449
|
+
});
|
|
450
|
+
if (changed.length === 0) return undefined;
|
|
451
|
+
if (changed.length > 1) {
|
|
452
|
+
return undoRefused(
|
|
453
|
+
request,
|
|
454
|
+
"One package_undo may reconcile only one Catalog Package change",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const packageId = changed[0]!;
|
|
458
|
+
const targetMember = target.members.find(
|
|
459
|
+
(member) => member.packageId === packageId,
|
|
460
|
+
);
|
|
461
|
+
const targetCatalogMember =
|
|
462
|
+
targetMember?.provenance.kind === "catalog" ? targetMember : undefined;
|
|
463
|
+
if (targetMember && !targetCatalogMember) {
|
|
464
|
+
return undoRefused(
|
|
465
|
+
request,
|
|
466
|
+
`Undo would replace Catalog Package "${packageId}" with non-Catalog provenance`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const user = await options.user.read();
|
|
471
|
+
let intent = await options.storage.get<PackageCatalogUndoIntentV1>(
|
|
472
|
+
packageCatalogUndoIntentKey(request.effectId),
|
|
473
|
+
);
|
|
474
|
+
if (!intent) {
|
|
475
|
+
intent = {
|
|
476
|
+
schemaVersion: 1,
|
|
477
|
+
effectId: request.effectId,
|
|
478
|
+
targetGenerationId: target.generationId,
|
|
479
|
+
packageId,
|
|
480
|
+
expectedUserRevision: user.revision,
|
|
481
|
+
action: targetCatalogMember ? "install" : "uninstall",
|
|
482
|
+
recordedAt: now().toISOString(),
|
|
483
|
+
status: "recorded",
|
|
484
|
+
};
|
|
485
|
+
// Durable Bot intent before changing User installation state.
|
|
486
|
+
await options.storage.put({
|
|
487
|
+
[packageCatalogUndoIntentKey(request.effectId)]: intent,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let command: UserConfigurationCommandV1;
|
|
492
|
+
if (!targetCatalogMember) {
|
|
493
|
+
command = {
|
|
494
|
+
schemaVersion: 1,
|
|
495
|
+
type: "user/uninstall-package",
|
|
496
|
+
commandId: `${request.effectId}-catalog-user`,
|
|
497
|
+
expectedRevision: intent.expectedUserRevision,
|
|
498
|
+
packageId,
|
|
499
|
+
};
|
|
500
|
+
} else {
|
|
501
|
+
const provenance = targetCatalogMember.provenance;
|
|
502
|
+
if (provenance.kind !== "catalog") {
|
|
503
|
+
throw new Error("Catalog undo target lost its decoded provenance");
|
|
504
|
+
}
|
|
505
|
+
const pin = requiredCatalogPin(user);
|
|
506
|
+
const index = await options.catalog.readIndex(
|
|
507
|
+
provenance.catalogGeneration,
|
|
508
|
+
pin.indexHash,
|
|
509
|
+
);
|
|
510
|
+
const targetEntry = await options.catalog.readEntry({
|
|
511
|
+
generation: provenance.catalogGeneration,
|
|
512
|
+
index,
|
|
513
|
+
catalogId: provenance.catalogId,
|
|
514
|
+
});
|
|
515
|
+
if (
|
|
516
|
+
!targetEntry?.bundle ||
|
|
517
|
+
targetEntry.bundle.contentHash !== provenance.contentHash
|
|
518
|
+
) {
|
|
519
|
+
return undoRefused(
|
|
520
|
+
request,
|
|
521
|
+
`Catalog entry for "${packageId}" is unavailable for undo`,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
command = {
|
|
525
|
+
schemaVersion: 1,
|
|
526
|
+
type: "user/install-package",
|
|
527
|
+
commandId: `${request.effectId}-catalog-user`,
|
|
528
|
+
expectedRevision: intent.expectedUserRevision,
|
|
529
|
+
packageId,
|
|
530
|
+
version: targetCatalogMember.version,
|
|
531
|
+
catalogId: provenance.catalogId,
|
|
532
|
+
catalogGeneration: provenance.catalogGeneration,
|
|
533
|
+
contentHash: provenance.contentHash,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const receipt = await options.user.execute(command);
|
|
537
|
+
if (receipt.status === "rejected") {
|
|
538
|
+
return undoRefused(request, receipt.failure);
|
|
539
|
+
}
|
|
540
|
+
const generation = await options.composition.revert(
|
|
541
|
+
target.generationId,
|
|
542
|
+
{
|
|
543
|
+
kind: "revert",
|
|
544
|
+
revertsTo: target.generationId,
|
|
545
|
+
botId: options.botId,
|
|
546
|
+
runId: options.runId,
|
|
547
|
+
turnId: options.turnId,
|
|
548
|
+
},
|
|
549
|
+
{ createdAt: intent.recordedAt },
|
|
550
|
+
);
|
|
551
|
+
const outcome: PackageUndoRecordV1 = {
|
|
552
|
+
schemaVersion: 1,
|
|
553
|
+
status: "recorded",
|
|
554
|
+
effectId: request.effectId,
|
|
555
|
+
generationId: generation.generationId,
|
|
556
|
+
targetGenerationId: target.generationId,
|
|
557
|
+
recordedAt: intent.recordedAt,
|
|
558
|
+
};
|
|
559
|
+
await options.storage.put({
|
|
560
|
+
[authorshipUndoOutcomeKey(request.effectId)]: outcome,
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
status: "recorded",
|
|
564
|
+
effectId: request.effectId,
|
|
565
|
+
generationId: generation.generationId,
|
|
566
|
+
targetGenerationId: target.generationId,
|
|
567
|
+
};
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
async change(request: PackageCatalogChangeRequestV1) {
|
|
571
|
+
const replay =
|
|
572
|
+
await options.storage.get<PackageCatalogChangeOutcomeRecordV1>(
|
|
573
|
+
packageCatalogChangeOutcomeKey(request.effectId),
|
|
574
|
+
);
|
|
575
|
+
if (replay) return recordedOutcome(replay);
|
|
576
|
+
|
|
577
|
+
const { action } = request.change;
|
|
578
|
+
let existingIntent =
|
|
579
|
+
await options.storage.get<PackageCatalogChangeIntentV1>(
|
|
580
|
+
packageCatalogChangeIntentKey(request.effectId),
|
|
581
|
+
);
|
|
582
|
+
let entry: CatalogEntryV1;
|
|
583
|
+
let user: UserSettingsViewV1;
|
|
584
|
+
let base: CompositionGenerationV1;
|
|
585
|
+
let intent: PackageCatalogChangeIntentV1;
|
|
586
|
+
try {
|
|
587
|
+
user = await options.user.read();
|
|
588
|
+
base = await options.composition.lastKnownGood();
|
|
589
|
+
if (existingIntent) {
|
|
590
|
+
const pin = requiredCatalogPin(user);
|
|
591
|
+
const index = await options.catalog.readIndex(
|
|
592
|
+
existingIntent.catalogGeneration,
|
|
593
|
+
pin.indexHash,
|
|
594
|
+
);
|
|
595
|
+
const resumed = await options.catalog.readEntry({
|
|
596
|
+
generation: existingIntent.catalogGeneration,
|
|
597
|
+
index,
|
|
598
|
+
catalogId: existingIntent.catalogId,
|
|
599
|
+
});
|
|
600
|
+
if (!resumed)
|
|
601
|
+
throw new Error(
|
|
602
|
+
"the intent's immutable Catalog entry is unavailable",
|
|
603
|
+
);
|
|
604
|
+
entry = resumed;
|
|
605
|
+
intent = existingIntent;
|
|
606
|
+
} else if (action === "remove") {
|
|
607
|
+
const packageId = request.change.input.packageId;
|
|
608
|
+
const member = base.members.find(
|
|
609
|
+
(candidate) => candidate.packageId === packageId,
|
|
610
|
+
);
|
|
611
|
+
if (!member)
|
|
612
|
+
throw new Error(`Package "${packageId}" is not installed`);
|
|
613
|
+
if (member.provenance.kind !== "catalog") {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`Package "${packageId}" is a required or non-Catalog member and cannot be removed by package_remove`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const pin = requiredCatalogPin(user);
|
|
619
|
+
const index = await options.catalog.readIndex(
|
|
620
|
+
member.provenance.catalogGeneration,
|
|
621
|
+
pin.indexHash,
|
|
622
|
+
);
|
|
623
|
+
const found = await options.catalog.readEntry({
|
|
624
|
+
generation: member.provenance.catalogGeneration,
|
|
625
|
+
index,
|
|
626
|
+
catalogId: member.provenance.catalogId,
|
|
627
|
+
});
|
|
628
|
+
if (!found?.bundle) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`Installed Catalog Package "${packageId}" has no immutable entry`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
const bundle = found.bundle;
|
|
634
|
+
entry = found;
|
|
635
|
+
const recordedAt = now().toISOString();
|
|
636
|
+
intent = {
|
|
637
|
+
schemaVersion: 1,
|
|
638
|
+
effectId: request.effectId,
|
|
639
|
+
action,
|
|
640
|
+
userId: options.userId,
|
|
641
|
+
botId: options.botId,
|
|
642
|
+
sessionId: request.sessionId,
|
|
643
|
+
runId: options.runId,
|
|
644
|
+
turnId: options.turnId,
|
|
645
|
+
packageId: entry.packageId,
|
|
646
|
+
displayName: entry.displayName,
|
|
647
|
+
version: entry.version,
|
|
648
|
+
catalogId: entry.catalogId,
|
|
649
|
+
catalogGeneration: member.provenance.catalogGeneration,
|
|
650
|
+
contentHash: bundle.contentHash,
|
|
651
|
+
manifestHash: entry.manifestHash,
|
|
652
|
+
summary:
|
|
653
|
+
request.change.input.summary ??
|
|
654
|
+
summary(action, entry.displayName),
|
|
655
|
+
missingConnectionTypes: [],
|
|
656
|
+
baseGenerationId: base.generationId,
|
|
657
|
+
expectedUserRevision: user.revision,
|
|
658
|
+
previousInstallation: user.packages.find(
|
|
659
|
+
(candidate) => candidate.packageId === packageId,
|
|
660
|
+
),
|
|
661
|
+
recordedAt,
|
|
662
|
+
status: "recorded",
|
|
663
|
+
};
|
|
664
|
+
} else {
|
|
665
|
+
const resolved = await pinnedEntry(request.change.input.catalogId);
|
|
666
|
+
user = resolved.user;
|
|
667
|
+
entry = resolved.entry;
|
|
668
|
+
if (!entry.bundle) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`Catalog entry "${entry.catalogId}" does not carry installable code`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
if (entry.bundle.contentHash !== request.change.input.contentHash) {
|
|
674
|
+
throw new Error(
|
|
675
|
+
`Catalog entry "${entry.catalogId}" has content hash "${entry.bundle.contentHash}", not "${request.change.input.contentHash}"`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
await verifyArtifact(entry);
|
|
679
|
+
const member = base.members.find(
|
|
680
|
+
(candidate) => candidate.packageId === entry.packageId,
|
|
681
|
+
);
|
|
682
|
+
if (action === "install" && member) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
`Package "${entry.packageId}" is already in this Bot's Composition`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if (
|
|
688
|
+
action === "update" &&
|
|
689
|
+
(!member || member.provenance.kind !== "catalog")
|
|
690
|
+
) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
`Package "${entry.packageId}" is not an installed Catalog Package`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const connections = connectionState(entry, user);
|
|
696
|
+
const recordedAt = now().toISOString();
|
|
697
|
+
intent = {
|
|
698
|
+
schemaVersion: 1,
|
|
699
|
+
effectId: request.effectId,
|
|
700
|
+
action,
|
|
701
|
+
userId: options.userId,
|
|
702
|
+
botId: options.botId,
|
|
703
|
+
sessionId: request.sessionId,
|
|
704
|
+
runId: options.runId,
|
|
705
|
+
turnId: options.turnId,
|
|
706
|
+
packageId: entry.packageId,
|
|
707
|
+
displayName: entry.displayName,
|
|
708
|
+
version: entry.version,
|
|
709
|
+
catalogId: entry.catalogId,
|
|
710
|
+
catalogGeneration: resolved.generation,
|
|
711
|
+
contentHash: entry.bundle.contentHash,
|
|
712
|
+
manifestHash: entry.manifestHash,
|
|
713
|
+
summary:
|
|
714
|
+
request.change.input.summary ??
|
|
715
|
+
summary(action, entry.displayName),
|
|
716
|
+
missingConnectionTypes: connections
|
|
717
|
+
.filter((connection) => !connection.connected)
|
|
718
|
+
.map((connection) => connection.id),
|
|
719
|
+
baseGenerationId: base.generationId,
|
|
720
|
+
expectedUserRevision: user.revision,
|
|
721
|
+
previousInstallation: user.packages.find(
|
|
722
|
+
(candidate) => candidate.packageId === entry.packageId,
|
|
723
|
+
),
|
|
724
|
+
recordedAt,
|
|
725
|
+
status: "recorded",
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return refuse(
|
|
730
|
+
action,
|
|
731
|
+
request.effectId,
|
|
732
|
+
error instanceof Error ? error.message : String(error),
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Intent and manifest before the User settings effect. The manifest key
|
|
737
|
+
// is the existing isolate loader seam used by authored Packages.
|
|
738
|
+
if (!existingIntent) {
|
|
739
|
+
await options.storage.put({
|
|
740
|
+
[packageCatalogChangeIntentKey(request.effectId)]: intent,
|
|
741
|
+
[authorshipManifestKey(intent.manifestHash)]: {
|
|
742
|
+
schemaVersion: 1,
|
|
743
|
+
manifestHash: intent.manifestHash,
|
|
744
|
+
packageId: intent.packageId,
|
|
745
|
+
version: intent.version,
|
|
746
|
+
manifest: entry.bundle!.manifest,
|
|
747
|
+
createdAt: intent.recordedAt,
|
|
748
|
+
} satisfies AuthoredManifestRecordV1,
|
|
749
|
+
});
|
|
750
|
+
existingIntent = intent;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const command: UserConfigurationCommandV1 =
|
|
754
|
+
action === "remove"
|
|
755
|
+
? {
|
|
756
|
+
schemaVersion: 1,
|
|
757
|
+
type: "user/uninstall-package",
|
|
758
|
+
commandId: `${request.effectId}-user`,
|
|
759
|
+
expectedRevision: intent.expectedUserRevision,
|
|
760
|
+
packageId: intent.packageId,
|
|
761
|
+
}
|
|
762
|
+
: {
|
|
763
|
+
schemaVersion: 1,
|
|
764
|
+
type: "user/install-package",
|
|
765
|
+
commandId: `${request.effectId}-user`,
|
|
766
|
+
expectedRevision: intent.expectedUserRevision,
|
|
767
|
+
packageId: intent.packageId,
|
|
768
|
+
version: intent.version,
|
|
769
|
+
catalogId: intent.catalogId,
|
|
770
|
+
catalogGeneration: intent.catalogGeneration,
|
|
771
|
+
contentHash: intent.contentHash,
|
|
772
|
+
};
|
|
773
|
+
const receipt = await options.user.execute(command);
|
|
774
|
+
if (receipt.status === "rejected") {
|
|
775
|
+
return refuse(action, request.effectId, receipt.failure);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const parent =
|
|
779
|
+
(await options.composition.read(intent.baseGenerationId)) ?? base;
|
|
780
|
+
const members = parent.members.filter(
|
|
781
|
+
(member) => member.packageId !== intent.packageId,
|
|
782
|
+
);
|
|
783
|
+
if (action !== "remove") {
|
|
784
|
+
const bundle = entry.bundle!;
|
|
785
|
+
const member: CompositionMemberV1 = {
|
|
786
|
+
packageId: entry.packageId,
|
|
787
|
+
specifier: `catalog:${entry.catalogId}`,
|
|
788
|
+
version: entry.version,
|
|
789
|
+
manifestHash: entry.manifestHash,
|
|
790
|
+
provenance: {
|
|
791
|
+
kind: "catalog",
|
|
792
|
+
packageId: entry.packageId,
|
|
793
|
+
version: entry.version,
|
|
794
|
+
catalogId: entry.catalogId,
|
|
795
|
+
catalogGeneration: intent.catalogGeneration,
|
|
796
|
+
contentHash: bundle.contentHash,
|
|
797
|
+
},
|
|
798
|
+
artifact: {
|
|
799
|
+
contentHash: bundle.contentHash,
|
|
800
|
+
size: bundle.size,
|
|
801
|
+
mediaType: bundle.mediaType,
|
|
802
|
+
bundlerVersion: bundle.bundlerVersion,
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
members.push(member);
|
|
806
|
+
}
|
|
807
|
+
members.sort((left, right) =>
|
|
808
|
+
left.packageId.localeCompare(right.packageId),
|
|
809
|
+
);
|
|
810
|
+
const artifactSetHash = await compositionArtifactSetHashV1(members);
|
|
811
|
+
const generation = decodeCompositionGenerationV1({
|
|
812
|
+
schemaVersion: 1,
|
|
813
|
+
generationId: compositionGenerationIdV1(
|
|
814
|
+
intent.recordedAt,
|
|
815
|
+
artifactSetHash,
|
|
816
|
+
),
|
|
817
|
+
artifactSetHash,
|
|
818
|
+
parentGenerationId: parent.generationId,
|
|
819
|
+
createdAt: intent.recordedAt,
|
|
820
|
+
origin: {
|
|
821
|
+
kind: "bot-catalog",
|
|
822
|
+
action,
|
|
823
|
+
packageId: intent.packageId,
|
|
824
|
+
catalogId: intent.catalogId,
|
|
825
|
+
botId: options.botId,
|
|
826
|
+
runId: options.runId,
|
|
827
|
+
sessionId: request.sessionId,
|
|
828
|
+
turnId: options.turnId,
|
|
829
|
+
},
|
|
830
|
+
summary: intent.summary,
|
|
831
|
+
members,
|
|
832
|
+
status: "pending",
|
|
833
|
+
});
|
|
834
|
+
if (!(await options.composition.read(generation.generationId))) {
|
|
835
|
+
await options.composition.propose(generation, { pin: true });
|
|
836
|
+
}
|
|
837
|
+
const outcome: PackageCatalogChangeOutcomeRecordV1 = {
|
|
838
|
+
schemaVersion: 1,
|
|
839
|
+
status: "recorded",
|
|
840
|
+
effectId: request.effectId,
|
|
841
|
+
action,
|
|
842
|
+
packageId: intent.packageId,
|
|
843
|
+
displayName: intent.displayName,
|
|
844
|
+
...(action === "remove" ? {} : { version: intent.version }),
|
|
845
|
+
...(action === "remove" ? {} : { contentHash: intent.contentHash }),
|
|
846
|
+
generationId: generation.generationId,
|
|
847
|
+
missingConnectionTypes: intent.missingConnectionTypes,
|
|
848
|
+
recordedAt: intent.recordedAt,
|
|
849
|
+
};
|
|
850
|
+
await options.storage.put({
|
|
851
|
+
[packageCatalogChangeOutcomeKey(request.effectId)]: outcome,
|
|
852
|
+
});
|
|
853
|
+
return recordedOutcome(outcome);
|
|
854
|
+
},
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Hash-verifying R2 reader over the Catalog and Package artifact stores. */
|
|
859
|
+
export function createR2BotPackageCatalogReader(
|
|
860
|
+
catalog: R2Bucket,
|
|
861
|
+
artifacts: R2Bucket,
|
|
862
|
+
): BotPackageCatalogReader {
|
|
863
|
+
async function document(
|
|
864
|
+
bucket: R2Bucket,
|
|
865
|
+
key: string,
|
|
866
|
+
): Promise<string | undefined> {
|
|
867
|
+
const object = await bucket.get(key);
|
|
868
|
+
if (!object) return undefined;
|
|
869
|
+
if (object.size > MAX_CATALOG_DOCUMENT_BYTES_V1) {
|
|
870
|
+
throw new Error(`Catalog object "${key}" exceeds its size bound`);
|
|
871
|
+
}
|
|
872
|
+
return object.text();
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
async readIndex(generation, expectedHash) {
|
|
876
|
+
const value = await document(catalog, catalogIndexKeyV1(generation));
|
|
877
|
+
if (value === undefined)
|
|
878
|
+
throw new Error(`Catalog generation "${generation}" has no index`);
|
|
879
|
+
return decodeCatalogIndexDocumentV1(value, expectedHash);
|
|
880
|
+
},
|
|
881
|
+
async readEntry({ generation, index, catalogId }) {
|
|
882
|
+
const row = index.entries.find(
|
|
883
|
+
(candidate) => candidate.catalogId === catalogId,
|
|
884
|
+
);
|
|
885
|
+
if (!row) return undefined;
|
|
886
|
+
const value = await document(
|
|
887
|
+
catalog,
|
|
888
|
+
catalogEntryKeyV1(generation, catalogId),
|
|
889
|
+
);
|
|
890
|
+
if (value === undefined)
|
|
891
|
+
throw new Error(`Catalog entry "${catalogId}" is absent`);
|
|
892
|
+
const entry = parseCatalogEntryDocumentV1(value);
|
|
893
|
+
assertCatalogEntryMatchesIndexV1(entry, row);
|
|
894
|
+
await assertCatalogPackageBundleV1(entry);
|
|
895
|
+
return entry;
|
|
896
|
+
},
|
|
897
|
+
async readSource(sourceHash) {
|
|
898
|
+
const value = await document(
|
|
899
|
+
artifacts,
|
|
900
|
+
catalogPackageSourceKeyV1(sourceHash),
|
|
901
|
+
);
|
|
902
|
+
if (value === undefined) return undefined;
|
|
903
|
+
if ((await catalogContentHashV1(value)) !== sourceHash) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
`Catalog Package source "${sourceHash}" failed hash verification`,
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
return value;
|
|
909
|
+
},
|
|
910
|
+
async headArtifact(contentHash) {
|
|
911
|
+
const object = await artifacts.head(
|
|
912
|
+
catalogPackageArtifactKeyV1(contentHash),
|
|
913
|
+
);
|
|
914
|
+
return object ? { size: object.size } : undefined;
|
|
915
|
+
},
|
|
916
|
+
async headUiArtifact(contentHash) {
|
|
917
|
+
const object = await artifacts.head(
|
|
918
|
+
catalogPackageUiArtifactKeyV1(contentHash),
|
|
919
|
+
);
|
|
920
|
+
return object ? { size: object.size } : undefined;
|
|
921
|
+
},
|
|
922
|
+
};
|
|
923
|
+
}
|