@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
package/src/backend-authoring.ts
CHANGED
|
@@ -15,10 +15,18 @@
|
|
|
15
15
|
// re-authored Package appends a version and supersedes its predecessor inside
|
|
16
16
|
// a new generation.
|
|
17
17
|
import {
|
|
18
|
+
BOT_ISOLATE_CONTEXT_DTS_V1,
|
|
18
19
|
decodePackageBundleResultV1,
|
|
20
|
+
PACKAGE_UI_ARTIFACT_VERSION,
|
|
21
|
+
PACKAGE_IFRAME_BRIDGE_DTS_V1,
|
|
22
|
+
PACKAGE_IFRAME_HELPER_JS_V1,
|
|
19
23
|
type PackageBundleRequestV1,
|
|
20
24
|
type PackageBundlerBinding,
|
|
21
25
|
} from "@frockbot/kernel-contracts";
|
|
26
|
+
import type {
|
|
27
|
+
CompositionFailureV1,
|
|
28
|
+
CompositionQuarantineV1,
|
|
29
|
+
} from "@frockbot/kernel-composition/activation";
|
|
22
30
|
import { canonicalJson, sha256 } from "@frockbot/kernel-composition/compiler";
|
|
23
31
|
import {
|
|
24
32
|
compositionArtifactSetHashV1,
|
|
@@ -27,6 +35,7 @@ import {
|
|
|
27
35
|
type CompositionGenerationV1,
|
|
28
36
|
type CompositionMemberV1,
|
|
29
37
|
} from "@frockbot/kernel-composition/generation";
|
|
38
|
+
import { decodeFrockBotManifest } from "@frockbot/kernel-composition";
|
|
30
39
|
import {
|
|
31
40
|
artifactKey,
|
|
32
41
|
artifactR2KeyV1,
|
|
@@ -34,13 +43,19 @@ import {
|
|
|
34
43
|
authoredSpecifierV1,
|
|
35
44
|
authoredVersionV1,
|
|
36
45
|
authoringEffectIdV1,
|
|
46
|
+
packageUndoEffectIdV1,
|
|
37
47
|
authoringQuotaDayV1,
|
|
38
48
|
authorshipArtifactKey,
|
|
39
49
|
authorshipFailureKey,
|
|
40
50
|
authorshipIntentKey,
|
|
51
|
+
authorshipLatestFailureKey,
|
|
52
|
+
authorshipManifestKey,
|
|
41
53
|
authorshipPackageKey,
|
|
54
|
+
authorshipUndoIntentKey,
|
|
55
|
+
authorshipUndoOutcomeKey,
|
|
42
56
|
classifyAuthoringEffectV1,
|
|
43
57
|
type AuthoredArtifactRecordV1,
|
|
58
|
+
type AuthoredManifestRecordV1,
|
|
44
59
|
type AuthoredPackageRecordV1,
|
|
45
60
|
type AuthoringEffectOutcomeV1,
|
|
46
61
|
type AuthoringFailureRecordV1,
|
|
@@ -49,28 +64,55 @@ import {
|
|
|
49
64
|
type AuthorPackageRequestV1,
|
|
50
65
|
type AuthorshipIntentV1,
|
|
51
66
|
type PackageAuthoringHost,
|
|
67
|
+
type PackageInspectFailureV1,
|
|
68
|
+
type PackageInspectSelfOutcomeV1,
|
|
69
|
+
type PackageUndoIntentV1,
|
|
70
|
+
type PackageUndoOutcomeV1,
|
|
71
|
+
type PackageUndoRecordV1,
|
|
72
|
+
type PackageUndoRequestV1,
|
|
73
|
+
sourceR2KeyV1,
|
|
52
74
|
} from "@frockbot/plugin-authoring";
|
|
53
75
|
|
|
54
76
|
/** The narrow Bot Durable Object storage surface authoring needs. */
|
|
55
77
|
export interface AuthoringStorage {
|
|
56
78
|
get<T>(key: string): Promise<T | undefined>;
|
|
57
79
|
put(entries: Record<string, unknown>): Promise<void>;
|
|
80
|
+
list?<T>(options: { prefix: string }): Promise<Map<string, T>>;
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
/** The Composition surface authoring needs; `DurableCompositionStore` satisfies it. */
|
|
61
84
|
export interface AuthoringCompositionStore {
|
|
62
85
|
current(): Promise<CompositionGenerationV1>;
|
|
86
|
+
lastKnownGood(): Promise<CompositionGenerationV1>;
|
|
63
87
|
read(generationId: string): Promise<CompositionGenerationV1 | undefined>;
|
|
64
88
|
propose(
|
|
65
89
|
generation: CompositionGenerationV1,
|
|
66
90
|
options?: { pin?: boolean },
|
|
67
91
|
): Promise<void>;
|
|
68
92
|
retainedCount(): Promise<number>;
|
|
93
|
+
revert(
|
|
94
|
+
toGenerationId: string,
|
|
95
|
+
origin: {
|
|
96
|
+
kind: "revert";
|
|
97
|
+
revertsTo: string;
|
|
98
|
+
botId: string;
|
|
99
|
+
runId: string;
|
|
100
|
+
turnId: string;
|
|
101
|
+
},
|
|
102
|
+
options?: { createdAt?: string },
|
|
103
|
+
): Promise<CompositionGenerationV1>;
|
|
104
|
+
list(query: {
|
|
105
|
+
limit: number;
|
|
106
|
+
cursor?: string;
|
|
107
|
+
}): Promise<{ generations: CompositionGenerationV1[]; cursor?: string }>;
|
|
69
108
|
}
|
|
70
109
|
|
|
71
110
|
/** Immutable content, written once and addressed by hash. */
|
|
72
111
|
export interface AuthoringArtifactStore {
|
|
73
112
|
putPackageArtifact(contentHash: string, module: string): Promise<void>;
|
|
113
|
+
putPackageUiArtifact?(contentHash: string, html: string): Promise<void>;
|
|
114
|
+
putPackageSource(sourceHash: string, source: string): Promise<void>;
|
|
115
|
+
loadPackageSource(sourceHash: string): Promise<string | undefined>;
|
|
74
116
|
headPackageArtifact(
|
|
75
117
|
contentHash: string,
|
|
76
118
|
): Promise<{ contentHash: string; size: number } | undefined>;
|
|
@@ -89,6 +131,16 @@ export interface PackageAuthoringHostOptions {
|
|
|
89
131
|
runId: string;
|
|
90
132
|
turnId: string;
|
|
91
133
|
compatibilityDate: string;
|
|
134
|
+
/** The mounted root's exact catalog, read only when `author` is called. */
|
|
135
|
+
currentToolNames?(): readonly string[];
|
|
136
|
+
/** The generation actually mounted after fail-closed fallback. */
|
|
137
|
+
mountedGeneration?(): CompositionGenerationV1 | undefined;
|
|
138
|
+
activationFailures?: {
|
|
139
|
+
list(generationId: string): Promise<CompositionFailureV1[]>;
|
|
140
|
+
quarantine(
|
|
141
|
+
generationId: string,
|
|
142
|
+
): Promise<CompositionQuarantineV1 | undefined>;
|
|
143
|
+
};
|
|
92
144
|
now?(): Date;
|
|
93
145
|
newId?(): string;
|
|
94
146
|
}
|
|
@@ -127,7 +179,10 @@ export function createPackageAuthoringHost(
|
|
|
127
179
|
diagnostics: input.diagnostics ?? [],
|
|
128
180
|
recordedAt: now().toISOString(),
|
|
129
181
|
};
|
|
130
|
-
await options.storage.put({
|
|
182
|
+
await options.storage.put({
|
|
183
|
+
[authorshipFailureKey(failureId)]: record,
|
|
184
|
+
[authorshipLatestFailureKey(input.packageId)]: record,
|
|
185
|
+
});
|
|
131
186
|
return record;
|
|
132
187
|
}
|
|
133
188
|
|
|
@@ -141,6 +196,27 @@ export function createPackageAuthoringHost(
|
|
|
141
196
|
};
|
|
142
197
|
}
|
|
143
198
|
|
|
199
|
+
async function undoRefused(
|
|
200
|
+
record: AuthoringFailureRecordV1,
|
|
201
|
+
): Promise<Extract<PackageUndoOutcomeV1, { status: "refused" }>> {
|
|
202
|
+
const outcome = {
|
|
203
|
+
status: "refused",
|
|
204
|
+
reason: record.reason,
|
|
205
|
+
failureId: record.failureId,
|
|
206
|
+
} as const;
|
|
207
|
+
await options.storage.put({
|
|
208
|
+
[authorshipUndoOutcomeKey(record.effectId)]: {
|
|
209
|
+
schemaVersion: 1,
|
|
210
|
+
effectId: record.effectId,
|
|
211
|
+
failureId: record.failureId,
|
|
212
|
+
reason: record.reason,
|
|
213
|
+
recordedAt: record.recordedAt,
|
|
214
|
+
status: "refused",
|
|
215
|
+
} satisfies PackageUndoRecordV1,
|
|
216
|
+
});
|
|
217
|
+
return outcome;
|
|
218
|
+
}
|
|
219
|
+
|
|
144
220
|
/**
|
|
145
221
|
* The constitutional shadowing rule: a Bot authors only over its own
|
|
146
222
|
* Packages. A member of the current Composition whose provenance is
|
|
@@ -151,17 +227,85 @@ export function createPackageAuthoringHost(
|
|
|
151
227
|
async function shadowedMember(
|
|
152
228
|
packageId: string,
|
|
153
229
|
): Promise<CompositionMemberV1 | undefined> {
|
|
154
|
-
const
|
|
155
|
-
|
|
230
|
+
const current = await options.composition.current();
|
|
231
|
+
const lastKnownGood = await options.composition.lastKnownGood();
|
|
232
|
+
return [current, lastKnownGood]
|
|
233
|
+
.flatMap((generation) => generation.members)
|
|
234
|
+
.find(
|
|
235
|
+
(member) =>
|
|
236
|
+
member.packageId === packageId && member.provenance.kind !== "bot",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function storedManifest(
|
|
241
|
+
member: CompositionMemberV1,
|
|
242
|
+
): Promise<AuthoredManifestRecordV1 | undefined> {
|
|
243
|
+
if (member.provenance.kind !== "bot") return undefined;
|
|
244
|
+
const stored = await options.storage.get<AuthoredManifestRecordV1>(
|
|
245
|
+
authorshipManifestKey(member.manifestHash),
|
|
246
|
+
);
|
|
247
|
+
if (
|
|
248
|
+
!stored ||
|
|
249
|
+
stored.manifestHash !== member.manifestHash ||
|
|
250
|
+
stored.packageId !== member.packageId ||
|
|
251
|
+
stored.version !== member.version
|
|
252
|
+
) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
return stored;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function compositionHistory(): Promise<CompositionGenerationV1[]> {
|
|
259
|
+
const generations: CompositionGenerationV1[] = [];
|
|
260
|
+
let cursor: string | undefined;
|
|
261
|
+
for (let pageNumber = 0; pageNumber < 100; pageNumber += 1) {
|
|
262
|
+
const page = await options.composition.list({
|
|
263
|
+
limit: 100,
|
|
264
|
+
...(cursor ? { cursor } : {}),
|
|
265
|
+
});
|
|
266
|
+
generations.push(...page.generations);
|
|
267
|
+
if (!page.cursor) return generations;
|
|
268
|
+
if (page.cursor === cursor) {
|
|
269
|
+
throw new Error("Composition history returned a repeated cursor");
|
|
270
|
+
}
|
|
271
|
+
cursor = page.cursor;
|
|
272
|
+
}
|
|
273
|
+
throw new Error("Composition history exceeds its durable bound");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A declaration that would collide is refused before quota reservation.
|
|
278
|
+
* Re-authoring may keep or rename tools owned by that same Package, so its
|
|
279
|
+
* currently stored declarations are subtracted from the mounted catalog;
|
|
280
|
+
* every first-party tool and every other authored Package remains reserved.
|
|
281
|
+
*/
|
|
282
|
+
async function collidingToolName(
|
|
283
|
+
packageId: string,
|
|
284
|
+
declaredNames: readonly string[],
|
|
285
|
+
): Promise<string | undefined> {
|
|
286
|
+
const registered = new Set(options.currentToolNames?.() ?? []);
|
|
287
|
+
const mounted =
|
|
288
|
+
options.mountedGeneration?.() ??
|
|
289
|
+
(await options.composition.lastKnownGood());
|
|
290
|
+
const own = mounted.members.find(
|
|
156
291
|
(member) =>
|
|
157
|
-
member.packageId === packageId && member.provenance.kind
|
|
292
|
+
member.packageId === packageId && member.provenance.kind === "bot",
|
|
158
293
|
);
|
|
294
|
+
if (own) {
|
|
295
|
+
const recorded = await storedManifest(own);
|
|
296
|
+
const manifest = recorded
|
|
297
|
+
? decodeFrockBotManifest(recorded.manifest)
|
|
298
|
+
: undefined;
|
|
299
|
+
for (const tool of manifest?.tools ?? []) registered.delete(tool.name);
|
|
300
|
+
}
|
|
301
|
+
return declaredNames.find((name) => registered.has(name));
|
|
159
302
|
}
|
|
160
303
|
|
|
161
304
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
305
|
+
* New authoring always branches from last-known-good, replacing only the
|
|
306
|
+
* Package being authored. This deliberately drops every pending, failed, or
|
|
307
|
+
* quarantined proposal: a broken member can be repaired by re-authoring its
|
|
308
|
+
* packageId, but it can never poison a later, unrelated generation.
|
|
165
309
|
*/
|
|
166
310
|
async function nextGeneration(input: {
|
|
167
311
|
member: CompositionMemberV1;
|
|
@@ -171,7 +315,7 @@ export function createPackageAuthoringHost(
|
|
|
171
315
|
generation: CompositionGenerationV1;
|
|
172
316
|
supersededVersion?: string;
|
|
173
317
|
}> {
|
|
174
|
-
const parent = await options.composition.
|
|
318
|
+
const parent = await options.composition.lastKnownGood();
|
|
175
319
|
const superseded = parent.members.find(
|
|
176
320
|
(member) => member.packageId === input.member.packageId,
|
|
177
321
|
);
|
|
@@ -205,10 +349,11 @@ export function createPackageAuthoringHost(
|
|
|
205
349
|
|
|
206
350
|
async function compose(input: {
|
|
207
351
|
request: AuthorPackageRequestV1;
|
|
352
|
+
intent: AuthorshipIntentV1;
|
|
208
353
|
outcome: Extract<AuthoringEffectOutcomeV1, { status: "bundled" }>;
|
|
209
354
|
artifact: AuthoredArtifactRecordV1;
|
|
210
355
|
}): Promise<AuthorPackageOutcomeV1> {
|
|
211
|
-
const { request, outcome, artifact } = input;
|
|
356
|
+
const { request, intent, outcome, artifact } = input;
|
|
212
357
|
if (outcome.generationId) {
|
|
213
358
|
const recorded = await options.composition.read(outcome.generationId);
|
|
214
359
|
if (recorded) {
|
|
@@ -221,18 +366,39 @@ export function createPackageAuthoringHost(
|
|
|
221
366
|
};
|
|
222
367
|
}
|
|
223
368
|
}
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
369
|
+
const manifestRecord = await options.storage.get<AuthoredManifestRecordV1>(
|
|
370
|
+
authorshipManifestKey(intent.manifestHash),
|
|
371
|
+
);
|
|
372
|
+
if (!manifestRecord) {
|
|
373
|
+
return refused(
|
|
374
|
+
await recordFailure({
|
|
375
|
+
effectId: request.effectId,
|
|
376
|
+
packageId: request.input.packageId,
|
|
377
|
+
phase: "recovery",
|
|
378
|
+
reason: `authoring effect "${request.effectId}" has no stored manifest "${intent.manifestHash}"`,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const manifest = decodeFrockBotManifest(manifestRecord.manifest);
|
|
383
|
+
if (
|
|
384
|
+
manifestRecord.manifestHash !== intent.manifestHash ||
|
|
385
|
+
manifest.id !== request.input.packageId ||
|
|
386
|
+
manifest.version !== outcome.version
|
|
387
|
+
) {
|
|
388
|
+
return refused(
|
|
389
|
+
await recordFailure({
|
|
390
|
+
effectId: request.effectId,
|
|
391
|
+
packageId: request.input.packageId,
|
|
392
|
+
phase: "recovery",
|
|
393
|
+
reason: `stored manifest "${intent.manifestHash}" does not match this authoring effect`,
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
231
397
|
const member: CompositionMemberV1 = {
|
|
232
398
|
packageId: request.input.packageId,
|
|
233
399
|
specifier: authoredSpecifierV1(request.input.packageId),
|
|
234
400
|
version: outcome.version,
|
|
235
|
-
manifestHash:
|
|
401
|
+
manifestHash: intent.manifestHash,
|
|
236
402
|
provenance: artifact.provenance,
|
|
237
403
|
artifact: {
|
|
238
404
|
contentHash: artifact.contentHash,
|
|
@@ -271,6 +437,16 @@ export function createPackageAuthoringHost(
|
|
|
271
437
|
runId: options.runId,
|
|
272
438
|
packageId: input.packageId,
|
|
273
439
|
sourceHash: input.sourceHash,
|
|
440
|
+
...(input.uiHtmlHash === undefined
|
|
441
|
+
? {}
|
|
442
|
+
: { uiHtmlHash: input.uiHtmlHash }),
|
|
443
|
+
...(input.hooks === undefined ? {} : { hooks: input.hooks }),
|
|
444
|
+
}),
|
|
445
|
+
|
|
446
|
+
undoEffectIdFor: (input) =>
|
|
447
|
+
packageUndoEffectIdV1({
|
|
448
|
+
runId: options.runId,
|
|
449
|
+
...(input.generationId ? { generationId: input.generationId } : {}),
|
|
274
450
|
}),
|
|
275
451
|
|
|
276
452
|
async author(
|
|
@@ -292,6 +468,20 @@ export function createPackageAuthoringHost(
|
|
|
292
468
|
}),
|
|
293
469
|
);
|
|
294
470
|
}
|
|
471
|
+
const collision = await collidingToolName(
|
|
472
|
+
packageId,
|
|
473
|
+
request.input.tools.map((tool) => tool.name),
|
|
474
|
+
);
|
|
475
|
+
if (collision) {
|
|
476
|
+
return refused(
|
|
477
|
+
await recordFailure({
|
|
478
|
+
effectId,
|
|
479
|
+
packageId,
|
|
480
|
+
phase: "compose",
|
|
481
|
+
reason: `Tool "${collision}" is already registered in this Bot's current Composition; choose a different tool name`,
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
295
485
|
|
|
296
486
|
const intent = await options.storage.get<AuthorshipIntentV1>(
|
|
297
487
|
authorshipIntentKey(effectId),
|
|
@@ -325,6 +515,9 @@ export function createPackageAuthoringHost(
|
|
|
325
515
|
};
|
|
326
516
|
}
|
|
327
517
|
if (classification.kind === "settled") {
|
|
518
|
+
if (!intent) {
|
|
519
|
+
throw new Error("a settled authoring effect has no durable intent");
|
|
520
|
+
}
|
|
328
521
|
const artifact = await options.storage.get<AuthoredArtifactRecordV1>(
|
|
329
522
|
artifactKey(classification.outcome.contentHash),
|
|
330
523
|
);
|
|
@@ -340,19 +533,24 @@ export function createPackageAuthoringHost(
|
|
|
340
533
|
}
|
|
341
534
|
return await compose({
|
|
342
535
|
request,
|
|
536
|
+
intent,
|
|
343
537
|
outcome: classification.outcome,
|
|
344
538
|
artifact,
|
|
345
539
|
});
|
|
346
540
|
}
|
|
347
541
|
|
|
348
|
-
if (
|
|
542
|
+
if (
|
|
543
|
+
!options.bundler ||
|
|
544
|
+
!options.artifacts ||
|
|
545
|
+
(request.input.ui && !options.artifacts.putPackageUiArtifact)
|
|
546
|
+
) {
|
|
349
547
|
return refused(
|
|
350
548
|
await recordFailure({
|
|
351
549
|
effectId,
|
|
352
550
|
packageId,
|
|
353
551
|
phase: "compose",
|
|
354
552
|
reason:
|
|
355
|
-
"this host cannot author Packages: it has no Package bundler or artifact store",
|
|
553
|
+
"this host cannot author Packages: it has no required Package bundler or artifact store",
|
|
356
554
|
}),
|
|
357
555
|
);
|
|
358
556
|
}
|
|
@@ -362,6 +560,33 @@ export function createPackageAuthoringHost(
|
|
|
362
560
|
);
|
|
363
561
|
const ordinal = (previous?.ordinal ?? 0) + 1;
|
|
364
562
|
const version = authoredVersionV1(ordinal);
|
|
563
|
+
const uiArtifact = request.input.ui
|
|
564
|
+
? {
|
|
565
|
+
contentHash: await sha256(request.input.ui.html),
|
|
566
|
+
size: new TextEncoder().encode(request.input.ui.html).byteLength,
|
|
567
|
+
mediaType: "text/html" as const,
|
|
568
|
+
bundlerVersion: PACKAGE_UI_ARTIFACT_VERSION,
|
|
569
|
+
}
|
|
570
|
+
: undefined;
|
|
571
|
+
const rawManifest = authoredManifestV1({
|
|
572
|
+
packageId,
|
|
573
|
+
displayName: request.input.displayName,
|
|
574
|
+
version,
|
|
575
|
+
tools: request.input.tools,
|
|
576
|
+
hooks: request.input.hooks,
|
|
577
|
+
...(request.input.ui && uiArtifact
|
|
578
|
+
? {
|
|
579
|
+
ui: {
|
|
580
|
+
artifact: uiArtifact,
|
|
581
|
+
mounts: request.input.ui.mounts,
|
|
582
|
+
},
|
|
583
|
+
}
|
|
584
|
+
: {}),
|
|
585
|
+
});
|
|
586
|
+
// The generated document crosses the same strict seam as every other
|
|
587
|
+
// manifest before it becomes durable or participates in a generation.
|
|
588
|
+
decodeFrockBotManifest(rawManifest);
|
|
589
|
+
const manifestHash = await sha256(canonicalJson(rawManifest));
|
|
365
590
|
const sourceBytes = new TextEncoder().encode(
|
|
366
591
|
request.input.source,
|
|
367
592
|
).byteLength;
|
|
@@ -404,12 +629,21 @@ export function createPackageAuthoringHost(
|
|
|
404
629
|
packageId,
|
|
405
630
|
version,
|
|
406
631
|
sourceHash: request.sourceHash,
|
|
632
|
+
manifestHash,
|
|
407
633
|
sourceBytes,
|
|
408
634
|
recordedAt,
|
|
409
635
|
status: "recorded",
|
|
410
636
|
};
|
|
411
637
|
await options.storage.put({
|
|
412
638
|
[authorshipIntentKey(effectId)]: recordedIntent,
|
|
639
|
+
[authorshipManifestKey(manifestHash)]: {
|
|
640
|
+
schemaVersion: 1,
|
|
641
|
+
manifestHash,
|
|
642
|
+
packageId,
|
|
643
|
+
version,
|
|
644
|
+
manifest: rawManifest,
|
|
645
|
+
createdAt: recordedAt,
|
|
646
|
+
} satisfies AuthoredManifestRecordV1,
|
|
413
647
|
});
|
|
414
648
|
|
|
415
649
|
const bundleRequest: PackageBundleRequestV1 = {
|
|
@@ -419,6 +653,9 @@ export function createPackageAuthoringHost(
|
|
|
419
653
|
compatibilityDate: options.compatibilityDate,
|
|
420
654
|
entry: "package.ts",
|
|
421
655
|
sources: [{ path: "package.ts", text: request.input.source }],
|
|
656
|
+
...(request.input.ui
|
|
657
|
+
? { ui: { path: "ui.html" as const, html: request.input.ui.html } }
|
|
658
|
+
: {}),
|
|
422
659
|
};
|
|
423
660
|
let bundled;
|
|
424
661
|
try {
|
|
@@ -459,12 +696,41 @@ export function createPackageAuthoringHost(
|
|
|
459
696
|
}
|
|
460
697
|
|
|
461
698
|
const { artifact } = bundled;
|
|
699
|
+
if (
|
|
700
|
+
uiArtifact &&
|
|
701
|
+
(!bundled.uiArtifact ||
|
|
702
|
+
bundled.uiHtml !== request.input.ui?.html ||
|
|
703
|
+
bundled.uiArtifact.contentHash !== uiArtifact.contentHash ||
|
|
704
|
+
bundled.uiArtifact.size !== uiArtifact.size ||
|
|
705
|
+
bundled.uiArtifact.mediaType !== uiArtifact.mediaType ||
|
|
706
|
+
bundled.uiArtifact.bundlerVersion !== uiArtifact.bundlerVersion)
|
|
707
|
+
) {
|
|
708
|
+
return refused(
|
|
709
|
+
await recordFailure({
|
|
710
|
+
effectId,
|
|
711
|
+
packageId,
|
|
712
|
+
phase: "bundle",
|
|
713
|
+
reason:
|
|
714
|
+
"the Package bundler returned UI bytes that do not match the recorded manifest",
|
|
715
|
+
}),
|
|
716
|
+
);
|
|
717
|
+
}
|
|
462
718
|
// Content-addressed and immutable: writing the same hash twice is a
|
|
463
719
|
// no-op, so the object write is safe to repeat and the record is not.
|
|
464
720
|
await options.artifacts.putPackageArtifact(
|
|
465
721
|
artifact.contentHash,
|
|
466
722
|
bundled.module,
|
|
467
723
|
);
|
|
724
|
+
if (bundled.uiArtifact && bundled.uiHtml) {
|
|
725
|
+
await options.artifacts.putPackageUiArtifact!(
|
|
726
|
+
bundled.uiArtifact.contentHash,
|
|
727
|
+
bundled.uiHtml,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
await options.artifacts.putPackageSource(
|
|
731
|
+
request.sourceHash,
|
|
732
|
+
request.input.source,
|
|
733
|
+
);
|
|
468
734
|
const artifactRecord: AuthoredArtifactRecordV1 = {
|
|
469
735
|
schemaVersion: 1,
|
|
470
736
|
contentHash: artifact.contentHash,
|
|
@@ -473,6 +739,9 @@ export function createPackageAuthoringHost(
|
|
|
473
739
|
bundlerVersion: artifact.bundlerVersion,
|
|
474
740
|
effectId,
|
|
475
741
|
r2Key: artifactR2KeyV1(artifact.contentHash),
|
|
742
|
+
sourceHash: request.sourceHash,
|
|
743
|
+
sourceR2Key: sourceR2KeyV1(request.sourceHash),
|
|
744
|
+
manifestHash,
|
|
476
745
|
provenance: {
|
|
477
746
|
kind: "bot",
|
|
478
747
|
packageId,
|
|
@@ -505,7 +774,261 @@ export function createPackageAuthoringHost(
|
|
|
505
774
|
updatedAt: recordedAt,
|
|
506
775
|
} satisfies AuthoredPackageRecordV1,
|
|
507
776
|
});
|
|
508
|
-
return await compose({
|
|
777
|
+
return await compose({
|
|
778
|
+
request,
|
|
779
|
+
intent: recordedIntent,
|
|
780
|
+
outcome,
|
|
781
|
+
artifact: artifactRecord,
|
|
782
|
+
});
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
async undo(request: PackageUndoRequestV1): Promise<PackageUndoOutcomeV1> {
|
|
786
|
+
const replay = await options.storage.get<PackageUndoRecordV1>(
|
|
787
|
+
authorshipUndoOutcomeKey(request.effectId),
|
|
788
|
+
);
|
|
789
|
+
if (replay) {
|
|
790
|
+
return replay.status === "recorded"
|
|
791
|
+
? {
|
|
792
|
+
status: "recorded",
|
|
793
|
+
effectId: replay.effectId,
|
|
794
|
+
generationId: replay.generationId,
|
|
795
|
+
targetGenerationId: replay.targetGenerationId,
|
|
796
|
+
}
|
|
797
|
+
: {
|
|
798
|
+
status: "refused",
|
|
799
|
+
reason: replay.reason,
|
|
800
|
+
failureId: replay.failureId,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const existingIntent = await options.storage.get<PackageUndoIntentV1>(
|
|
805
|
+
authorshipUndoIntentKey(request.effectId),
|
|
806
|
+
);
|
|
807
|
+
const history = (await options.composition.list({ limit: 100 }))
|
|
808
|
+
.generations;
|
|
809
|
+
const currentGood = await options.composition.lastKnownGood();
|
|
810
|
+
const current = await options.composition.current();
|
|
811
|
+
let target: CompositionGenerationV1 | undefined;
|
|
812
|
+
if (existingIntent) {
|
|
813
|
+
target = await options.composition.read(
|
|
814
|
+
existingIntent.targetGenerationId,
|
|
815
|
+
);
|
|
816
|
+
} else if (request.input.generationId) {
|
|
817
|
+
target = await options.composition.read(request.input.generationId);
|
|
818
|
+
} else {
|
|
819
|
+
const latestAuthored = history.find(
|
|
820
|
+
(generation) => generation.origin.kind === "bot-authored",
|
|
821
|
+
);
|
|
822
|
+
target = latestAuthored?.parentGenerationId
|
|
823
|
+
? await options.composition.read(latestAuthored.parentGenerationId)
|
|
824
|
+
: undefined;
|
|
825
|
+
}
|
|
826
|
+
if (!target) {
|
|
827
|
+
return undoRefused(
|
|
828
|
+
await recordFailure({
|
|
829
|
+
effectId: request.effectId,
|
|
830
|
+
packageId: "composition",
|
|
831
|
+
phase: "compose",
|
|
832
|
+
reason: request.input.generationId
|
|
833
|
+
? `Composition generation "${request.input.generationId}" is unavailable`
|
|
834
|
+
: "There is no earlier Bot-authored Package setup change to undo",
|
|
835
|
+
}),
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (target.status !== "active" && target.status !== "superseded") {
|
|
839
|
+
return undoRefused(
|
|
840
|
+
await recordFailure({
|
|
841
|
+
effectId: request.effectId,
|
|
842
|
+
packageId: "composition",
|
|
843
|
+
phase: "compose",
|
|
844
|
+
reason: `Composition generation "${target.generationId}" was never successfully mounted and cannot be an undo target`,
|
|
845
|
+
}),
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (target.generationId === current.generationId) {
|
|
849
|
+
return undoRefused(
|
|
850
|
+
await recordFailure({
|
|
851
|
+
effectId: request.effectId,
|
|
852
|
+
packageId: "composition",
|
|
853
|
+
phase: "compose",
|
|
854
|
+
reason: `Package setup already matches Composition generation "${target.generationId}"`,
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// A Bot-origin undo may change only Bot-provenance members. First-party
|
|
860
|
+
// and User members must be byte-for-byte identical to the current good
|
|
861
|
+
// setup, so this path cannot become an alternate authority grant/revoke.
|
|
862
|
+
const nonBot = (generation: CompositionGenerationV1) =>
|
|
863
|
+
generation.members
|
|
864
|
+
.filter((member) => member.provenance.kind !== "bot")
|
|
865
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId));
|
|
866
|
+
if (
|
|
867
|
+
canonicalJson(nonBot(target)) !== canonicalJson(nonBot(currentGood))
|
|
868
|
+
) {
|
|
869
|
+
return undoRefused(
|
|
870
|
+
await recordFailure({
|
|
871
|
+
effectId: request.effectId,
|
|
872
|
+
packageId: "composition",
|
|
873
|
+
phase: "compose",
|
|
874
|
+
reason:
|
|
875
|
+
"That generation changes first-party or User Package setup; package_undo may revert only this Bot's authored Packages",
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const recordedAt = existingIntent?.recordedAt ?? now().toISOString();
|
|
881
|
+
const intent: PackageUndoIntentV1 = existingIntent ?? {
|
|
882
|
+
schemaVersion: 1,
|
|
883
|
+
effectId: request.effectId,
|
|
884
|
+
botId: options.botId,
|
|
885
|
+
runId: options.runId,
|
|
886
|
+
turnId: options.turnId,
|
|
887
|
+
...(request.input.generationId
|
|
888
|
+
? { requestedGenerationId: request.input.generationId }
|
|
889
|
+
: {}),
|
|
890
|
+
targetGenerationId: target.generationId,
|
|
891
|
+
recordedAt,
|
|
892
|
+
status: "recorded",
|
|
893
|
+
};
|
|
894
|
+
// Intent before effect. `createdAt` makes the new generation id stable
|
|
895
|
+
// when a Durable Object resumes after `revert` but before outcome write.
|
|
896
|
+
if (!existingIntent) {
|
|
897
|
+
await options.storage.put({
|
|
898
|
+
[authorshipUndoIntentKey(request.effectId)]: intent,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
const generation = await options.composition.revert(
|
|
902
|
+
target.generationId,
|
|
903
|
+
{
|
|
904
|
+
kind: "revert",
|
|
905
|
+
revertsTo: target.generationId,
|
|
906
|
+
botId: options.botId,
|
|
907
|
+
runId: options.runId,
|
|
908
|
+
turnId: options.turnId,
|
|
909
|
+
},
|
|
910
|
+
{ createdAt: intent.recordedAt },
|
|
911
|
+
);
|
|
912
|
+
const outcome: PackageUndoRecordV1 = {
|
|
913
|
+
schemaVersion: 1,
|
|
914
|
+
effectId: request.effectId,
|
|
915
|
+
generationId: generation.generationId,
|
|
916
|
+
targetGenerationId: target.generationId,
|
|
917
|
+
recordedAt,
|
|
918
|
+
status: "recorded",
|
|
919
|
+
};
|
|
920
|
+
await options.storage.put({
|
|
921
|
+
[authorshipUndoOutcomeKey(request.effectId)]: outcome,
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
status: "recorded",
|
|
925
|
+
effectId: request.effectId,
|
|
926
|
+
generationId: generation.generationId,
|
|
927
|
+
targetGenerationId: target.generationId,
|
|
928
|
+
};
|
|
929
|
+
},
|
|
930
|
+
|
|
931
|
+
async inspectSelf(): Promise<PackageInspectSelfOutcomeV1> {
|
|
932
|
+
const composition =
|
|
933
|
+
options.mountedGeneration?.() ?? (await options.composition.current());
|
|
934
|
+
const history = await compositionHistory();
|
|
935
|
+
const members = await Promise.all(
|
|
936
|
+
composition.members.map(async (member) => {
|
|
937
|
+
const recorded = await storedManifest(member);
|
|
938
|
+
const manifest = recorded
|
|
939
|
+
? decodeFrockBotManifest(recorded.manifest)
|
|
940
|
+
: undefined;
|
|
941
|
+
const source =
|
|
942
|
+
member.provenance.kind === "bot" && options.artifacts
|
|
943
|
+
? await readAuthoredCompositionMemberSourceV1({
|
|
944
|
+
storage: options.storage,
|
|
945
|
+
artifacts: options.artifacts,
|
|
946
|
+
member,
|
|
947
|
+
})
|
|
948
|
+
: undefined;
|
|
949
|
+
return {
|
|
950
|
+
packageId: member.packageId,
|
|
951
|
+
version: member.version,
|
|
952
|
+
provenance: structuredClone(member.provenance) as unknown as Record<
|
|
953
|
+
string,
|
|
954
|
+
unknown
|
|
955
|
+
>,
|
|
956
|
+
declaredTools: (manifest?.tools ?? []).map((tool) => tool.name),
|
|
957
|
+
...(source === undefined ? {} : { source }),
|
|
958
|
+
};
|
|
959
|
+
}),
|
|
960
|
+
);
|
|
961
|
+
const packageIds = new Set(
|
|
962
|
+
history.flatMap((generation) =>
|
|
963
|
+
generation.members
|
|
964
|
+
.filter((member) => member.provenance.kind === "bot")
|
|
965
|
+
.map((member) => member.packageId),
|
|
966
|
+
),
|
|
967
|
+
);
|
|
968
|
+
const failures: PackageInspectFailureV1[] = [];
|
|
969
|
+
for (const packageId of [...packageIds].sort()) {
|
|
970
|
+
const authoring = await options.storage.get<AuthoringFailureRecordV1>(
|
|
971
|
+
authorshipLatestFailureKey(packageId),
|
|
972
|
+
);
|
|
973
|
+
let activation: PackageInspectFailureV1["activation"] | undefined;
|
|
974
|
+
if (options.activationFailures) {
|
|
975
|
+
for (const generation of history) {
|
|
976
|
+
if (activation || generation.origin.kind !== "bot-authored") {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const origin = generation.origin;
|
|
980
|
+
const authoredMember = generation.members.find(
|
|
981
|
+
(member) =>
|
|
982
|
+
member.packageId === packageId &&
|
|
983
|
+
member.provenance.kind === "bot" &&
|
|
984
|
+
member.provenance.runId === origin.runId,
|
|
985
|
+
);
|
|
986
|
+
if (!authoredMember) continue;
|
|
987
|
+
const recorded = await options.activationFailures.list(
|
|
988
|
+
generation.generationId,
|
|
989
|
+
);
|
|
990
|
+
const latest = recorded.at(-1);
|
|
991
|
+
if (!latest) continue;
|
|
992
|
+
activation = {
|
|
993
|
+
generationId: generation.generationId,
|
|
994
|
+
attempt: latest.attempt,
|
|
995
|
+
phase: latest.phase,
|
|
996
|
+
message: latest.message,
|
|
997
|
+
diagnostics: latest.diagnostics,
|
|
998
|
+
at: latest.at,
|
|
999
|
+
quarantined: Boolean(
|
|
1000
|
+
await options.activationFailures.quarantine(
|
|
1001
|
+
generation.generationId,
|
|
1002
|
+
),
|
|
1003
|
+
),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
failures.push({
|
|
1008
|
+
packageId,
|
|
1009
|
+
...(authoring
|
|
1010
|
+
? {
|
|
1011
|
+
authoring: {
|
|
1012
|
+
failureId: authoring.failureId,
|
|
1013
|
+
phase: authoring.phase,
|
|
1014
|
+
reason: authoring.reason,
|
|
1015
|
+
diagnostics: authoring.diagnostics,
|
|
1016
|
+
recordedAt: authoring.recordedAt,
|
|
1017
|
+
},
|
|
1018
|
+
}
|
|
1019
|
+
: {}),
|
|
1020
|
+
...(activation ? { activation } : {}),
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
contextContract: `${BOT_ISOLATE_CONTEXT_DTS_V1}\n\n${PACKAGE_IFRAME_BRIDGE_DTS_V1}\nInline ui.html helper:\n<script>${PACKAGE_IFRAME_HELPER_JS_V1}</script>`,
|
|
1025
|
+
composition: {
|
|
1026
|
+
generationId: composition.generationId,
|
|
1027
|
+
status: composition.status,
|
|
1028
|
+
members,
|
|
1029
|
+
},
|
|
1030
|
+
failures,
|
|
1031
|
+
};
|
|
509
1032
|
},
|
|
510
1033
|
};
|
|
511
1034
|
}
|
|
@@ -523,9 +1046,51 @@ export function createR2AuthoringArtifactStore(
|
|
|
523
1046
|
httpMetadata: { contentType: "application/javascript" },
|
|
524
1047
|
});
|
|
525
1048
|
},
|
|
1049
|
+
async putPackageUiArtifact(contentHash: string, html: string) {
|
|
1050
|
+
await bucket.put(`packages/${contentHash}.html`, html, {
|
|
1051
|
+
httpMetadata: { contentType: "text/html; charset=utf-8" },
|
|
1052
|
+
});
|
|
1053
|
+
},
|
|
1054
|
+
async putPackageSource(sourceHash: string, source: string) {
|
|
1055
|
+
await bucket.put(sourceR2KeyV1(sourceHash), source, {
|
|
1056
|
+
httpMetadata: { contentType: "text/typescript; charset=utf-8" },
|
|
1057
|
+
});
|
|
1058
|
+
},
|
|
1059
|
+
async loadPackageSource(sourceHash: string) {
|
|
1060
|
+
const object = await bucket.get(sourceR2KeyV1(sourceHash));
|
|
1061
|
+
if (!object) return undefined;
|
|
1062
|
+
const source = await object.text();
|
|
1063
|
+
if ((await sha256(source)) !== sourceHash) {
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
`Package source "${sourceHash}" failed hash verification`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
return source;
|
|
1069
|
+
},
|
|
526
1070
|
async headPackageArtifact(contentHash: string) {
|
|
527
1071
|
const object = await bucket.head(artifactR2KeyV1(contentHash));
|
|
528
1072
|
return object ? { contentHash, size: object.size } : undefined;
|
|
529
1073
|
},
|
|
530
1074
|
};
|
|
531
1075
|
}
|
|
1076
|
+
|
|
1077
|
+
/** Reads one Bot-authored member's retained source through its durable record. */
|
|
1078
|
+
export async function readAuthoredCompositionMemberSourceV1(input: {
|
|
1079
|
+
storage: Pick<AuthoringStorage, "get">;
|
|
1080
|
+
artifacts: Pick<AuthoringArtifactStore, "loadPackageSource">;
|
|
1081
|
+
member: CompositionMemberV1;
|
|
1082
|
+
}): Promise<string | undefined> {
|
|
1083
|
+
const { member } = input;
|
|
1084
|
+
if (member.provenance.kind !== "bot" || !member.artifact) return undefined;
|
|
1085
|
+
const artifact = await input.storage.get<AuthoredArtifactRecordV1>(
|
|
1086
|
+
artifactKey(member.artifact.contentHash),
|
|
1087
|
+
);
|
|
1088
|
+
if (
|
|
1089
|
+
!artifact ||
|
|
1090
|
+
artifact.contentHash !== member.artifact.contentHash ||
|
|
1091
|
+
artifact.manifestHash !== member.manifestHash
|
|
1092
|
+
) {
|
|
1093
|
+
return undefined;
|
|
1094
|
+
}
|
|
1095
|
+
return input.artifacts.loadPackageSource(artifact.sourceHash);
|
|
1096
|
+
}
|