@frockbot/computer-host-protocol 0.2.5 → 0.3.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/package.json +1 -1
- package/src/protocol.test.ts +90 -0
- package/src/protocol.ts +104 -9
package/package.json
CHANGED
package/src/protocol.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
COMPUTER_HOST_LIMITS,
|
|
4
4
|
COMPUTER_HOST_ROUTES,
|
|
5
5
|
ComputerHostExecFrameReaderV1,
|
|
6
|
+
ComputerHostOpenFrameReaderV1,
|
|
6
7
|
computerHostOperationKindV1,
|
|
7
8
|
computerHostProblemV1,
|
|
8
9
|
decodeBase64FieldV1,
|
|
@@ -17,15 +18,18 @@ import {
|
|
|
17
18
|
decodeComputerHostFileWriteResultV1,
|
|
18
19
|
decodeComputerHostHttpRequestV1,
|
|
19
20
|
decodeComputerHostOpenResultV1,
|
|
21
|
+
decodeComputerHostOpenFrameV1,
|
|
20
22
|
decodeComputerHostProblemV1,
|
|
21
23
|
decodeComputerHostRequestV1,
|
|
22
24
|
decodeComputerHostServiceResultV1,
|
|
23
25
|
decodeComputerHostViewerResultV1,
|
|
24
26
|
decodeComputerPathV1,
|
|
25
27
|
encodeComputerHostExecFrameV1,
|
|
28
|
+
encodeComputerHostOpenFrameV1,
|
|
26
29
|
encodeComputerHostRequestV1,
|
|
27
30
|
problem,
|
|
28
31
|
type ComputerHostOperationV1,
|
|
32
|
+
type ComputerHostOpenFrameV1,
|
|
29
33
|
} from "./protocol.ts";
|
|
30
34
|
|
|
31
35
|
const envelope = {
|
|
@@ -108,6 +112,29 @@ describe("envelope", () => {
|
|
|
108
112
|
});
|
|
109
113
|
});
|
|
110
114
|
|
|
115
|
+
describe("open", () => {
|
|
116
|
+
test("selects a stream without making it mandatory for earlier V1 callers", () => {
|
|
117
|
+
expect(
|
|
118
|
+
decodeComputerHostRequestV1(
|
|
119
|
+
"open",
|
|
120
|
+
request({ kind: "open", stream: true }),
|
|
121
|
+
).operation,
|
|
122
|
+
).toEqual({ kind: "open", stream: true });
|
|
123
|
+
expect(
|
|
124
|
+
decodeComputerHostRequestV1("open", request({ kind: "open" })).operation,
|
|
125
|
+
).toEqual({ kind: "open" });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("refuses a non-boolean stream flag", () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
decodeComputerHostRequestV1("open", {
|
|
131
|
+
...request({ kind: "open" }),
|
|
132
|
+
stream: "yes",
|
|
133
|
+
}),
|
|
134
|
+
).toThrow(/open stream must be a boolean/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
111
138
|
describe("exec", () => {
|
|
112
139
|
const exec: ComputerHostOperationV1 = {
|
|
113
140
|
kind: "exec",
|
|
@@ -617,6 +644,69 @@ describe("problems", () => {
|
|
|
617
644
|
});
|
|
618
645
|
});
|
|
619
646
|
|
|
647
|
+
describe("open frames", () => {
|
|
648
|
+
const progress = {
|
|
649
|
+
kind: "provision" as const,
|
|
650
|
+
phase: "packages",
|
|
651
|
+
label: "installing the desktop packages",
|
|
652
|
+
index: 2,
|
|
653
|
+
total: 5,
|
|
654
|
+
status: "running" as const,
|
|
655
|
+
resumed: false,
|
|
656
|
+
};
|
|
657
|
+
const result = {
|
|
658
|
+
version: 1 as const,
|
|
659
|
+
effectId: "effect-1",
|
|
660
|
+
spriteName: "frockbot-0123456789ab",
|
|
661
|
+
directory: "agent-data/agents/bot-1",
|
|
662
|
+
generation: 1,
|
|
663
|
+
provisioning: {
|
|
664
|
+
...progress,
|
|
665
|
+
phase: "ready",
|
|
666
|
+
label: "the Computer is ready",
|
|
667
|
+
status: "complete" as const,
|
|
668
|
+
index: 5,
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
test("round-trips progress, result, and error frames", () => {
|
|
673
|
+
const frames: ComputerHostOpenFrameV1[] = [
|
|
674
|
+
{ type: "progress", progress },
|
|
675
|
+
{ type: "result", result },
|
|
676
|
+
{
|
|
677
|
+
type: "error",
|
|
678
|
+
code: "provider-unavailable",
|
|
679
|
+
message: "the container stopped",
|
|
680
|
+
retryable: true,
|
|
681
|
+
},
|
|
682
|
+
];
|
|
683
|
+
for (const frame of frames) {
|
|
684
|
+
expect(
|
|
685
|
+
decodeComputerHostOpenFrameV1(
|
|
686
|
+
encodeComputerHostOpenFrameV1(frame).trimEnd(),
|
|
687
|
+
),
|
|
688
|
+
).toEqual(frame);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("reassembles open frames across arbitrary chunk boundaries", () => {
|
|
693
|
+
const frames: ComputerHostOpenFrameV1[] = [
|
|
694
|
+
{ type: "progress", progress },
|
|
695
|
+
{ type: "result", result },
|
|
696
|
+
];
|
|
697
|
+
const wire = frames.map(encodeComputerHostOpenFrameV1).join("");
|
|
698
|
+
for (const size of [1, 3, 17, wire.length]) {
|
|
699
|
+
const reader = new ComputerHostOpenFrameReaderV1();
|
|
700
|
+
const seen: ComputerHostOpenFrameV1[] = [];
|
|
701
|
+
for (let index = 0; index < wire.length; index += size) {
|
|
702
|
+
seen.push(...reader.push(wire.slice(index, index + size)));
|
|
703
|
+
}
|
|
704
|
+
seen.push(...reader.end());
|
|
705
|
+
expect(seen).toEqual(frames);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
620
710
|
describe("exec frames", () => {
|
|
621
711
|
test("round-trips every frame type", () => {
|
|
622
712
|
const frames = [
|
package/src/protocol.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const COMPUTER_HOST_PROTOCOL_VERSION = 1;
|
|
|
21
21
|
/** Header carrying the shared secret between the two Workers and the container. */
|
|
22
22
|
export const COMPUTER_HOST_TOKEN_HEADER = "x-frockbot-host-token";
|
|
23
23
|
|
|
24
|
-
/** NDJSON media type used by a streaming
|
|
24
|
+
/** NDJSON media type used by a streaming Computer host response. */
|
|
25
25
|
export const COMPUTER_HOST_STREAM_MEDIA_TYPE = "application/x-ndjson";
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -104,6 +104,11 @@ export interface ComputerHostEnvelopeV1 {
|
|
|
104
104
|
|
|
105
105
|
export interface ComputerHostOpenOperationV1 {
|
|
106
106
|
kind: "open";
|
|
107
|
+
/**
|
|
108
|
+
* True for provisioning frames followed by one terminal result, false or
|
|
109
|
+
* absent for one buffered answer. Absence is the pre-streaming V1 shape.
|
|
110
|
+
*/
|
|
111
|
+
stream?: boolean;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
export interface ComputerHostExecOperationV1 {
|
|
@@ -382,6 +387,16 @@ export interface ComputerHostProblemV1 {
|
|
|
382
387
|
retryable: boolean;
|
|
383
388
|
}
|
|
384
389
|
|
|
390
|
+
export type ComputerHostOpenFrameV1 =
|
|
391
|
+
| { type: "progress"; progress: ComputerHostProvisioningV1 }
|
|
392
|
+
| { type: "result"; result: ComputerHostOpenResultV1 }
|
|
393
|
+
| {
|
|
394
|
+
type: "error";
|
|
395
|
+
code: ComputerHostErrorCodeV1;
|
|
396
|
+
message: string;
|
|
397
|
+
retryable: boolean;
|
|
398
|
+
};
|
|
399
|
+
|
|
385
400
|
export type ComputerHostExecFrameV1 =
|
|
386
401
|
| { type: "stdout"; dataBase64: string }
|
|
387
402
|
| { type: "stderr"; dataBase64: string }
|
|
@@ -606,6 +621,12 @@ function decodeOperation(
|
|
|
606
621
|
): ComputerHostOperationV1 {
|
|
607
622
|
switch (kind) {
|
|
608
623
|
case "open":
|
|
624
|
+
return {
|
|
625
|
+
kind,
|
|
626
|
+
...(value.stream === undefined
|
|
627
|
+
? {}
|
|
628
|
+
: { stream: boolean(value.stream, "Computer open stream") }),
|
|
629
|
+
};
|
|
609
630
|
case "cancel":
|
|
610
631
|
return { kind };
|
|
611
632
|
case "exec":
|
|
@@ -723,7 +744,7 @@ function decodeOperation(
|
|
|
723
744
|
|
|
724
745
|
const OPERATION_FIELDS: Record<ComputerHostOperationKindV1, readonly string[]> =
|
|
725
746
|
{
|
|
726
|
-
open: [],
|
|
747
|
+
open: ["stream"],
|
|
727
748
|
exec: [
|
|
728
749
|
"script",
|
|
729
750
|
"cwd",
|
|
@@ -1187,7 +1208,64 @@ export function decodeComputerHostCancelResultV1(
|
|
|
1187
1208
|
};
|
|
1188
1209
|
}
|
|
1189
1210
|
|
|
1190
|
-
// ---
|
|
1211
|
+
// --- stream frames ---------------------------------------------------------
|
|
1212
|
+
|
|
1213
|
+
/** One open-stream NDJSON line, newline included. */
|
|
1214
|
+
export function encodeComputerHostOpenFrameV1(
|
|
1215
|
+
frame: ComputerHostOpenFrameV1,
|
|
1216
|
+
): string {
|
|
1217
|
+
return `${JSON.stringify(frame)}\n`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export function decodeComputerHostOpenFrameV1(
|
|
1221
|
+
line: string,
|
|
1222
|
+
): ComputerHostOpenFrameV1 {
|
|
1223
|
+
let parsed: unknown;
|
|
1224
|
+
try {
|
|
1225
|
+
parsed = JSON.parse(line);
|
|
1226
|
+
} catch {
|
|
1227
|
+
fail("Computer open frame is not JSON");
|
|
1228
|
+
}
|
|
1229
|
+
const value = object(parsed, "Computer open frame");
|
|
1230
|
+
if (value.type === "progress") {
|
|
1231
|
+
exactly(value, ["type", "progress"], "Computer open frame");
|
|
1232
|
+
return {
|
|
1233
|
+
type: "progress",
|
|
1234
|
+
progress: decodeComputerHostProvisioningV1(
|
|
1235
|
+
value.progress,
|
|
1236
|
+
"Computer open frame",
|
|
1237
|
+
),
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
if (value.type === "result") {
|
|
1241
|
+
exactly(value, ["type", "result"], "Computer open frame");
|
|
1242
|
+
return {
|
|
1243
|
+
type: "result",
|
|
1244
|
+
result: decodeComputerHostOpenResultV1(value.result),
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (value.type === "error") {
|
|
1248
|
+
exactly(
|
|
1249
|
+
value,
|
|
1250
|
+
["type", "code", "message", "retryable"],
|
|
1251
|
+
"Computer open frame",
|
|
1252
|
+
);
|
|
1253
|
+
if (!ERROR_CODES.includes(value.code as ComputerHostErrorCodeV1)) {
|
|
1254
|
+
fail("Computer open frame code is invalid");
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
type: "error",
|
|
1258
|
+
code: value.code as ComputerHostErrorCodeV1,
|
|
1259
|
+
message: boundedString(
|
|
1260
|
+
value.message,
|
|
1261
|
+
COMPUTER_HOST_LIMITS.message,
|
|
1262
|
+
"Computer open frame message",
|
|
1263
|
+
),
|
|
1264
|
+
retryable: boolean(value.retryable, "Computer open frame retryable"),
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
return fail("Computer open frame type is invalid");
|
|
1268
|
+
}
|
|
1191
1269
|
|
|
1192
1270
|
/** One NDJSON line, newline included. */
|
|
1193
1271
|
export function encodeComputerHostExecFrameV1(
|
|
@@ -1267,31 +1345,48 @@ export function decodeComputerHostExecFrameV1(
|
|
|
1267
1345
|
* transport may split or coalesce anywhere, so a frame boundary is the newline
|
|
1268
1346
|
* this decoder finds and never the chunk the transport delivered.
|
|
1269
1347
|
*/
|
|
1270
|
-
|
|
1348
|
+
class ComputerHostFrameReaderV1<Frame> {
|
|
1271
1349
|
private buffer = "";
|
|
1272
1350
|
private readonly decoder = new TextDecoder();
|
|
1351
|
+
private readonly decodeFrame: (line: string) => Frame;
|
|
1352
|
+
|
|
1353
|
+
constructor(decodeFrame: (line: string) => Frame) {
|
|
1354
|
+
this.decodeFrame = decodeFrame;
|
|
1355
|
+
}
|
|
1273
1356
|
|
|
1274
1357
|
/** Frames completed by this chunk, in order. */
|
|
1275
|
-
push(chunk: Uint8Array | string):
|
|
1358
|
+
push(chunk: Uint8Array | string): Frame[] {
|
|
1276
1359
|
this.buffer +=
|
|
1277
1360
|
typeof chunk === "string"
|
|
1278
1361
|
? chunk
|
|
1279
1362
|
: this.decoder.decode(chunk, { stream: true });
|
|
1280
|
-
const frames:
|
|
1363
|
+
const frames: Frame[] = [];
|
|
1281
1364
|
let newline = this.buffer.indexOf("\n");
|
|
1282
1365
|
while (newline >= 0) {
|
|
1283
1366
|
const line = this.buffer.slice(0, newline).trim();
|
|
1284
1367
|
this.buffer = this.buffer.slice(newline + 1);
|
|
1285
|
-
if (line) frames.push(
|
|
1368
|
+
if (line) frames.push(this.decodeFrame(line));
|
|
1286
1369
|
newline = this.buffer.indexOf("\n");
|
|
1287
1370
|
}
|
|
1288
1371
|
return frames;
|
|
1289
1372
|
}
|
|
1290
1373
|
|
|
1291
1374
|
/** The trailing frame of a stream that ended without its final newline. */
|
|
1292
|
-
end():
|
|
1375
|
+
end(): Frame[] {
|
|
1293
1376
|
const line = this.buffer.trim();
|
|
1294
1377
|
this.buffer = "";
|
|
1295
|
-
return line ? [
|
|
1378
|
+
return line ? [this.decodeFrame(line)] : [];
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
export class ComputerHostOpenFrameReaderV1 extends ComputerHostFrameReaderV1<ComputerHostOpenFrameV1> {
|
|
1383
|
+
constructor() {
|
|
1384
|
+
super(decodeComputerHostOpenFrameV1);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
export class ComputerHostExecFrameReaderV1 extends ComputerHostFrameReaderV1<ComputerHostExecFrameV1> {
|
|
1389
|
+
constructor() {
|
|
1390
|
+
super(decodeComputerHostExecFrameV1);
|
|
1296
1391
|
}
|
|
1297
1392
|
}
|