Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-cli/src/packet.ts
Snapshot at 34e3175
1import { open } from "node:fs/promises";
2import { CliError, record, slug } from "./safety.ts";
3
4export async function smallFile(path: string): Promise<string> {
5 const file = await open(path, "r");
6 try {
7 if (!(await file.stat()).isFile()) throw new CliError("Input must be a regular file.");
8 const bytes = Buffer.alloc(16385);
9 let size = 0;
10 while (size < bytes.length) {
11 const { bytesRead } = await file.read(bytes, size, bytes.length - size);
12 if (!bytesRead) break;
13 size += bytesRead;
14 }
15 if (size > 16384) throw new CliError("Input file exceeds 16 KiB.");
16 return bytes.subarray(0, size).toString("utf8");
17 } finally {
18 await file.close();
19 }
20}
21
22// Intentionally small YAML subset: string scalars/lists and a numeric tokenBudget.
23// No YAML tags, aliases, flow mappings, multiline values or executable extensions.
24function scalar(text: string): string {
25 if (!text || /^[&*!>|[{]/.test(text) || /\s+#/.test(text))
26 throw new CliError("Unsupported YAML value; use a quoted scalar or JSON.");
27 if (text.startsWith('"')) {
28 const value: unknown = JSON.parse(text);
29 if (typeof value !== "string") throw new CliError("YAML values must be strings.");
30 return value;
31 }
32 if (text.startsWith("'")) {
33 if (!/^'(?:[^']|'')*'$/.test(text)) throw new CliError("Invalid quoted YAML value.");
34 return text.slice(1, -1).replaceAll("''", "'");
35 }
36 return text;
37}
38
39function yaml(text: string): Record<string, unknown> {
40 const result: Record<string, unknown> = Object.create(null);
41 let list: string[] | undefined;
42 for (const line of text.split(/\r?\n/)) {
43 if (!line.trim() || line.trimStart().startsWith("#")) continue;
44 const item = /^ - (.+)$/.exec(line);
45 if (item && list) {
46 list.push(scalar(item[1]!.trim()));
47 continue;
48 }
49 const field = /^([a-zA-Z]+):(?: (.*))?$/.exec(line);
50 if (!field || Object.hasOwn(result, field[1]!))
51 throw new CliError("Invalid or duplicate YAML field.");
52 list = undefined;
53 if (!field[2]) result[field[1]!] = list = [];
54 else {
55 const scalarText = field[2].trim();
56 // Only an unquoted JSON number in this numeric field changes scalar type.
57 result[field[1]!] =
58 field[1] === "tokenBudget" &&
59 /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(scalarText)
60 ? Number(scalarText)
61 : scalar(scalarText);
62 }
63 }
64 return result;
65}
66
67export function packet(text: string): Record<string, unknown> {
68 const value: unknown = text.trimStart().startsWith("{") ? JSON.parse(text) : yaml(text);
69 const fields = ["id", "title", "goal", "paths", "acceptance", "mode", "tokenBudget", "judge"];
70 if (!record(value) || Object.keys(value).some((key) => !fields.includes(key)))
71 throw new CliError(
72 "Packet must contain only id, title, goal, paths, acceptance, mode, tokenBudget and judge.",
73 );
74 slug(typeof value.id === "string" ? value.id : undefined);
75 if (value.mode !== undefined && value.mode !== "standard" && value.mode !== "race")
76 throw new CliError("Packet mode must be standard or race.");
77 if (value.tokenBudget !== undefined) {
78 if (value.mode !== "race") throw new CliError("Packet tokenBudget requires mode: race.");
79 if (
80 typeof value.tokenBudget !== "number" ||
81 !Number.isSafeInteger(value.tokenBudget) ||
82 value.tokenBudget <= 0
83 )
84 throw new CliError("Packet tokenBudget must be a positive safe integer.");
85 }
86 if (value.judge !== undefined) {
87 if (value.mode !== "race") throw new CliError("Packet judge requires mode: race.");
88 try {
89 slug(typeof value.judge === "string" ? value.judge : undefined, 32);
90 } catch {
91 throw new CliError(
92 "Packet judge must be a lowercase agent ID of at most 32 characters with single internal hyphens.",
93 );
94 }
95 }
96 for (const [field, max] of [
97 ["title", 200],
98 ["goal", 4000],
99 ] as const) {
100 if (typeof value[field] !== "string" || !value[field].trim() || value[field].length > max)
101 throw new CliError("Invalid packet title or goal.");
102 }
103 for (const [field, count, length] of [
104 ["paths", 200, 1024],
105 ["acceptance", 100, 2000],
106 ] as const) {
107 const items = value[field];
108 if (
109 !Array.isArray(items) ||
110 !items.length ||
111 items.length > count ||
112 !items.every((item) => typeof item === "string" && item.trim() && item.length <= length)
113 )
114 throw new CliError("Invalid packet paths or acceptance list.");
115 }
116 // Core is authoritative for paths and judge enrollment; never create claims here.
117 return value;
118}
119