@frockbot/plugin-shell 0.1.3 → 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 +23 -5
- package/src/backend-configuration.test.ts +549 -1468
- package/src/backend-contracts.test.ts +28 -0
- package/src/backend-contracts.ts +3 -1
- package/src/backend-execution.ts +0 -4
- package/src/backend-iframe-ui.test.ts +131 -0
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +230 -89
- package/src/backend-isolate.ts +103 -200
- package/src/backend-package-catalog.test.ts +451 -0
- package/src/backend-package-catalog.ts +923 -0
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend-runner-iframe.test.ts +74 -0
- package/src/backend-runner.ts +192 -0
- package/src/backend.ts +1198 -1781
- package/src/client/FrockBotApp.vue +44 -73
- 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 +439 -380
- package/src/client/index.ts +163 -257
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/package-iframe-host-message.test.ts +41 -0
- package/src/client/package-iframe-host-message.ts +27 -0
- package/src/client/styles.css +0 -27
- package/src/composition-views.ts +54 -0
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +10 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/backend-isolate.ts
CHANGED
|
@@ -1,116 +1,61 @@
|
|
|
1
1
|
// The Bot Durable Object half of the isolate capability boundary.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// rather than a grant, and a model call records its normalized request and
|
|
8
|
-
// acquires its credential lease through the existing provider path before a
|
|
9
|
-
// single byte leaves the account.
|
|
3
|
+
// Every Package mounted for one Bot receives the same authority projection:
|
|
4
|
+
// the User's ready Connections, the Bot's configured model, and the shared
|
|
5
|
+
// tool, Memory, Workspace, and notification surfaces. Package identity remains
|
|
6
|
+
// in props only for attribution; it never narrows the authority list.
|
|
10
7
|
import type {
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
IsolateCapabilityListV1,
|
|
9
|
+
IsolateConnectionLeaseV1,
|
|
10
|
+
IsolateConnectionOutcomeV1,
|
|
11
|
+
IsolateConnectionV1,
|
|
12
|
+
IsolateModelBindingV1,
|
|
13
13
|
IsolateModelInvocationV1,
|
|
14
|
-
IsolatePendingDecisionV1,
|
|
15
14
|
LlmStreamEvent,
|
|
16
15
|
NormalizedModelRequest,
|
|
17
16
|
} from "@frockbot/kernel-contracts";
|
|
18
|
-
import {
|
|
19
|
-
decodeIsolateAuthorityRequestV1,
|
|
20
|
-
encodeIsolateModelEventLineV1,
|
|
21
|
-
} from "@frockbot/kernel-contracts";
|
|
17
|
+
import { encodeIsolateModelEventLineV1 } from "@frockbot/kernel-contracts";
|
|
22
18
|
import type { BotIsolateArtifactStore } from "@frockbot/kernel-composition/isolate";
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* gateway Worker's own so Bot-authored code cannot outrun the kernel wrapper.
|
|
27
|
-
*/
|
|
20
|
+
export type { IsolateModelBindingV1 } from "@frockbot/kernel-contracts";
|
|
21
|
+
|
|
28
22
|
export const BOT_ISOLATE_COMPATIBILITY_DATE = "2026-08-27";
|
|
29
23
|
|
|
30
|
-
/**
|
|
31
|
-
* What travels in `ctx.exports.BotCapabilities({ props })`. Structured-clonable
|
|
32
|
-
* by necessity: props cross into a loopback service binding.
|
|
33
|
-
*/
|
|
34
24
|
export interface BotCapabilitiesPropsV1 {
|
|
35
25
|
userId: string;
|
|
36
26
|
botId: string;
|
|
27
|
+
runId: string;
|
|
28
|
+
sessionId: string;
|
|
29
|
+
turnId: string;
|
|
37
30
|
generationId: string;
|
|
38
31
|
packageId: string;
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
connections: IsolateConnectionV1[];
|
|
33
|
+
model?: IsolateModelBindingV1;
|
|
34
|
+
memory: boolean;
|
|
35
|
+
workspace: boolean;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
export const ISOLATE_DECISION_PREFIX = "isolate:decision:";
|
|
44
38
|
export const ISOLATE_MODEL_REQUEST_PREFIX = "isolate:model-request:";
|
|
45
39
|
|
|
46
|
-
/** A durable record that the Bot asked for authority it does not hold. */
|
|
47
|
-
export interface IsolatePendingAuthorityDecisionV1 {
|
|
48
|
-
schemaVersion: 1;
|
|
49
|
-
decisionId: string;
|
|
50
|
-
botId: string;
|
|
51
|
-
packageId: string;
|
|
52
|
-
generationId: string;
|
|
53
|
-
capabilityId: string;
|
|
54
|
-
reason: string;
|
|
55
|
-
requestedAt: string;
|
|
56
|
-
status: "pending";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** The intent recorded before a Bot-authored adapter's model call is forwarded. */
|
|
60
40
|
export interface IsolateModelRequestRecordV1 {
|
|
61
41
|
schemaVersion: 1;
|
|
62
|
-
/** Minted by this Durable Object; the record is keyed by it. */
|
|
63
42
|
recordId: string;
|
|
64
|
-
/** The Bot's own correlation id. Bounded, and never a storage key. */
|
|
65
43
|
requestId: string;
|
|
66
44
|
botId: string;
|
|
67
45
|
packageId: string;
|
|
68
46
|
generationId: string;
|
|
69
|
-
capabilityId: string;
|
|
70
47
|
request: NormalizedModelRequest;
|
|
71
48
|
recordedAt: string;
|
|
72
49
|
}
|
|
73
50
|
|
|
74
|
-
/** The narrow storage surface this module needs from the Durable Object. */
|
|
75
51
|
export interface IsolateCapabilityStore {
|
|
76
52
|
put(key: string, value: unknown): Promise<void>;
|
|
77
|
-
get<T>(key: string): Promise<T | undefined>;
|
|
78
53
|
list<T>(options: { prefix: string }): Promise<Map<string, T>>;
|
|
79
54
|
}
|
|
80
55
|
|
|
81
|
-
/** One enabled Assignment, already resolved the way `plugin-shell` resolves them. */
|
|
82
|
-
export interface IsolateAssignmentV1 {
|
|
83
|
-
assignmentId: string;
|
|
84
|
-
packageId: string;
|
|
85
|
-
capabilityId: string;
|
|
86
|
-
kind: IsolateCapabilityDescriptorV1["kind"];
|
|
87
|
-
connectionId?: string;
|
|
88
|
-
providerModelId?: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The Bot's durable model binding, resolved by the authority from the Bot's
|
|
93
|
-
* own configuration and the User's Connection — never from anything the Bot
|
|
94
|
-
* supplied. An `invokeModel` request is authorized only when it names exactly
|
|
95
|
-
* this provider and this model, and it is forwarded carrying exactly this
|
|
96
|
-
* binding.
|
|
97
|
-
*/
|
|
98
|
-
export interface IsolateModelBindingV1 {
|
|
99
|
-
assignmentId: string;
|
|
100
|
-
packageId: string;
|
|
101
|
-
capabilityId: string;
|
|
102
|
-
connectionId: string;
|
|
103
|
-
provider: string;
|
|
104
|
-
providerModelId: string;
|
|
105
|
-
connectionGeneration?: string;
|
|
106
|
-
catalogGeneration?: string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** The bound Bot-supplied correlation id: a field, never a key, and never unbounded. */
|
|
110
56
|
export const MAX_ISOLATE_REQUEST_ID = 256;
|
|
111
57
|
|
|
112
58
|
export interface IsolateModelPath {
|
|
113
|
-
/** Streams through the mounted provider Plugin; the lease is taken inside it. */
|
|
114
59
|
stream(
|
|
115
60
|
request: NormalizedModelRequest,
|
|
116
61
|
signal: AbortSignal,
|
|
@@ -122,57 +67,56 @@ export interface IsolateCapabilityHostOptions {
|
|
|
122
67
|
botId: string;
|
|
123
68
|
packageId: string;
|
|
124
69
|
generationId: string;
|
|
125
|
-
|
|
126
|
-
assignments: readonly IsolateAssignmentV1[];
|
|
127
|
-
/**
|
|
128
|
-
* The one model binding this Bot durably holds, or absent when it holds
|
|
129
|
-
* none. Absent means every model request is a pending decision.
|
|
130
|
-
*/
|
|
70
|
+
connections: readonly IsolateConnectionV1[];
|
|
131
71
|
modelBinding?: IsolateModelBindingV1;
|
|
132
|
-
/** Absent when the Bot has no enabled model Assignment at all. */
|
|
133
72
|
modelPath?: IsolateModelPath;
|
|
73
|
+
memory: boolean;
|
|
74
|
+
workspace: boolean;
|
|
134
75
|
now?(): Date;
|
|
135
76
|
newId?(): string;
|
|
136
77
|
}
|
|
137
78
|
|
|
138
79
|
export interface IsolateCapabilityHost {
|
|
139
|
-
list(): Promise<
|
|
140
|
-
requestAuthority(request: unknown): Promise<IsolatePendingDecisionV1>;
|
|
80
|
+
list(): Promise<IsolateCapabilityListV1>;
|
|
141
81
|
invokeModel(
|
|
142
82
|
request: NormalizedModelRequest,
|
|
143
83
|
): Promise<IsolateModelInvocationV1>;
|
|
144
|
-
pendingDecisions(): Promise<IsolatePendingAuthorityDecisionV1[]>;
|
|
145
84
|
recordedModelRequests(): Promise<IsolateModelRequestRecordV1[]>;
|
|
146
85
|
}
|
|
147
86
|
|
|
148
|
-
|
|
149
|
-
* The enabled model Assignment that can serve this request, if any.
|
|
150
|
-
*
|
|
151
|
-
* An Assignment authorizes exactly one Package, one Connection, and one
|
|
152
|
-
* provider model: the Bot's durable binding. A request naming any other
|
|
153
|
-
* provider or model resolves to nothing, whatever the Bot claims about it —
|
|
154
|
-
* the Bot-supplied `modelBinding` is never read here or anywhere downstream.
|
|
155
|
-
*/
|
|
156
|
-
export function matchingModelAssignmentV1(
|
|
157
|
-
assignments: readonly IsolateAssignmentV1[],
|
|
87
|
+
export function matchingModelBindingV1(
|
|
158
88
|
binding: IsolateModelBindingV1 | undefined,
|
|
159
89
|
request: NormalizedModelRequest,
|
|
160
|
-
):
|
|
161
|
-
|
|
90
|
+
): IsolateModelBindingV1 | undefined {
|
|
91
|
+
const admitted = request.modelBinding;
|
|
162
92
|
if (
|
|
93
|
+
!binding ||
|
|
94
|
+
!admitted ||
|
|
163
95
|
request.provider !== binding.provider ||
|
|
164
|
-
request.model !== binding.providerModelId
|
|
96
|
+
request.model !== binding.providerModelId ||
|
|
97
|
+
admitted.connectionId !== binding.connectionId ||
|
|
98
|
+
admitted.connectionGeneration !== binding.connectionGeneration ||
|
|
99
|
+
admitted.catalogGeneration !== binding.catalogGeneration
|
|
165
100
|
) {
|
|
166
101
|
return undefined;
|
|
167
102
|
}
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
103
|
+
return binding;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A loaded isolate may receive only the Connection generation baked into its
|
|
108
|
+
* admitted authority snapshot. A later User Connection change gets a new
|
|
109
|
+
* binding digest and isolate identity; it must not leak through this old stub.
|
|
110
|
+
*/
|
|
111
|
+
export function matchesAdmittedConnectionV1(
|
|
112
|
+
admitted: IsolateConnectionV1 | undefined,
|
|
113
|
+
outcome: IsolateConnectionOutcomeV1,
|
|
114
|
+
): outcome is IsolateConnectionLeaseV1 {
|
|
115
|
+
return (
|
|
116
|
+
admitted !== undefined &&
|
|
117
|
+
outcome.status === "available" &&
|
|
118
|
+
outcome.connectionId === admitted.connectionId &&
|
|
119
|
+
outcome.generation === admitted.generation
|
|
176
120
|
);
|
|
177
121
|
}
|
|
178
122
|
|
|
@@ -182,47 +126,20 @@ export function createIsolateCapabilityHost(
|
|
|
182
126
|
const now = options.now ?? (() => new Date());
|
|
183
127
|
const newId = options.newId ?? (() => crypto.randomUUID());
|
|
184
128
|
|
|
185
|
-
async function recordDecision(
|
|
186
|
-
capabilityId: string,
|
|
187
|
-
reason: string,
|
|
188
|
-
): Promise<IsolatePendingDecisionV1> {
|
|
189
|
-
const decisionId = `decision-${newId()}`;
|
|
190
|
-
const record: IsolatePendingAuthorityDecisionV1 = {
|
|
191
|
-
schemaVersion: 1,
|
|
192
|
-
decisionId,
|
|
193
|
-
botId: options.botId,
|
|
194
|
-
packageId: options.packageId,
|
|
195
|
-
generationId: options.generationId,
|
|
196
|
-
capabilityId,
|
|
197
|
-
reason,
|
|
198
|
-
requestedAt: now().toISOString(),
|
|
199
|
-
status: "pending",
|
|
200
|
-
};
|
|
201
|
-
await options.storage.put(
|
|
202
|
-
`${ISOLATE_DECISION_PREFIX}${decisionId}`,
|
|
203
|
-
record,
|
|
204
|
-
);
|
|
205
|
-
return { status: "pending-user-decision", decisionId };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
129
|
return {
|
|
209
|
-
list(): Promise<
|
|
210
|
-
return Promise.resolve(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
decodeIsolateAuthorityRequestV1(request);
|
|
223
|
-
// Self-modification never widens authority, even when the capability is
|
|
224
|
-
// already assigned: the answer is a decision the User makes.
|
|
225
|
-
return await recordDecision(decoded.capabilityId, decoded.reason);
|
|
130
|
+
list(): Promise<IsolateCapabilityListV1> {
|
|
131
|
+
return Promise.resolve({
|
|
132
|
+
status: "available",
|
|
133
|
+
connections: structuredClone([...options.connections]),
|
|
134
|
+
...(options.modelBinding
|
|
135
|
+
? { model: structuredClone(options.modelBinding) }
|
|
136
|
+
: {}),
|
|
137
|
+
tools: true,
|
|
138
|
+
memory: options.memory,
|
|
139
|
+
workspace: options.workspace,
|
|
140
|
+
notify: true,
|
|
141
|
+
schedule: true,
|
|
142
|
+
});
|
|
226
143
|
},
|
|
227
144
|
|
|
228
145
|
async invokeModel(
|
|
@@ -231,36 +148,25 @@ export function createIsolateCapabilityHost(
|
|
|
231
148
|
if (request.requestId.length > MAX_ISOLATE_REQUEST_ID) {
|
|
232
149
|
throw new Error("isolate model request requestId is not bounded");
|
|
233
150
|
}
|
|
234
|
-
const binding = options.modelBinding;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`models:${request.provider}:${request.model}`,
|
|
243
|
-
`Bot Package "${options.packageId}" asked to invoke a model with no matching enabled Assignment`,
|
|
244
|
-
);
|
|
151
|
+
const binding = matchingModelBindingV1(options.modelBinding, request);
|
|
152
|
+
if (!binding || !options.modelPath) {
|
|
153
|
+
return {
|
|
154
|
+
status: "unavailable",
|
|
155
|
+
reason: options.modelBinding
|
|
156
|
+
? "the request does not match this Bot's configured model"
|
|
157
|
+
: "this Bot has no configured model",
|
|
158
|
+
};
|
|
245
159
|
}
|
|
246
|
-
// The binding the provider path receives is the authority's, never the
|
|
247
|
-
// Bot's: a Bot-composed request carries no Connection authority.
|
|
248
160
|
const forwarded: NormalizedModelRequest = {
|
|
249
161
|
...structuredClone(request),
|
|
250
162
|
modelBinding: {
|
|
251
163
|
connectionId: binding.connectionId,
|
|
252
|
-
|
|
253
|
-
? { connectionGeneration: binding.connectionGeneration }
|
|
254
|
-
: {}),
|
|
164
|
+
connectionGeneration: binding.connectionGeneration,
|
|
255
165
|
...(binding.catalogGeneration
|
|
256
166
|
? { catalogGeneration: binding.catalogGeneration }
|
|
257
167
|
: {}),
|
|
258
168
|
},
|
|
259
169
|
};
|
|
260
|
-
// Record the exact normalized request before forwarding; the provider
|
|
261
|
-
// path takes the credential lease on the way through. The record is
|
|
262
|
-
// keyed by an id this authority mints, so a Bot cannot overwrite one of
|
|
263
|
-
// its own earlier records by reusing a `requestId`.
|
|
264
170
|
const recordId = `model-request-${newId()}`;
|
|
265
171
|
const record: IsolateModelRequestRecordV1 = {
|
|
266
172
|
schemaVersion: 1,
|
|
@@ -269,7 +175,6 @@ export function createIsolateCapabilityHost(
|
|
|
269
175
|
botId: options.botId,
|
|
270
176
|
packageId: options.packageId,
|
|
271
177
|
generationId: options.generationId,
|
|
272
|
-
capabilityId: assignment.capabilityId,
|
|
273
178
|
request: forwarded,
|
|
274
179
|
recordedAt: now().toISOString(),
|
|
275
180
|
};
|
|
@@ -278,22 +183,16 @@ export function createIsolateCapabilityHost(
|
|
|
278
183
|
record,
|
|
279
184
|
);
|
|
280
185
|
const controller = new AbortController();
|
|
281
|
-
const events = options.modelPath.stream(forwarded, controller.signal);
|
|
282
186
|
return {
|
|
283
187
|
status: "streaming",
|
|
284
188
|
requestId: request.requestId,
|
|
285
|
-
events: isolateModelEventStreamV1(
|
|
189
|
+
events: isolateModelEventStreamV1(
|
|
190
|
+
options.modelPath.stream(forwarded, controller.signal),
|
|
191
|
+
controller,
|
|
192
|
+
),
|
|
286
193
|
};
|
|
287
194
|
},
|
|
288
195
|
|
|
289
|
-
async pendingDecisions(): Promise<IsolatePendingAuthorityDecisionV1[]> {
|
|
290
|
-
const stored =
|
|
291
|
-
await options.storage.list<IsolatePendingAuthorityDecisionV1>({
|
|
292
|
-
prefix: ISOLATE_DECISION_PREFIX,
|
|
293
|
-
});
|
|
294
|
-
return [...stored.values()];
|
|
295
|
-
},
|
|
296
|
-
|
|
297
196
|
async recordedModelRequests(): Promise<IsolateModelRequestRecordV1[]> {
|
|
298
197
|
const stored = await options.storage.list<IsolateModelRequestRecordV1>({
|
|
299
198
|
prefix: ISOLATE_MODEL_REQUEST_PREFIX,
|
|
@@ -346,31 +245,35 @@ export function isolateModelEventStreamV1(
|
|
|
346
245
|
}
|
|
347
246
|
|
|
348
247
|
/**
|
|
349
|
-
* The content address of the bindings
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* Both are bindings the isolate was granted, so both belong in this digest and
|
|
356
|
-
* the loader id stays derived from the artifact set and the binding digest
|
|
357
|
-
* alone.
|
|
248
|
+
* The content address of the bindings baked into a loaded isolate's env.
|
|
249
|
+
*
|
|
250
|
+
* Connection order is irrelevant; ids and generations are the authority
|
|
251
|
+
* identity. User, Bot, the resolved model binding, and the pinned Composition
|
|
252
|
+
* generation complete the digest. Package id is deliberately absent: two
|
|
253
|
+
* Packages of one Bot receive the same authority projection.
|
|
358
254
|
*/
|
|
359
|
-
export async function isolateBindingDigestV1(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
255
|
+
export async function isolateBindingDigestV1(input: {
|
|
256
|
+
userId: string;
|
|
257
|
+
botId: string;
|
|
258
|
+
connections: readonly Pick<
|
|
259
|
+
IsolateConnectionV1,
|
|
260
|
+
"connectionId" | "generation"
|
|
261
|
+
>[];
|
|
262
|
+
model?: IsolateModelBindingV1;
|
|
263
|
+
compositionGenerationId: string;
|
|
264
|
+
}): Promise<string> {
|
|
265
|
+
const connections = [...input.connections]
|
|
266
|
+
.map(({ connectionId, generation }) => ({ connectionId, generation }))
|
|
267
|
+
.sort((left, right) => left.connectionId.localeCompare(right.connectionId));
|
|
268
|
+
return await sha256Hex(
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
userId: input.userId,
|
|
271
|
+
botId: input.botId,
|
|
272
|
+
compositionGenerationId: input.compositionGenerationId,
|
|
273
|
+
connections,
|
|
274
|
+
model: input.model ?? null,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
374
277
|
}
|
|
375
278
|
|
|
376
279
|
async function sha256Hex(value: string): Promise<string> {
|