clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

main
tribes / worker / src / validate.ts
validate.ts 2.7K
1/**2 * Script validation pipeline, stages 1-3 (size, encoding, syntax).3 *4 * The syntax stage is built around an injected `compile` (ScriptHost.compile5 * from @tribes/protocol) so unit tests can mock it and integration can wire6 * the real QuickJS host. The stage list is exported/composable so integration7 * appends a 'smoke' stage later: `createValidator([...baseStages(compile), smokeStage])`.8 */9import { LIMITS } from '@tribes/protocol';10import type { CompileResult, ValidationReport } from '@tribes/protocol';1112export type CompileFn = (source: string) => CompileResult | Promise<CompileResult>;1314export interface ValidationStage {15 /** Reported as ValidationReport.failedStage: 'size' | 'encoding' | 'syntax' | 'smoke'. */16 name: string;17 /** Returns null on pass, or a human/agent-readable failure detail. */18 run(source: string): string | null | Promise<string | null>;19}2021export const sizeStage: ValidationStage = {22 name: 'size',23 run(source) {24 const bytes = new TextEncoder().encode(source).byteLength;25 return bytes <= LIMITS.SCRIPT_MAX_BYTES26 ? null27 : `script is ${bytes} bytes; limit is ${LIMITS.SCRIPT_MAX_BYTES}`;28 },29};3031/** Lone UTF-16 surrogate = not encodable as valid UTF-8. */32const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;33/** Bidi override/isolate + LRM/RLM marks — trojan-source vectors. */34const BIDI_CONTROL = /[\u202A-\u202E\u2066-\u2069\u200E\u200F]/;3536export const encodingStage: ValidationStage = {37 name: 'encoding',38 run(source) {39 if (LONE_SURROGATE.test(source)) return 'source is not valid UTF-8 (lone surrogate)';40 const m = source.match(BIDI_CONTROL);41 if (m && m[0]) {42 const cp = m[0].codePointAt(0)!.toString(16).toUpperCase().padStart(4, '0');43 return `source contains forbidden bidi/control character U+${cp}`;44 }45 return null;46 },47};4849export function syntaxStage(compile: CompileFn): ValidationStage {50 return {51 name: 'syntax',52 async run(source) {53 const result = await compile(source);54 return result.ok ? null : (result.error ?? 'syntax error');55 },56 };57}5859/** Stages 1-3. Integration appends the smoke stage after these. */60export function baseStages(compile: CompileFn): ValidationStage[] {61 return [sizeStage, encodingStage, syntaxStage(compile)];62}6364/** Runs stages in order; first failure wins. */65export function createValidator(66 stages: ValidationStage[],67): (source: string) => Promise<ValidationReport> {68 return async (source) => {69 for (const stage of stages) {70 const detail = await stage.run(source);71 if (detail !== null) return { ok: false, failedStage: stage.name, detail };72 }73 return { ok: true, failedStage: null, detail: null };74 };75}