import { FileHelper, z } from '@start9labs/start-sdk' /** * Boardroom Map configuration, persisted to the `main` volume as config.json. * * Written by the StartOS actions (Configure Sparks / Models / Graders / * Grading / Companies) and read by the Python orchestrator inside the * container, which mounts the same volume at /data and reads /data/config.json. * Keep field names in sync with orchestrator/bm_config.py (CONFIG_DEFAULTS). * * Boardroom Map is a CONTROL PLANE: a GPU-free orchestrator on StartOS that * SSHes into one or two DGX Sparks to serve local models and grade the board * decks you drop into /data/inbox//. A panel of sandboxed * "grader" containers scores each deck against the BDEF v1.1 framework * (Girdley + Munger/Buffett); Python then computes a deterministic composite * (quant KPI attainment 60 incl. profitability 30, qualitative categories 40, * red-flag penalties up to -15) and appends it to the company's running * ledger. There is NO frontier model and NO cloud key — everything stays on * your hardware. The only secrets are the Spark SSH key and an optional * Hugging Face token (secrets.ts). */ export const configShape = z.object({ // --- Spark connection (mirrors LLaMA-Factory / Nightshift) --- primarySparkHost: z.string().default(''), primarySparkUser: z.string().default('nvidia'), sshPort: z.number().int().positive().default(22), // Second Spark for extra model capacity. null = single node. secondarySparkHost: z.string().nullable().default(null), useBothSparks: z.boolean().default(false), // Address the head Spark's own preflight checks use to reach the local proxy. headInternalHost: z.string().default('127.0.0.1'), // --- Remote execution --- remoteWorkDir: z.string().default('/home/nvidia/boardroom-map'), // --- Images (built ON the Sparks; not packed into the s9pk) --- servingImage: z.string().default('boardroom-vllm:latest'), graderImage: z.string().default('boardroom-grader:latest'), // --- Serving (vLLM on the Sparks) --- gpuMemoryUtilization: z.string().default('0.85'), maxModelLen: z.number().int().positive().default(32768), // vLLM tool-call parser for native function-calling (the grader's read-file // tool loop relies on it). Must match the served model family — Qwen3 → // 'hermes'. Empty disables native tool-calling (graders fall back to a // JSON-action text protocol). toolCallParser: z.string().default('hermes'), // LiteLLM router exposing every model alias on one OpenAI-compatible endpoint. proxyPort: z.number().int().positive().default(4000), // How many distinct models may be co-resident on the HEAD Spark at once. The // job runner loads models in WAVES so it never exceeds this — letting you run // a panel across more models than fit in GPU memory simultaneously. 1 is // safest. maxConcurrentModels: z.number().int().positive().default(1), // The MODEL CATALOG: the set of local models the service can serve. Each // grader (below) references one of these by `alias`. Mirrors // bm_config.py CONFIG_DEFAULTS["models"]. models: z .array( z.object({ alias: z.string(), hfModel: z.string(), // Which Spark serves this model. In `airgapped` network mode all models // must be on the head Spark (see networkMode). spark: z.enum(['primary', 'secondary']).default('primary'), port: z.number().int().positive().default(8001), }), ) .default([ { alias: 'grader-a', hfModel: 'Qwen/Qwen3-32B-FP8', spark: 'primary', port: 8001 }, ]), // --- The grading panel: one entry per grader --- // Each grader is a model + a persona (the lens it grades through — e.g. a // Munger-style inversion skeptic or a Girdley-style operator) + an optional // temperature. Mirrors bm_config.py CONFIG_DEFAULTS["graders"]. graders: z .array( z.object({ name: z.string(), // Must match one of the model catalog aliases above. model: z.string(), persona: z.string().nullable().default(''), temperature: z.number().nullable().default(null), }), ) .default([ // >= 2 graders required: every deck needs >= 2 valid grade reports. { name: 'munger-lens', model: 'grader-a', persona: '', temperature: null }, { name: 'girdley-operator', model: 'grader-a', persona: '', temperature: null }, ]), // Which catalog model runs the stage-1 structured KPI extractor over each // deck. Empty = first model in the catalog. extractorModel: z.string().default(''), // --- Grading job settings --- // The rubric override. Empty = the baked-in BDEF v1.1 framework // (orchestrator/bdef.md — Girdley + Munger/Buffett). Non-empty text replaces // it wholesale, so include scoring categories A-H if you customize. bdefOverride: z.string().default(''), // Deterministic-scorer knobs. The composite is 0-100 = quant 60 (profitability // 30 + other KPIs 20 + forecast integrity 10) + qualitative 40 (8 BDEF // categories x 5) - red-flag penalties (capped at 15). Mirrors // bm_config.py WEIGHTS_DEFAULTS; keep both in sync. weights: z .object({ // Points for the profitability KPI attainment bucket (heaviest weight). profitabilityKpi: z.number().default(30), // Points for the non-profitability measurable-KPI bucket. otherKpi: z.number().default(20), // Points for forecast integrity: deck N actuals vs deck N-1 stated targets. forecastIntegrity: z.number().default(10), // Max points per qualitative BDEF category A-H (8 x 5 = 40). qualCategoryMax: z.number().default(5), // Cap on total red-flag penalty. redFlagCap: z.number().default(15), // actual/target ratio below which a KPI earns zero credit. kpiCreditFloor: z.number().default(0.5), // Penalty per KPI that silently disappeared from the deck. droppedKpiPenalty: z.number().default(2), // Count at most this many dropped-KPI flags. droppedKpiMax: z.number().default(3), // Quote characters required for full qualitative-evidence weight. evidenceFullCredit: z.number().default(400), // Damping factor for red flags raised by a single grader only. singleSourceFlagFactor: z.number().default(0.5), }) .default({ profitabilityKpi: 30, otherKpi: 20, forecastIntegrity: 10, qualCategoryMax: 5, redFlagCap: 15, kpiCreditFloor: 0.5, droppedKpiPenalty: 2, droppedKpiMax: 3, evidenceFullCredit: 400, singleSourceFlagFactor: 0.5, }), // Confidentiality posture for the grader containers: // 'airgapped' — graders join an --internal Docker network: they can // reach ONLY the on-Spark model proxy, with zero internet // egress. Models must be pre-pulled into the Spark's HF // cache (no live download). All models must be on the head // Spark. Strongest confidentiality. // 'local_services' — graders may also reach configured LAN services // (e.g. SearXNG) and the second Spark. NOTE: this network // has egress unless you firewall it — use only when you // accept that graders can reach the network. networkMode: z.enum(['airgapped', 'local_services']).default('airgapped'), // SearXNG JSON endpoint, used ONLY in local_services mode to give graders a // web_search tool. Empty = no web search. searxngUrl: z.string().default(''), // --- Adjudication (a local lead grader; no frontier model) --- adjudicatorEnabled: z.boolean().default(true), // Alias of the model that reconciles the panel's grades. Empty = first model. adjudicatorModel: z.string().default(''), // Optional persona/instructions for the adjudicator. Empty = built-in default. adjudicatorPersona: z.string().default(''), // --- Document handling --- // After a job, wipe the extracted deck text from the Sparks. Scorecards are // kept on the StartOS box regardless. Default true for confidentiality. wipeRemoteDocs: z.boolean().default(true), // Watch /data/inbox and auto-start grading when decks land (debounced). // Default false: you trigger grading explicitly with "Grade Decks". autoRunOnDrop: z.boolean().default(false), // Name of the per-job Docker network created on the head Spark. networkName: z.string().default('boardroom-net'), // Space/comma-separated docker container names stopped on the head Spark at // the start of every job to free GPU memory (e.g. an always-on vLLM another // service runs). NOT restarted afterwards — their owner reloads them. preJobStopContainers: z.string().default(''), // --- Portfolio companies --- // The authoritative source of pinned KPI targets and KPI-name aliases. Decks // are dropped into /data/inbox// and each company keeps its own running // scorecard ledger. Mirrors bm_config.py CONFIG_DEFAULTS["companies"]. companies: z .array( z.object({ // Directory name under /data/inbox and the ledger key. Stable — do not // rename once decks have been graded. slug: z.string(), // Display name for the dashboard. Empty = the slug. name: z.string().default(''), // Newline-separated "canonical=alias1;alias2" lines mapping the names a // deck uses for a KPI onto its canonical name. kpiAliases: z.string().default(''), // Targets the scorer holds the company to even when a deck goes quiet // about them. `profitability: true` marks the KPI as part of the // heavier profitability bucket. pinnedTargets: z .array( z.object({ kpi: z.string(), target: z.number(), unit: z.string().default(''), direction: z.enum(['gte', 'lte']).default('gte'), profitability: z.boolean().default(false), }), ) .default([]), }), ) .default([]), // --- Auth flags (the secret itself lives in secrets.ts) --- hfTokenSet: z.boolean().default(false), }) export type Config = z.infer // Absolute path into the `main` volume (mounted at /media/startos/volumes/main // in the JS runtime, /data in the orchestrator container). A relative path here // resolves against the JS runtime's EPHEMERAL working directory and silently // loses the config on restart. export const configFile = FileHelper.json( '/media/startos/volumes/main/config.json', configShape, )