@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
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
decodeConnectionDependencyRequirementV1,
|
|
3
|
-
decodeModelAssignmentV1,
|
|
4
|
-
decodeOperationReceiptV1,
|
|
5
|
-
isPublicIdentifier,
|
|
6
|
-
type CapabilityAssignmentView,
|
|
7
|
-
type ConnectionDependencyRequirementV1,
|
|
8
|
-
type ModelAssignment,
|
|
9
|
-
type OperationReceiptV1,
|
|
10
|
-
} from "@frockbot/configuration-core";
|
|
11
|
-
|
|
12
|
-
export type AssignmentOperationKind = "assigning" | "replacing" | "unassigning";
|
|
13
|
-
|
|
14
|
-
export type AssignmentSagaPhase =
|
|
15
|
-
"claiming" | "committing" | "acknowledging" | "releasing";
|
|
16
|
-
|
|
17
|
-
/** Durable Bot-owned coordinator state, separate from its stable Assignment. */
|
|
18
|
-
export interface StoredAssignmentSaga {
|
|
19
|
-
schemaVersion: 1;
|
|
20
|
-
commandId: string;
|
|
21
|
-
commandFingerprint: string;
|
|
22
|
-
userId: string;
|
|
23
|
-
botId: string;
|
|
24
|
-
operation: AssignmentOperationKind;
|
|
25
|
-
assignmentId: string;
|
|
26
|
-
generation: string;
|
|
27
|
-
phase: AssignmentSagaPhase;
|
|
28
|
-
target?: Omit<CapabilityAssignmentView, "state">;
|
|
29
|
-
claimDispatched?: boolean;
|
|
30
|
-
acknowledgeDispatched?: boolean;
|
|
31
|
-
releaseDispatched?: boolean;
|
|
32
|
-
targetRequirement?: ConnectionDependencyRequirementV1;
|
|
33
|
-
previous?: CapabilityAssignmentView;
|
|
34
|
-
previousGeneration?: string;
|
|
35
|
-
/**
|
|
36
|
-
* The Bot model this Assignment carries. It commits inside the saga's commit
|
|
37
|
-
* phase, so the Connection dependency claim and the model binding are one
|
|
38
|
-
* durable, idempotent unit rather than two commands that can disagree.
|
|
39
|
-
*/
|
|
40
|
-
model?: ModelAssignment;
|
|
41
|
-
/** The Assignment's removal also unbinds the Bot's model. */
|
|
42
|
-
clearModel?: boolean;
|
|
43
|
-
deadlineAt: number;
|
|
44
|
-
acceptedReceipt: OperationReceiptV1;
|
|
45
|
-
receipt?: OperationReceiptV1;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function exact(
|
|
49
|
-
value: Record<PropertyKey, unknown>,
|
|
50
|
-
required: readonly string[],
|
|
51
|
-
optional: readonly string[] = [],
|
|
52
|
-
): void {
|
|
53
|
-
const allowed = new Set<PropertyKey>([...required, ...optional]);
|
|
54
|
-
if (
|
|
55
|
-
!required.every((key) => Object.hasOwn(value, key)) ||
|
|
56
|
-
!Reflect.ownKeys(value).every((key) => allowed.has(key))
|
|
57
|
-
) {
|
|
58
|
-
throw new Error("Stored Assignment saga has invalid fields");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function identifier(value: unknown, label: string): string {
|
|
63
|
-
if (typeof value !== "string" || !isPublicIdentifier(value)) {
|
|
64
|
-
throw new Error(`Stored Assignment saga ${label} is invalid`);
|
|
65
|
-
}
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function requireStoredAssignmentSaga(
|
|
70
|
-
input: unknown,
|
|
71
|
-
): StoredAssignmentSaga {
|
|
72
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
73
|
-
throw new Error("Stored Assignment saga is invalid");
|
|
74
|
-
}
|
|
75
|
-
const value = input as Record<PropertyKey, unknown>;
|
|
76
|
-
exact(
|
|
77
|
-
value,
|
|
78
|
-
[
|
|
79
|
-
"schemaVersion",
|
|
80
|
-
"commandId",
|
|
81
|
-
"commandFingerprint",
|
|
82
|
-
"userId",
|
|
83
|
-
"botId",
|
|
84
|
-
"operation",
|
|
85
|
-
"assignmentId",
|
|
86
|
-
"generation",
|
|
87
|
-
"phase",
|
|
88
|
-
"deadlineAt",
|
|
89
|
-
"acceptedReceipt",
|
|
90
|
-
],
|
|
91
|
-
[
|
|
92
|
-
"target",
|
|
93
|
-
"model",
|
|
94
|
-
"clearModel",
|
|
95
|
-
"targetRequirement",
|
|
96
|
-
"previous",
|
|
97
|
-
"previousGeneration",
|
|
98
|
-
"claimDispatched",
|
|
99
|
-
"acknowledgeDispatched",
|
|
100
|
-
"releaseDispatched",
|
|
101
|
-
"receipt",
|
|
102
|
-
],
|
|
103
|
-
);
|
|
104
|
-
if (
|
|
105
|
-
value.schemaVersion !== 1 ||
|
|
106
|
-
(value.operation !== "assigning" &&
|
|
107
|
-
value.operation !== "replacing" &&
|
|
108
|
-
value.operation !== "unassigning") ||
|
|
109
|
-
(value.phase !== "claiming" &&
|
|
110
|
-
value.phase !== "committing" &&
|
|
111
|
-
value.phase !== "acknowledging" &&
|
|
112
|
-
value.phase !== "releasing") ||
|
|
113
|
-
typeof value.deadlineAt !== "number" ||
|
|
114
|
-
!Number.isFinite(value.deadlineAt)
|
|
115
|
-
) {
|
|
116
|
-
throw new Error("Stored Assignment saga is invalid");
|
|
117
|
-
}
|
|
118
|
-
for (const key of [
|
|
119
|
-
"claimDispatched",
|
|
120
|
-
"acknowledgeDispatched",
|
|
121
|
-
"releaseDispatched",
|
|
122
|
-
"clearModel",
|
|
123
|
-
] as const) {
|
|
124
|
-
if (value[key] !== undefined && typeof value[key] !== "boolean") {
|
|
125
|
-
throw new Error(`Stored Assignment saga ${key} is invalid`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const target = (candidate: unknown, state: boolean) => {
|
|
129
|
-
if (
|
|
130
|
-
!candidate ||
|
|
131
|
-
typeof candidate !== "object" ||
|
|
132
|
-
Array.isArray(candidate)
|
|
133
|
-
) {
|
|
134
|
-
throw new Error("Stored Assignment saga target is invalid");
|
|
135
|
-
}
|
|
136
|
-
const item = candidate as Record<PropertyKey, unknown>;
|
|
137
|
-
exact(
|
|
138
|
-
item,
|
|
139
|
-
state
|
|
140
|
-
? ["assignmentId", "packageId", "capabilityId", "state"]
|
|
141
|
-
: ["assignmentId", "packageId", "capabilityId"],
|
|
142
|
-
["connectionId"],
|
|
143
|
-
);
|
|
144
|
-
if (
|
|
145
|
-
state &&
|
|
146
|
-
item.state !== "enabled" &&
|
|
147
|
-
item.state !== "disabled" &&
|
|
148
|
-
item.state !== "unavailable"
|
|
149
|
-
) {
|
|
150
|
-
throw new Error("Stored Assignment saga target state is invalid");
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
assignmentId: identifier(item.assignmentId, "assignmentId"),
|
|
154
|
-
packageId: identifier(item.packageId, "packageId"),
|
|
155
|
-
capabilityId: identifier(item.capabilityId, "capabilityId"),
|
|
156
|
-
connectionId:
|
|
157
|
-
item.connectionId === undefined
|
|
158
|
-
? undefined
|
|
159
|
-
: identifier(item.connectionId, "connectionId"),
|
|
160
|
-
...(state
|
|
161
|
-
? { state: item.state as CapabilityAssignmentView["state"] }
|
|
162
|
-
: {}),
|
|
163
|
-
};
|
|
164
|
-
};
|
|
165
|
-
const acceptedReceipt = decodeOperationReceiptV1(value.acceptedReceipt);
|
|
166
|
-
if (
|
|
167
|
-
acceptedReceipt.status !== "pending" ||
|
|
168
|
-
acceptedReceipt.commandId !== value.commandId
|
|
169
|
-
) {
|
|
170
|
-
throw new Error("Stored Assignment saga accepted receipt is invalid");
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
schemaVersion: 1,
|
|
174
|
-
commandId: identifier(value.commandId, "commandId"),
|
|
175
|
-
commandFingerprint:
|
|
176
|
-
typeof value.commandFingerprint === "string" &&
|
|
177
|
-
value.commandFingerprint.length <= 2_000
|
|
178
|
-
? value.commandFingerprint
|
|
179
|
-
: (() => {
|
|
180
|
-
throw new Error("Stored Assignment saga fingerprint is invalid");
|
|
181
|
-
})(),
|
|
182
|
-
userId: identifier(value.userId, "userId"),
|
|
183
|
-
botId: identifier(value.botId, "botId"),
|
|
184
|
-
operation: value.operation,
|
|
185
|
-
assignmentId: identifier(value.assignmentId, "assignmentId"),
|
|
186
|
-
generation: identifier(value.generation, "generation"),
|
|
187
|
-
phase: value.phase,
|
|
188
|
-
target:
|
|
189
|
-
value.target === undefined
|
|
190
|
-
? undefined
|
|
191
|
-
: (target(value.target, false) as Omit<
|
|
192
|
-
CapabilityAssignmentView,
|
|
193
|
-
"state"
|
|
194
|
-
>),
|
|
195
|
-
model:
|
|
196
|
-
value.model === undefined
|
|
197
|
-
? undefined
|
|
198
|
-
: decodeModelAssignmentV1(value.model),
|
|
199
|
-
clearModel: value.clearModel as boolean | undefined,
|
|
200
|
-
targetRequirement:
|
|
201
|
-
value.targetRequirement === undefined
|
|
202
|
-
? undefined
|
|
203
|
-
: decodeConnectionDependencyRequirementV1(value.targetRequirement),
|
|
204
|
-
previous:
|
|
205
|
-
value.previous === undefined
|
|
206
|
-
? undefined
|
|
207
|
-
: (target(value.previous, true) as CapabilityAssignmentView),
|
|
208
|
-
previousGeneration:
|
|
209
|
-
value.previousGeneration === undefined
|
|
210
|
-
? undefined
|
|
211
|
-
: identifier(value.previousGeneration, "previousGeneration"),
|
|
212
|
-
claimDispatched: value.claimDispatched as boolean | undefined,
|
|
213
|
-
acknowledgeDispatched: value.acknowledgeDispatched as boolean | undefined,
|
|
214
|
-
releaseDispatched: value.releaseDispatched as boolean | undefined,
|
|
215
|
-
deadlineAt: value.deadlineAt,
|
|
216
|
-
acceptedReceipt,
|
|
217
|
-
receipt:
|
|
218
|
-
value.receipt === undefined
|
|
219
|
-
? undefined
|
|
220
|
-
: decodeOperationReceiptV1(value.receipt),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function nextAssignmentPhase(
|
|
225
|
-
saga: StoredAssignmentSaga,
|
|
226
|
-
event: "claimed" | "committed" | "acknowledged" | "released",
|
|
227
|
-
): StoredAssignmentSaga | undefined {
|
|
228
|
-
if (saga.phase === "claiming" && event === "claimed") {
|
|
229
|
-
return { ...saga, phase: "committing" };
|
|
230
|
-
}
|
|
231
|
-
if (saga.phase === "committing" && event === "committed") {
|
|
232
|
-
if (saga.target?.connectionId) {
|
|
233
|
-
return { ...saga, phase: "acknowledging" };
|
|
234
|
-
}
|
|
235
|
-
if (saga.previous?.connectionId) {
|
|
236
|
-
return { ...saga, phase: "releasing" };
|
|
237
|
-
}
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
if (saga.phase === "acknowledging" && event === "acknowledged") {
|
|
241
|
-
return saga.previous?.connectionId
|
|
242
|
-
? { ...saga, phase: "releasing" }
|
|
243
|
-
: undefined;
|
|
244
|
-
}
|
|
245
|
-
if (saga.phase === "releasing" && event === "released") return undefined;
|
|
246
|
-
throw new Error(`Assignment saga cannot apply ${event} while ${saga.phase}`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export type AssignmentSagaSettlement =
|
|
250
|
-
"acknowledged" | "compensated" | "rejected";
|
|
251
|
-
|
|
252
|
-
export interface AssignmentSagaEffects {
|
|
253
|
-
acknowledge(saga: StoredAssignmentSaga): Promise<boolean>;
|
|
254
|
-
compensate(saga: StoredAssignmentSaga): Promise<void>;
|
|
255
|
-
release(saga: StoredAssignmentSaga): Promise<boolean>;
|
|
256
|
-
rejectCommitted(saga: StoredAssignmentSaga): Promise<void>;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export async function settleAssignmentSaga(
|
|
260
|
-
saga: StoredAssignmentSaga,
|
|
261
|
-
effects: AssignmentSagaEffects,
|
|
262
|
-
): Promise<AssignmentSagaSettlement> {
|
|
263
|
-
if (saga.phase === "claiming") {
|
|
264
|
-
await effects.compensate(saga);
|
|
265
|
-
return "compensated";
|
|
266
|
-
}
|
|
267
|
-
if (await effects.acknowledge(saga)) return "acknowledged";
|
|
268
|
-
await effects.compensate(saga);
|
|
269
|
-
if (!(await effects.release(saga))) {
|
|
270
|
-
throw new Error("Rejected Connection dependency was not released");
|
|
271
|
-
}
|
|
272
|
-
await effects.rejectCommitted(saga);
|
|
273
|
-
return "rejected";
|
|
274
|
-
}
|