@frockbot/plugin-shell 0.1.3 → 0.1.4
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/package.json +28 -28
- package/src/backend-composition.ts +4 -4
- package/src/backend-configuration.test.ts +366 -1451
- package/src/backend-execution.ts +0 -4
- package/src/backend-image.test.ts +8 -8
- package/src/backend-image.ts +13 -13
- package/src/backend-isolate.test.ts +181 -35
- package/src/backend-isolate.ts +110 -72
- package/src/backend-recovery-integration.test.ts +17 -67
- package/src/backend-routines.ts +1 -1
- package/src/backend.ts +335 -1659
- package/src/client/FrockBotApp.vue +6 -73
- package/src/client/index.test.ts +320 -380
- package/src/client/index.ts +85 -256
- package/src/client/model-presentation.test.ts +21 -8
- package/src/client/model-presentation.ts +14 -7
- package/src/client/styles.css +0 -27
- package/src/settings-links.test.ts +2 -10
- package/src/settings-links.ts +1 -13
- package/src/shared.ts +0 -13
- package/src/backend-assignment.test.ts +0 -161
- package/src/backend-assignment.ts +0 -274
package/src/backend-isolate.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// acquires its credential lease through the existing provider path before a
|
|
9
9
|
// single byte leaves the account.
|
|
10
10
|
import type {
|
|
11
|
+
IsolateCapabilityFailureV1,
|
|
11
12
|
IsolateAuthorityRequestV1,
|
|
12
13
|
IsolateCapabilityDescriptorV1,
|
|
13
|
-
|
|
14
|
+
IsolateModelOutcomeV1,
|
|
14
15
|
IsolatePendingDecisionV1,
|
|
15
16
|
LlmStreamEvent,
|
|
16
17
|
NormalizedModelRequest,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
encodeIsolateModelEventLineV1,
|
|
21
22
|
} from "@frockbot/kernel-contracts";
|
|
22
23
|
import type { BotIsolateArtifactStore } from "@frockbot/kernel-composition/isolate";
|
|
24
|
+
import type { EnabledCapabilityV1 } from "@frockbot/configuration-core";
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* The compatibility date every Bot isolate is loaded with. Pinned beside the
|
|
@@ -36,8 +38,8 @@ export interface BotCapabilitiesPropsV1 {
|
|
|
36
38
|
botId: string;
|
|
37
39
|
generationId: string;
|
|
38
40
|
packageId: string;
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
+
/** The User's enabled set, already resolved by the authority. */
|
|
42
|
+
capabilities: IsolateCapabilityV1[];
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export const ISOLATE_DECISION_PREFIX = "isolate:decision:";
|
|
@@ -78,25 +80,17 @@ export interface IsolateCapabilityStore {
|
|
|
78
80
|
list<T>(options: { prefix: string }): Promise<Map<string, T>>;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
/** One
|
|
82
|
-
export
|
|
83
|
-
assignmentId: string;
|
|
84
|
-
packageId: string;
|
|
85
|
-
capabilityId: string;
|
|
86
|
-
kind: IsolateCapabilityDescriptorV1["kind"];
|
|
87
|
-
connectionId?: string;
|
|
88
|
-
providerModelId?: string;
|
|
89
|
-
}
|
|
83
|
+
/** One account-wide Capability, already resolved from User enablement. */
|
|
84
|
+
export type IsolateCapabilityV1 = EnabledCapabilityV1;
|
|
90
85
|
|
|
91
86
|
/**
|
|
92
|
-
* The Bot's
|
|
93
|
-
*
|
|
87
|
+
* The Bot's effective model binding, resolved by the authority from generic
|
|
88
|
+
* Package settings and the User's Connection — never from anything the Bot
|
|
94
89
|
* supplied. An `invokeModel` request is authorized only when it names exactly
|
|
95
90
|
* this provider and this model, and it is forwarded carrying exactly this
|
|
96
91
|
* binding.
|
|
97
92
|
*/
|
|
98
93
|
export interface IsolateModelBindingV1 {
|
|
99
|
-
assignmentId: string;
|
|
100
94
|
packageId: string;
|
|
101
95
|
capabilityId: string;
|
|
102
96
|
connectionId: string;
|
|
@@ -106,6 +100,15 @@ export interface IsolateModelBindingV1 {
|
|
|
106
100
|
catalogGeneration?: string;
|
|
107
101
|
}
|
|
108
102
|
|
|
103
|
+
/** A configured model binding whose Connection cannot currently be used. */
|
|
104
|
+
export interface IsolateUnavailableModelBindingV1 {
|
|
105
|
+
provider?: string;
|
|
106
|
+
providerModelId: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const ISOLATE_MODEL_UNAVAILABLE_MESSAGE =
|
|
110
|
+
"the enabled model binding is unavailable";
|
|
111
|
+
|
|
109
112
|
/** The bound Bot-supplied correlation id: a field, never a key, and never unbounded. */
|
|
110
113
|
export const MAX_ISOLATE_REQUEST_ID = 256;
|
|
111
114
|
|
|
@@ -122,14 +125,17 @@ export interface IsolateCapabilityHostOptions {
|
|
|
122
125
|
botId: string;
|
|
123
126
|
packageId: string;
|
|
124
127
|
generationId: string;
|
|
125
|
-
/**
|
|
126
|
-
|
|
128
|
+
/** User-enabled and nothing else. */
|
|
129
|
+
capabilities: readonly IsolateCapabilityV1[];
|
|
127
130
|
/**
|
|
128
|
-
* The one model binding this Bot
|
|
129
|
-
*
|
|
131
|
+
* The one ready model binding resolved for this Bot. Absent without an
|
|
132
|
+
* unavailable binding means every model request is an authority-widening
|
|
133
|
+
* pending decision.
|
|
130
134
|
*/
|
|
131
135
|
modelBinding?: IsolateModelBindingV1;
|
|
132
|
-
/**
|
|
136
|
+
/** Present when the configured binding is held but its Connection is unavailable. */
|
|
137
|
+
unavailableModelBinding?: IsolateUnavailableModelBindingV1;
|
|
138
|
+
/** Present only while the ready binding can reach its provider path. */
|
|
133
139
|
modelPath?: IsolateModelPath;
|
|
134
140
|
now?(): Date;
|
|
135
141
|
newId?(): string;
|
|
@@ -138,26 +144,24 @@ export interface IsolateCapabilityHostOptions {
|
|
|
138
144
|
export interface IsolateCapabilityHost {
|
|
139
145
|
list(): Promise<IsolateCapabilityDescriptorV1[]>;
|
|
140
146
|
requestAuthority(request: unknown): Promise<IsolatePendingDecisionV1>;
|
|
141
|
-
invokeModel(
|
|
142
|
-
request: NormalizedModelRequest,
|
|
143
|
-
): Promise<IsolateModelInvocationV1>;
|
|
147
|
+
invokeModel(request: NormalizedModelRequest): Promise<IsolateModelOutcomeV1>;
|
|
144
148
|
pendingDecisions(): Promise<IsolatePendingAuthorityDecisionV1[]>;
|
|
145
149
|
recordedModelRequests(): Promise<IsolateModelRequestRecordV1[]>;
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
/**
|
|
149
|
-
* The enabled model
|
|
153
|
+
* The enabled model Capability that can serve this request, if any.
|
|
150
154
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
+
* The authority resolves the effective model against one User-enabled
|
|
156
|
+
* Package, Connection, and Capability. A request naming any other provider or
|
|
157
|
+
* model resolves to nothing, whatever the Bot claims about it — the
|
|
158
|
+
* Bot-supplied `modelBinding` is never read here or anywhere downstream.
|
|
155
159
|
*/
|
|
156
|
-
export function
|
|
157
|
-
|
|
160
|
+
export function matchingModelCapabilityV1(
|
|
161
|
+
capabilities: readonly IsolateCapabilityV1[],
|
|
158
162
|
binding: IsolateModelBindingV1 | undefined,
|
|
159
163
|
request: NormalizedModelRequest,
|
|
160
|
-
):
|
|
164
|
+
): IsolateCapabilityV1 | undefined {
|
|
161
165
|
if (!binding) return undefined;
|
|
162
166
|
if (
|
|
163
167
|
request.provider !== binding.provider ||
|
|
@@ -165,14 +169,12 @@ export function matchingModelAssignmentV1(
|
|
|
165
169
|
) {
|
|
166
170
|
return undefined;
|
|
167
171
|
}
|
|
168
|
-
return
|
|
169
|
-
(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
assignment.connectionId === binding.connectionId &&
|
|
175
|
-
assignment.providerModelId === binding.providerModelId,
|
|
172
|
+
return capabilities.find(
|
|
173
|
+
(capability) =>
|
|
174
|
+
capability.kind === "model" &&
|
|
175
|
+
capability.packageId === binding.packageId &&
|
|
176
|
+
capability.capabilityId === binding.capabilityId &&
|
|
177
|
+
capability.connectionId === binding.connectionId,
|
|
176
178
|
);
|
|
177
179
|
}
|
|
178
180
|
|
|
@@ -208,9 +210,9 @@ export function createIsolateCapabilityHost(
|
|
|
208
210
|
return {
|
|
209
211
|
list(): Promise<IsolateCapabilityDescriptorV1[]> {
|
|
210
212
|
return Promise.resolve(
|
|
211
|
-
options.
|
|
212
|
-
capabilityId:
|
|
213
|
-
kind:
|
|
213
|
+
options.capabilities.map((capability) => ({
|
|
214
|
+
capabilityId: capability.capabilityId,
|
|
215
|
+
kind: capability.kind,
|
|
214
216
|
})),
|
|
215
217
|
);
|
|
216
218
|
},
|
|
@@ -221,26 +223,44 @@ export function createIsolateCapabilityHost(
|
|
|
221
223
|
const decoded: IsolateAuthorityRequestV1 =
|
|
222
224
|
decodeIsolateAuthorityRequestV1(request);
|
|
223
225
|
// Self-modification never widens authority, even when the capability is
|
|
224
|
-
// already
|
|
226
|
+
// already enabled: the answer is a decision the User makes.
|
|
225
227
|
return await recordDecision(decoded.capabilityId, decoded.reason);
|
|
226
228
|
},
|
|
227
229
|
|
|
228
230
|
async invokeModel(
|
|
229
231
|
request: NormalizedModelRequest,
|
|
230
|
-
): Promise<
|
|
232
|
+
): Promise<IsolateModelOutcomeV1> {
|
|
231
233
|
if (request.requestId.length > MAX_ISOLATE_REQUEST_ID) {
|
|
232
234
|
throw new Error("isolate model request requestId is not bounded");
|
|
233
235
|
}
|
|
234
236
|
const binding = options.modelBinding;
|
|
235
|
-
const
|
|
236
|
-
options.
|
|
237
|
+
const capability = matchingModelCapabilityV1(
|
|
238
|
+
options.capabilities,
|
|
237
239
|
binding,
|
|
238
240
|
request,
|
|
239
241
|
);
|
|
240
|
-
|
|
242
|
+
const unavailableBinding = options.unavailableModelBinding;
|
|
243
|
+
if (
|
|
244
|
+
unavailableBinding &&
|
|
245
|
+
request.model === unavailableBinding.providerModelId &&
|
|
246
|
+
(unavailableBinding.provider === undefined ||
|
|
247
|
+
request.provider === unavailableBinding.provider)
|
|
248
|
+
) {
|
|
249
|
+
return {
|
|
250
|
+
status: "unavailable",
|
|
251
|
+
reason: ISOLATE_MODEL_UNAVAILABLE_MESSAGE,
|
|
252
|
+
} satisfies IsolateCapabilityFailureV1;
|
|
253
|
+
}
|
|
254
|
+
if (capability && binding && !options.modelPath) {
|
|
255
|
+
return {
|
|
256
|
+
status: "unavailable",
|
|
257
|
+
reason: ISOLATE_MODEL_UNAVAILABLE_MESSAGE,
|
|
258
|
+
} satisfies IsolateCapabilityFailureV1;
|
|
259
|
+
}
|
|
260
|
+
if (!capability || !binding || !options.modelPath) {
|
|
241
261
|
return await recordDecision(
|
|
242
262
|
`models:${request.provider}:${request.model}`,
|
|
243
|
-
`Bot Package "${options.packageId}" asked to invoke a model with no matching enabled
|
|
263
|
+
`Bot Package "${options.packageId}" asked to invoke a model with no matching enabled Capability`,
|
|
244
264
|
);
|
|
245
265
|
}
|
|
246
266
|
// The binding the provider path receives is the authority's, never the
|
|
@@ -269,7 +289,7 @@ export function createIsolateCapabilityHost(
|
|
|
269
289
|
botId: options.botId,
|
|
270
290
|
packageId: options.packageId,
|
|
271
291
|
generationId: options.generationId,
|
|
272
|
-
capabilityId:
|
|
292
|
+
capabilityId: capability.capabilityId,
|
|
273
293
|
request: forwarded,
|
|
274
294
|
recordedAt: now().toISOString(),
|
|
275
295
|
};
|
|
@@ -346,31 +366,49 @@ export function isolateModelEventStreamV1(
|
|
|
346
366
|
}
|
|
347
367
|
|
|
348
368
|
/**
|
|
349
|
-
* The content address of
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* the loader id stays derived from the artifact set and the binding digest
|
|
357
|
-
* alone.
|
|
369
|
+
* The content address of every binding baked into an isolate's `env`: User,
|
|
370
|
+
* Bot, and Composition generation from `IDENTITY` and the `CAPABILITIES` props,
|
|
371
|
+
* plus the User-enabled capability set. A loader id is served from cache, so
|
|
372
|
+
* any change must produce a different digest or a stale isolate will answer
|
|
373
|
+
* under the wrong identity or authority. Identical artifacts share an isolate
|
|
374
|
+
* only when all of these binding inputs are identical (AGENTS.md Package
|
|
375
|
+
* composition; ADR 0019).
|
|
358
376
|
*/
|
|
359
|
-
export async function isolateBindingDigestV1(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
377
|
+
export async function isolateBindingDigestV1(input: {
|
|
378
|
+
userId: string;
|
|
379
|
+
botId: string;
|
|
380
|
+
generationId: string;
|
|
381
|
+
capabilities: readonly IsolateCapabilityV1[];
|
|
382
|
+
}): Promise<string> {
|
|
383
|
+
const ordered = [...input.capabilities]
|
|
384
|
+
.map((capability) => ({
|
|
385
|
+
packageId: capability.packageId,
|
|
386
|
+
capabilityId: capability.capabilityId,
|
|
387
|
+
kind: capability.kind,
|
|
388
|
+
connectionId: capability.connectionId ?? null,
|
|
371
389
|
}))
|
|
372
|
-
.sort(
|
|
373
|
-
|
|
390
|
+
.sort(
|
|
391
|
+
(left, right) =>
|
|
392
|
+
compareIsolateIdentifierV1(left.packageId, right.packageId) ||
|
|
393
|
+
compareIsolateIdentifierV1(left.capabilityId, right.capabilityId) ||
|
|
394
|
+
compareIsolateIdentifierV1(left.kind, right.kind) ||
|
|
395
|
+
compareIsolateIdentifierV1(
|
|
396
|
+
left.connectionId ?? "",
|
|
397
|
+
right.connectionId ?? "",
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
return await sha256Hex(
|
|
401
|
+
JSON.stringify({
|
|
402
|
+
userId: input.userId,
|
|
403
|
+
botId: input.botId,
|
|
404
|
+
generationId: input.generationId,
|
|
405
|
+
ordered,
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function compareIsolateIdentifierV1(left: string, right: string): number {
|
|
411
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
374
412
|
}
|
|
375
413
|
|
|
376
414
|
async function sha256Hex(value: string): Promise<string> {
|
|
@@ -105,6 +105,11 @@ describe("Bot recovery", () => {
|
|
|
105
105
|
revision: 1,
|
|
106
106
|
profile: { name: "User" },
|
|
107
107
|
packages: [
|
|
108
|
+
{
|
|
109
|
+
packageId: "custom-models",
|
|
110
|
+
version: "0.0.1",
|
|
111
|
+
state: "installed",
|
|
112
|
+
},
|
|
108
113
|
{
|
|
109
114
|
packageId: "provider-ollama-cloud",
|
|
110
115
|
version: "0.0.1",
|
|
@@ -140,6 +145,10 @@ describe("Bot recovery", () => {
|
|
|
140
145
|
},
|
|
141
146
|
},
|
|
142
147
|
],
|
|
148
|
+
platformModel: {
|
|
149
|
+
connectionId: "ollama-1",
|
|
150
|
+
providerModelId: "glm-5.3-flash:cloud",
|
|
151
|
+
},
|
|
143
152
|
};
|
|
144
153
|
const leasedRequests: Array<Record<string, unknown>> = [];
|
|
145
154
|
const settledEffects: string[] = [];
|
|
@@ -148,35 +157,6 @@ describe("Bot recovery", () => {
|
|
|
148
157
|
readConfiguration: () => Promise.resolve(structuredClone(userSettings)),
|
|
149
158
|
listBots: () =>
|
|
150
159
|
Promise.resolve({ schemaVersion: 1 as const, revision: 0, bots: [] }),
|
|
151
|
-
getConnection: () =>
|
|
152
|
-
Promise.resolve(structuredClone(userSettings.connections[0])),
|
|
153
|
-
executeConnectionDependency: (request: { action: string }) => {
|
|
154
|
-
if (request.action === "claim") {
|
|
155
|
-
return Promise.resolve({
|
|
156
|
-
schemaVersion: 1 as const,
|
|
157
|
-
status: "claimed" as const,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (request.action === "acknowledge") {
|
|
161
|
-
return Promise.resolve({
|
|
162
|
-
schemaVersion: 1 as const,
|
|
163
|
-
status: "acknowledged" as const,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (request.action === "read") {
|
|
167
|
-
return Promise.resolve({
|
|
168
|
-
schemaVersion: 1 as const,
|
|
169
|
-
status: "acknowledged" as const,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
return Promise.resolve({
|
|
173
|
-
schemaVersion: 1 as const,
|
|
174
|
-
status: "released" as const,
|
|
175
|
-
});
|
|
176
|
-
},
|
|
177
|
-
claimConnectionDependency: () => Promise.resolve(true),
|
|
178
|
-
acknowledgeConnectionDependency: () => Promise.resolve(true),
|
|
179
|
-
compensateConnectionDependency: () => Promise.resolve(true),
|
|
180
160
|
leaseModelCredential: (input: unknown) => {
|
|
181
161
|
leasedRequests.push(input as Record<string, unknown>);
|
|
182
162
|
const request = input as { effectId: string };
|
|
@@ -241,32 +221,8 @@ describe("Bot recovery", () => {
|
|
|
241
221
|
const configured = host();
|
|
242
222
|
await configured.materializeSettings(
|
|
243
223
|
{ userId: "user-1", botId: "primary" },
|
|
244
|
-
{
|
|
245
|
-
name: "Ollama Bot",
|
|
246
|
-
model: {
|
|
247
|
-
connectionId: "ollama-1",
|
|
248
|
-
providerModelId: "glm-5.3-flash:cloud",
|
|
249
|
-
},
|
|
250
|
-
},
|
|
224
|
+
{ name: "Ollama Bot" },
|
|
251
225
|
);
|
|
252
|
-
await configured.executeConfiguration({
|
|
253
|
-
schemaVersion: 1,
|
|
254
|
-
userId: "user-1",
|
|
255
|
-
botId: "primary",
|
|
256
|
-
command: {
|
|
257
|
-
schemaVersion: 1,
|
|
258
|
-
type: "bot/assign-capability",
|
|
259
|
-
commandId: "assign-ollama-model",
|
|
260
|
-
botId: "primary",
|
|
261
|
-
expectedRevision: 0,
|
|
262
|
-
assignment: {
|
|
263
|
-
assignmentId: "ollama-model",
|
|
264
|
-
packageId: "provider-ollama-cloud",
|
|
265
|
-
capabilityId: "ollama-cloud-models",
|
|
266
|
-
connectionId: "ollama-1",
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
226
|
const first = await host().run({
|
|
271
227
|
userId: "user-1",
|
|
272
228
|
botId: "primary",
|
|
@@ -1353,6 +1309,11 @@ describe("Bot recovery", () => {
|
|
|
1353
1309
|
revision: 1,
|
|
1354
1310
|
profile: { name: "User" },
|
|
1355
1311
|
packages: [
|
|
1312
|
+
{
|
|
1313
|
+
packageId: "custom-models",
|
|
1314
|
+
version: "0.0.1",
|
|
1315
|
+
state: "installed",
|
|
1316
|
+
},
|
|
1356
1317
|
{
|
|
1357
1318
|
packageId: "provider-ollama-cloud",
|
|
1358
1319
|
version: "0.0.1",
|
|
@@ -1371,23 +1332,12 @@ describe("Bot recovery", () => {
|
|
|
1371
1332
|
safeMetadata: {},
|
|
1372
1333
|
},
|
|
1373
1334
|
],
|
|
1374
|
-
|
|
1375
|
-
const settings = {
|
|
1376
|
-
...initializeBotSettingsV1("primary"),
|
|
1377
|
-
model: {
|
|
1335
|
+
platformModel: {
|
|
1378
1336
|
connectionId: "ollama-race",
|
|
1379
1337
|
providerModelId: "model:cloud",
|
|
1380
1338
|
},
|
|
1381
|
-
assignments: [
|
|
1382
|
-
{
|
|
1383
|
-
assignmentId: "model-race",
|
|
1384
|
-
packageId: "provider-ollama-cloud",
|
|
1385
|
-
capabilityId: "ollama-cloud-models",
|
|
1386
|
-
connectionId: "ollama-race",
|
|
1387
|
-
state: "enabled" as const,
|
|
1388
|
-
},
|
|
1389
|
-
],
|
|
1390
1339
|
};
|
|
1340
|
+
const settings = initializeBotSettingsV1("primary");
|
|
1391
1341
|
await storage.put("bot-configuration", settings);
|
|
1392
1342
|
const contribution = createShellBotBackendContribution({
|
|
1393
1343
|
state: { storage } as unknown as DurableObjectState,
|
package/src/backend-routines.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The Bot Durable Object's half of the Routines seam.
|
|
2
2
|
//
|
|
3
3
|
// "The Bot's Durable Object is the authority for everything Bot-scoped: …
|
|
4
|
-
// durable scheduling, Routines,
|
|
4
|
+
// durable scheduling, Routines, and Composition." The Routines Package holds the
|
|
5
5
|
// records, the codecs, the command semantics and the scheduler; this module
|
|
6
6
|
// supplies the two things the Package cannot own — the Durable Object's storage,
|
|
7
7
|
// and the one call that admits a Turn.
|