@frockbot/plugin-shell 0.0.0 → 0.1.1
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 +68 -0
- package/package.json +87 -6
- package/src/agent.test.ts +372 -0
- package/src/agent.ts +335 -0
- package/src/approvals.test.ts +224 -0
- package/src/approvals.ts +530 -0
- package/src/backend-assignment.test.ts +161 -0
- package/src/backend-assignment.ts +274 -0
- package/src/backend-authoring.test.ts +518 -0
- package/src/backend-authoring.ts +531 -0
- package/src/backend-bot-identity.test.ts +215 -0
- package/src/backend-completion.test.ts +289 -0
- package/src/backend-completion.ts +95 -0
- package/src/backend-composition.ts +242 -0
- package/src/backend-computer.ts +76 -0
- package/src/backend-configuration.test.ts +1757 -0
- package/src/backend-contracts.test.ts +189 -0
- package/src/backend-contracts.ts +44 -0
- package/src/backend-debug.test.ts +202 -0
- package/src/backend-execution.ts +55 -0
- package/src/backend-flock.ts +96 -0
- package/src/backend-image.test.ts +115 -0
- package/src/backend-image.ts +180 -0
- package/src/backend-isolate.test.ts +238 -0
- package/src/backend-isolate.ts +409 -0
- package/src/backend-machine.ts +144 -0
- package/src/backend-memory.ts +89 -0
- package/src/backend-recovery-integration.test.ts +1575 -0
- package/src/backend-recovery.ts +106 -0
- package/src/backend-routines.ts +375 -0
- package/src/backend-runner.ts +251 -0
- package/src/backend-skills.test.ts +126 -0
- package/src/backend-skills.ts +198 -0
- package/src/backend-stop.test.ts +356 -0
- package/src/backend-subagents.ts +459 -0
- package/src/backend.ts +6035 -0
- package/src/client/FrockBotApp.vue +1026 -0
- package/src/client/SendPayloadView.vue +337 -0
- package/src/client/composer-draft.test.ts +31 -0
- package/src/client/composer-draft.ts +35 -0
- package/src/client/cordis-client-shim.d.ts +15 -0
- package/src/client/index.test.ts +2548 -0
- package/src/client/index.ts +2346 -0
- package/src/client/model-presentation.test.ts +35 -0
- package/src/client/model-presentation.ts +19 -0
- package/src/client/notify.test.ts +89 -0
- package/src/client/notify.ts +101 -0
- package/src/client/skill-invocation.test.ts +143 -0
- package/src/client/skill-invocation.ts +175 -0
- package/src/client/styles.css +1043 -0
- package/src/composition-views.ts +118 -0
- package/src/debug-protocol.test.ts +80 -0
- package/src/debug-protocol.ts +165 -0
- package/src/env.d.ts +10 -0
- package/src/history.test.ts +163 -0
- package/src/history.ts +108 -0
- package/src/host.ts +20 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +3 -0
- package/src/run-cursor.ts +28 -0
- package/src/run-protocol.test.ts +1281 -0
- package/src/run-protocol.ts +1417 -0
- package/src/settings-links.test.ts +106 -0
- package/src/settings-links.ts +289 -0
- package/src/shared.ts +338 -0
- package/src/skill-protocol.ts +117 -0
- package/src/terminal-records.test.ts +217 -0
- package/src/terminal-records.ts +150 -0
- package/src/unread.test.ts +362 -0
- package/src/unread.ts +675 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
// The Bot Durable Object half of Package authoring.
|
|
2
|
+
//
|
|
3
|
+
// The Authoring Package holds no authority: it decodes the model's input and
|
|
4
|
+
// records the session events. Everything durable happens here, in the Package
|
|
5
|
+
// that owns the Bot object's state, and in this order (constitution, Durable
|
|
6
|
+
// effects — intent before effect):
|
|
7
|
+
//
|
|
8
|
+
// reserve a per-User quota unit → write `authorship:intent:<effectId>` →
|
|
9
|
+
// call `PACKAGE_BUNDLER` → write `artifact:<contentHash>` and put the module
|
|
10
|
+
// to the Package artifact store → build the `provenance.kind: "bot"` member →
|
|
11
|
+
// propose the new Composition generation, pinned for the *next* Turn.
|
|
12
|
+
//
|
|
13
|
+
// The authoring Turn keeps running on the generation it was admitted under.
|
|
14
|
+
// Nothing here mutates a recorded generation or a recorded artifact: a
|
|
15
|
+
// re-authored Package appends a version and supersedes its predecessor inside
|
|
16
|
+
// a new generation.
|
|
17
|
+
import {
|
|
18
|
+
decodePackageBundleResultV1,
|
|
19
|
+
type PackageBundleRequestV1,
|
|
20
|
+
type PackageBundlerBinding,
|
|
21
|
+
} from "@frockbot/kernel-contracts";
|
|
22
|
+
import { canonicalJson, sha256 } from "@frockbot/kernel-composition/compiler";
|
|
23
|
+
import {
|
|
24
|
+
compositionArtifactSetHashV1,
|
|
25
|
+
compositionGenerationIdV1,
|
|
26
|
+
decodeCompositionGenerationV1,
|
|
27
|
+
type CompositionGenerationV1,
|
|
28
|
+
type CompositionMemberV1,
|
|
29
|
+
} from "@frockbot/kernel-composition/generation";
|
|
30
|
+
import {
|
|
31
|
+
artifactKey,
|
|
32
|
+
artifactR2KeyV1,
|
|
33
|
+
authoredManifestV1,
|
|
34
|
+
authoredSpecifierV1,
|
|
35
|
+
authoredVersionV1,
|
|
36
|
+
authoringEffectIdV1,
|
|
37
|
+
authoringQuotaDayV1,
|
|
38
|
+
authorshipArtifactKey,
|
|
39
|
+
authorshipFailureKey,
|
|
40
|
+
authorshipIntentKey,
|
|
41
|
+
authorshipPackageKey,
|
|
42
|
+
classifyAuthoringEffectV1,
|
|
43
|
+
type AuthoredArtifactRecordV1,
|
|
44
|
+
type AuthoredPackageRecordV1,
|
|
45
|
+
type AuthoringEffectOutcomeV1,
|
|
46
|
+
type AuthoringFailureRecordV1,
|
|
47
|
+
type AuthoringQuotaBinding,
|
|
48
|
+
type AuthorPackageOutcomeV1,
|
|
49
|
+
type AuthorPackageRequestV1,
|
|
50
|
+
type AuthorshipIntentV1,
|
|
51
|
+
type PackageAuthoringHost,
|
|
52
|
+
} from "@frockbot/plugin-authoring";
|
|
53
|
+
|
|
54
|
+
/** The narrow Bot Durable Object storage surface authoring needs. */
|
|
55
|
+
export interface AuthoringStorage {
|
|
56
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
57
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The Composition surface authoring needs; `DurableCompositionStore` satisfies it. */
|
|
61
|
+
export interface AuthoringCompositionStore {
|
|
62
|
+
current(): Promise<CompositionGenerationV1>;
|
|
63
|
+
read(generationId: string): Promise<CompositionGenerationV1 | undefined>;
|
|
64
|
+
propose(
|
|
65
|
+
generation: CompositionGenerationV1,
|
|
66
|
+
options?: { pin?: boolean },
|
|
67
|
+
): Promise<void>;
|
|
68
|
+
retainedCount(): Promise<number>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Immutable content, written once and addressed by hash. */
|
|
72
|
+
export interface AuthoringArtifactStore {
|
|
73
|
+
putPackageArtifact(contentHash: string, module: string): Promise<void>;
|
|
74
|
+
headPackageArtifact(
|
|
75
|
+
contentHash: string,
|
|
76
|
+
): Promise<{ contentHash: string; size: number } | undefined>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PackageAuthoringHostOptions {
|
|
80
|
+
storage: AuthoringStorage;
|
|
81
|
+
composition: AuthoringCompositionStore;
|
|
82
|
+
/** Absent when this host has no bundler; authoring then refuses visibly. */
|
|
83
|
+
bundler?: PackageBundlerBinding;
|
|
84
|
+
/** Absent when this host has no artifact store; authoring then refuses visibly. */
|
|
85
|
+
artifacts?: AuthoringArtifactStore;
|
|
86
|
+
quota: AuthoringQuotaBinding;
|
|
87
|
+
userId: string;
|
|
88
|
+
botId: string;
|
|
89
|
+
runId: string;
|
|
90
|
+
turnId: string;
|
|
91
|
+
compatibilityDate: string;
|
|
92
|
+
now?(): Date;
|
|
93
|
+
newId?(): string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function errorMessage(error: unknown): string {
|
|
97
|
+
return error instanceof Error ? error.message : String(error);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds the authoring seam one admitted Turn runs under. Constructed per
|
|
102
|
+
* Turn because provenance names the run and turn that produced the artifact.
|
|
103
|
+
*/
|
|
104
|
+
export function createPackageAuthoringHost(
|
|
105
|
+
options: PackageAuthoringHostOptions,
|
|
106
|
+
): PackageAuthoringHost {
|
|
107
|
+
const now = options.now ?? (() => new Date());
|
|
108
|
+
const newId = options.newId ?? (() => crypto.randomUUID());
|
|
109
|
+
|
|
110
|
+
async function recordFailure(input: {
|
|
111
|
+
effectId: string;
|
|
112
|
+
packageId: string;
|
|
113
|
+
phase: AuthoringFailureRecordV1["phase"];
|
|
114
|
+
reason: string;
|
|
115
|
+
diagnostics?: string[];
|
|
116
|
+
}): Promise<AuthoringFailureRecordV1> {
|
|
117
|
+
const failureId = `authoring-failure-${newId()}`;
|
|
118
|
+
const record: AuthoringFailureRecordV1 = {
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
failureId,
|
|
121
|
+
effectId: input.effectId,
|
|
122
|
+
botId: options.botId,
|
|
123
|
+
packageId: input.packageId,
|
|
124
|
+
runId: options.runId,
|
|
125
|
+
phase: input.phase,
|
|
126
|
+
reason: input.reason,
|
|
127
|
+
diagnostics: input.diagnostics ?? [],
|
|
128
|
+
recordedAt: now().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
await options.storage.put({ [authorshipFailureKey(failureId)]: record });
|
|
131
|
+
return record;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function refused(
|
|
135
|
+
record: AuthoringFailureRecordV1,
|
|
136
|
+
): Extract<AuthorPackageOutcomeV1, { status: "refused" }> {
|
|
137
|
+
return {
|
|
138
|
+
status: "refused",
|
|
139
|
+
reason: record.reason,
|
|
140
|
+
failureId: record.failureId,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The constitutional shadowing rule: a Bot authors only over its own
|
|
146
|
+
* Packages. A member of the current Composition whose provenance is
|
|
147
|
+
* first-party or User is not the Bot's to supersede, so authoring that
|
|
148
|
+
* `packageId` is refused before any durable effect. Superseding the Bot's
|
|
149
|
+
* own prior version is the ordinary re-authoring path and passes.
|
|
150
|
+
*/
|
|
151
|
+
async function shadowedMember(
|
|
152
|
+
packageId: string,
|
|
153
|
+
): Promise<CompositionMemberV1 | undefined> {
|
|
154
|
+
const parent = await options.composition.current();
|
|
155
|
+
return parent.members.find(
|
|
156
|
+
(member) =>
|
|
157
|
+
member.packageId === packageId && member.provenance.kind !== "bot",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The member set of the next generation: every member of the pinned-forward
|
|
163
|
+
* current generation except the one this Package supersedes, plus the new
|
|
164
|
+
* one. A recorded generation is never edited.
|
|
165
|
+
*/
|
|
166
|
+
async function nextGeneration(input: {
|
|
167
|
+
member: CompositionMemberV1;
|
|
168
|
+
createdAt: string;
|
|
169
|
+
sessionId: string;
|
|
170
|
+
}): Promise<{
|
|
171
|
+
generation: CompositionGenerationV1;
|
|
172
|
+
supersededVersion?: string;
|
|
173
|
+
}> {
|
|
174
|
+
const parent = await options.composition.current();
|
|
175
|
+
const superseded = parent.members.find(
|
|
176
|
+
(member) => member.packageId === input.member.packageId,
|
|
177
|
+
);
|
|
178
|
+
const members = [
|
|
179
|
+
...parent.members.filter(
|
|
180
|
+
(member) => member.packageId !== input.member.packageId,
|
|
181
|
+
),
|
|
182
|
+
input.member,
|
|
183
|
+
].sort((left, right) => left.packageId.localeCompare(right.packageId));
|
|
184
|
+
const artifactSetHash = await compositionArtifactSetHashV1(members);
|
|
185
|
+
const generation = decodeCompositionGenerationV1({
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
generationId: compositionGenerationIdV1(input.createdAt, artifactSetHash),
|
|
188
|
+
artifactSetHash,
|
|
189
|
+
parentGenerationId: parent.generationId,
|
|
190
|
+
createdAt: input.createdAt,
|
|
191
|
+
origin: {
|
|
192
|
+
kind: "bot-authored",
|
|
193
|
+
runId: options.runId,
|
|
194
|
+
sessionId: input.sessionId,
|
|
195
|
+
turnId: options.turnId,
|
|
196
|
+
},
|
|
197
|
+
members,
|
|
198
|
+
status: "pending",
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
generation,
|
|
202
|
+
...(superseded ? { supersededVersion: superseded.version } : {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function compose(input: {
|
|
207
|
+
request: AuthorPackageRequestV1;
|
|
208
|
+
outcome: Extract<AuthoringEffectOutcomeV1, { status: "bundled" }>;
|
|
209
|
+
artifact: AuthoredArtifactRecordV1;
|
|
210
|
+
}): Promise<AuthorPackageOutcomeV1> {
|
|
211
|
+
const { request, outcome, artifact } = input;
|
|
212
|
+
if (outcome.generationId) {
|
|
213
|
+
const recorded = await options.composition.read(outcome.generationId);
|
|
214
|
+
if (recorded) {
|
|
215
|
+
return {
|
|
216
|
+
status: "authored",
|
|
217
|
+
packageId: request.input.packageId,
|
|
218
|
+
version: outcome.version,
|
|
219
|
+
contentHash: outcome.contentHash,
|
|
220
|
+
generationId: recorded.generationId,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const manifest = authoredManifestV1({
|
|
225
|
+
packageId: request.input.packageId,
|
|
226
|
+
displayName: request.input.displayName,
|
|
227
|
+
version: outcome.version,
|
|
228
|
+
tool: request.input.tool,
|
|
229
|
+
...(request.input.model ? { model: request.input.model } : {}),
|
|
230
|
+
});
|
|
231
|
+
const member: CompositionMemberV1 = {
|
|
232
|
+
packageId: request.input.packageId,
|
|
233
|
+
specifier: authoredSpecifierV1(request.input.packageId),
|
|
234
|
+
version: outcome.version,
|
|
235
|
+
manifestHash: await sha256(canonicalJson(manifest)),
|
|
236
|
+
provenance: artifact.provenance,
|
|
237
|
+
artifact: {
|
|
238
|
+
contentHash: artifact.contentHash,
|
|
239
|
+
size: artifact.size,
|
|
240
|
+
mediaType: artifact.mediaType,
|
|
241
|
+
bundlerVersion: artifact.bundlerVersion,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const { generation, supersededVersion } = await nextGeneration({
|
|
245
|
+
member,
|
|
246
|
+
createdAt: artifact.provenance.authoredAt,
|
|
247
|
+
sessionId: request.sessionId,
|
|
248
|
+
});
|
|
249
|
+
if (!(await options.composition.read(generation.generationId))) {
|
|
250
|
+
await options.composition.propose(generation, { pin: true });
|
|
251
|
+
}
|
|
252
|
+
await options.storage.put({
|
|
253
|
+
[authorshipArtifactKey(request.effectId)]: {
|
|
254
|
+
...outcome,
|
|
255
|
+
generationId: generation.generationId,
|
|
256
|
+
} satisfies AuthoringEffectOutcomeV1,
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
status: "authored",
|
|
260
|
+
packageId: request.input.packageId,
|
|
261
|
+
version: outcome.version,
|
|
262
|
+
contentHash: outcome.contentHash,
|
|
263
|
+
generationId: generation.generationId,
|
|
264
|
+
...(supersededVersion ? { supersededVersion } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
effectIdFor: (input) =>
|
|
270
|
+
authoringEffectIdV1({
|
|
271
|
+
runId: options.runId,
|
|
272
|
+
packageId: input.packageId,
|
|
273
|
+
sourceHash: input.sourceHash,
|
|
274
|
+
}),
|
|
275
|
+
|
|
276
|
+
async author(
|
|
277
|
+
request: AuthorPackageRequestV1,
|
|
278
|
+
): Promise<AuthorPackageOutcomeV1> {
|
|
279
|
+
const { effectId } = request;
|
|
280
|
+
const packageId = request.input.packageId;
|
|
281
|
+
|
|
282
|
+
// Ahead of every other path, including a settled effect being composed
|
|
283
|
+
// after recovery: a shadowed Package never reaches a generation.
|
|
284
|
+
const shadowed = await shadowedMember(packageId);
|
|
285
|
+
if (shadowed) {
|
|
286
|
+
return refused(
|
|
287
|
+
await recordFailure({
|
|
288
|
+
effectId,
|
|
289
|
+
packageId,
|
|
290
|
+
phase: "compose",
|
|
291
|
+
reason: `Package "${packageId}" is already in this Bot's Composition with ${shadowed.provenance.kind} provenance, and a Bot may author only over its own Packages`,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const intent = await options.storage.get<AuthorshipIntentV1>(
|
|
297
|
+
authorshipIntentKey(effectId),
|
|
298
|
+
);
|
|
299
|
+
const recordedOutcome =
|
|
300
|
+
await options.storage.get<AuthoringEffectOutcomeV1>(
|
|
301
|
+
authorshipArtifactKey(effectId),
|
|
302
|
+
);
|
|
303
|
+
const classification = classifyAuthoringEffectV1({
|
|
304
|
+
intent,
|
|
305
|
+
outcome: recordedOutcome,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (classification.kind === "unknown") {
|
|
309
|
+
// The bundler may have run. Nothing durable says so, and a blind
|
|
310
|
+
// re-bundle would duplicate the effect, so it is reported instead.
|
|
311
|
+
return refused(
|
|
312
|
+
await recordFailure({
|
|
313
|
+
effectId,
|
|
314
|
+
packageId,
|
|
315
|
+
phase: "recovery",
|
|
316
|
+
reason: classification.reason,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (classification.kind === "failed") {
|
|
321
|
+
return {
|
|
322
|
+
status: "refused",
|
|
323
|
+
reason: classification.reason,
|
|
324
|
+
failureId: classification.failureId,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (classification.kind === "settled") {
|
|
328
|
+
const artifact = await options.storage.get<AuthoredArtifactRecordV1>(
|
|
329
|
+
artifactKey(classification.outcome.contentHash),
|
|
330
|
+
);
|
|
331
|
+
if (!artifact) {
|
|
332
|
+
return refused(
|
|
333
|
+
await recordFailure({
|
|
334
|
+
effectId,
|
|
335
|
+
packageId,
|
|
336
|
+
phase: "recovery",
|
|
337
|
+
reason: `authoring effect "${effectId}" names artifact "${classification.outcome.contentHash}", which has no durable record`,
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return await compose({
|
|
342
|
+
request,
|
|
343
|
+
outcome: classification.outcome,
|
|
344
|
+
artifact,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!options.bundler || !options.artifacts) {
|
|
349
|
+
return refused(
|
|
350
|
+
await recordFailure({
|
|
351
|
+
effectId,
|
|
352
|
+
packageId,
|
|
353
|
+
phase: "compose",
|
|
354
|
+
reason:
|
|
355
|
+
"this host cannot author Packages: it has no Package bundler or artifact store",
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const previous = await options.storage.get<AuthoredPackageRecordV1>(
|
|
361
|
+
authorshipPackageKey(packageId),
|
|
362
|
+
);
|
|
363
|
+
const ordinal = (previous?.ordinal ?? 0) + 1;
|
|
364
|
+
const version = authoredVersionV1(ordinal);
|
|
365
|
+
const sourceBytes = new TextEncoder().encode(
|
|
366
|
+
request.input.source,
|
|
367
|
+
).byteLength;
|
|
368
|
+
const recordedAt = now().toISOString();
|
|
369
|
+
|
|
370
|
+
// The User Durable Object is the authority for User-scoped quotas, and
|
|
371
|
+
// it is asked before any durable intent is written here.
|
|
372
|
+
const receipt = await options.quota.reserve({
|
|
373
|
+
schemaVersion: 1,
|
|
374
|
+
userId: options.userId,
|
|
375
|
+
botId: options.botId,
|
|
376
|
+
effectId,
|
|
377
|
+
day: authoringQuotaDayV1(new Date(recordedAt)),
|
|
378
|
+
sourceBytes,
|
|
379
|
+
retainedGenerations: await options.composition.retainedCount(),
|
|
380
|
+
});
|
|
381
|
+
if (receipt.status === "refused") {
|
|
382
|
+
return refused(
|
|
383
|
+
await recordFailure({
|
|
384
|
+
effectId,
|
|
385
|
+
packageId,
|
|
386
|
+
phase: "quota",
|
|
387
|
+
reason: `a durable per-User quota refused this Package: ${receipt.reason}`,
|
|
388
|
+
diagnostics: [
|
|
389
|
+
`limit=${receipt.limitName}`,
|
|
390
|
+
`used=${receipt.used}`,
|
|
391
|
+
`allowed=${receipt.limit}`,
|
|
392
|
+
],
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const recordedIntent: AuthorshipIntentV1 = {
|
|
398
|
+
schemaVersion: 1,
|
|
399
|
+
effectId,
|
|
400
|
+
botId: options.botId,
|
|
401
|
+
sessionId: request.sessionId,
|
|
402
|
+
runId: options.runId,
|
|
403
|
+
turnId: options.turnId,
|
|
404
|
+
packageId,
|
|
405
|
+
version,
|
|
406
|
+
sourceHash: request.sourceHash,
|
|
407
|
+
sourceBytes,
|
|
408
|
+
recordedAt,
|
|
409
|
+
status: "recorded",
|
|
410
|
+
};
|
|
411
|
+
await options.storage.put({
|
|
412
|
+
[authorshipIntentKey(effectId)]: recordedIntent,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const bundleRequest: PackageBundleRequestV1 = {
|
|
416
|
+
schemaVersion: 1,
|
|
417
|
+
effectId,
|
|
418
|
+
target: "bot-isolate",
|
|
419
|
+
compatibilityDate: options.compatibilityDate,
|
|
420
|
+
entry: "package.ts",
|
|
421
|
+
sources: [{ path: "package.ts", text: request.input.source }],
|
|
422
|
+
};
|
|
423
|
+
let bundled;
|
|
424
|
+
try {
|
|
425
|
+
// Never inside a storage transaction: bundler CPU is the bundler's.
|
|
426
|
+
bundled = decodePackageBundleResultV1(
|
|
427
|
+
await options.bundler.bundle(bundleRequest),
|
|
428
|
+
);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return refused(
|
|
431
|
+
await recordFailure({
|
|
432
|
+
effectId,
|
|
433
|
+
packageId,
|
|
434
|
+
phase: "bundle",
|
|
435
|
+
reason: `the Package bundler could not be reached: ${errorMessage(error)}`,
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (bundled.status === "failed") {
|
|
440
|
+
const failure = await recordFailure({
|
|
441
|
+
effectId,
|
|
442
|
+
packageId,
|
|
443
|
+
phase: "bundle",
|
|
444
|
+
reason: `the Package bundler rejected this source: ${bundled.failure}`,
|
|
445
|
+
diagnostics: bundled.diagnostics,
|
|
446
|
+
});
|
|
447
|
+
// A deterministic refusal is itself the effect's outcome: a replay
|
|
448
|
+
// must reproduce it rather than classify the effect unknown.
|
|
449
|
+
await options.storage.put({
|
|
450
|
+
[authorshipArtifactKey(effectId)]: {
|
|
451
|
+
schemaVersion: 1,
|
|
452
|
+
status: "failed",
|
|
453
|
+
effectId,
|
|
454
|
+
failureId: failure.failureId,
|
|
455
|
+
reason: failure.reason,
|
|
456
|
+
} satisfies AuthoringEffectOutcomeV1,
|
|
457
|
+
});
|
|
458
|
+
return refused(failure);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const { artifact } = bundled;
|
|
462
|
+
// Content-addressed and immutable: writing the same hash twice is a
|
|
463
|
+
// no-op, so the object write is safe to repeat and the record is not.
|
|
464
|
+
await options.artifacts.putPackageArtifact(
|
|
465
|
+
artifact.contentHash,
|
|
466
|
+
bundled.module,
|
|
467
|
+
);
|
|
468
|
+
const artifactRecord: AuthoredArtifactRecordV1 = {
|
|
469
|
+
schemaVersion: 1,
|
|
470
|
+
contentHash: artifact.contentHash,
|
|
471
|
+
size: artifact.size,
|
|
472
|
+
mediaType: artifact.mediaType,
|
|
473
|
+
bundlerVersion: artifact.bundlerVersion,
|
|
474
|
+
effectId,
|
|
475
|
+
r2Key: artifactR2KeyV1(artifact.contentHash),
|
|
476
|
+
provenance: {
|
|
477
|
+
kind: "bot",
|
|
478
|
+
packageId,
|
|
479
|
+
version,
|
|
480
|
+
botId: options.botId,
|
|
481
|
+
sessionId: request.sessionId,
|
|
482
|
+
turnId: options.turnId,
|
|
483
|
+
runId: options.runId,
|
|
484
|
+
authoredAt: recordedAt,
|
|
485
|
+
},
|
|
486
|
+
createdAt: recordedAt,
|
|
487
|
+
};
|
|
488
|
+
const outcome: Extract<AuthoringEffectOutcomeV1, { status: "bundled" }> =
|
|
489
|
+
{
|
|
490
|
+
schemaVersion: 1,
|
|
491
|
+
status: "bundled",
|
|
492
|
+
effectId,
|
|
493
|
+
contentHash: artifact.contentHash,
|
|
494
|
+
version,
|
|
495
|
+
};
|
|
496
|
+
await options.storage.put({
|
|
497
|
+
[artifactKey(artifact.contentHash)]: artifactRecord,
|
|
498
|
+
[authorshipArtifactKey(effectId)]: outcome,
|
|
499
|
+
[authorshipPackageKey(packageId)]: {
|
|
500
|
+
schemaVersion: 1,
|
|
501
|
+
packageId,
|
|
502
|
+
ordinal,
|
|
503
|
+
version,
|
|
504
|
+
contentHash: artifact.contentHash,
|
|
505
|
+
updatedAt: recordedAt,
|
|
506
|
+
} satisfies AuthoredPackageRecordV1,
|
|
507
|
+
});
|
|
508
|
+
return await compose({ request, outcome, artifact: artifactRecord });
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The Package artifact store over the `APPLICATION_ARTIFACTS` bucket. Writes
|
|
515
|
+
* are content-addressed, so repeating one is a no-op rather than a mutation.
|
|
516
|
+
*/
|
|
517
|
+
export function createR2AuthoringArtifactStore(
|
|
518
|
+
bucket: R2Bucket,
|
|
519
|
+
): AuthoringArtifactStore {
|
|
520
|
+
return {
|
|
521
|
+
async putPackageArtifact(contentHash: string, module: string) {
|
|
522
|
+
await bucket.put(artifactR2KeyV1(contentHash), module, {
|
|
523
|
+
httpMetadata: { contentType: "application/javascript" },
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
async headPackageArtifact(contentHash: string) {
|
|
527
|
+
const object = await bucket.head(artifactR2KeyV1(contentHash));
|
|
528
|
+
return object ? { contentHash, size: object.size } : undefined;
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|