Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-cli/src/packet.ts

Snapshot at 34e3175

Containing directory

4,600 bytes · typescriptView raw
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