@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.
Files changed (41) hide show
  1. package/frockbot.json +4 -0
  2. package/package.json +30 -28
  3. package/src/agent.ts +10 -13
  4. package/src/backend-authoring.test.ts +356 -2
  5. package/src/backend-authoring.ts +585 -20
  6. package/src/backend-composition-input.test.ts +65 -0
  7. package/src/backend-composition-input.ts +58 -0
  8. package/src/backend-composition.ts +23 -5
  9. package/src/backend-configuration.test.ts +549 -1468
  10. package/src/backend-contracts.test.ts +28 -0
  11. package/src/backend-contracts.ts +3 -1
  12. package/src/backend-execution.ts +0 -4
  13. package/src/backend-iframe-ui.test.ts +131 -0
  14. package/src/backend-image.test.ts +8 -8
  15. package/src/backend-image.ts +13 -13
  16. package/src/backend-isolate.test.ts +230 -89
  17. package/src/backend-isolate.ts +103 -200
  18. package/src/backend-package-catalog.test.ts +451 -0
  19. package/src/backend-package-catalog.ts +923 -0
  20. package/src/backend-recovery-integration.test.ts +17 -67
  21. package/src/backend-routines.ts +1 -1
  22. package/src/backend-runner-iframe.test.ts +74 -0
  23. package/src/backend-runner.ts +192 -0
  24. package/src/backend.ts +1198 -1781
  25. package/src/client/FrockBotApp.vue +44 -73
  26. package/src/client/PackageIframeHost.vue +218 -0
  27. package/src/client/PackageIframeSettings.vue +52 -0
  28. package/src/client/SendPayloadView.vue +0 -60
  29. package/src/client/index.test.ts +439 -380
  30. package/src/client/index.ts +163 -257
  31. package/src/client/model-presentation.test.ts +21 -8
  32. package/src/client/model-presentation.ts +14 -7
  33. package/src/client/package-iframe-host-message.test.ts +41 -0
  34. package/src/client/package-iframe-host-message.ts +27 -0
  35. package/src/client/styles.css +0 -27
  36. package/src/composition-views.ts +54 -0
  37. package/src/settings-links.test.ts +2 -10
  38. package/src/settings-links.ts +1 -13
  39. package/src/shared.ts +10 -13
  40. package/src/backend-assignment.test.ts +0 -161
  41. 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
- }