Scaffold: fork of Chambers architecture, renamed to Boardroom Map

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-06 13:10:25 -05:00
co-authored by Claude Fable 5
commit 1dde915540
48 changed files with 4025 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
const { InputSpec, Value, List } = sdk
const inputSpec = InputSpec.of({
reviewers: Value.list(
List.obj(
{
name: 'Review Panel',
description:
'Who sits on the panel — one entry per review. Add as many as you like. ' +
'Each reviewer is a model from your catalog plus a PERSONA: the lens it ' +
'reads through, so the same document gets examined from different angles.',
default: [],
minLength: 1,
maxLength: 32,
},
{
uniqueBy: 'name',
displayAs: '{{name}} ({{model}})',
spec: InputSpec.of({
name: Value.text({
name: 'Name',
description: 'Unique reviewer name. Becomes its container and report filename.',
required: true,
default: null,
placeholder: 'risk-counsel',
patterns: [
{ regex: '^[A-Za-z0-9][A-Za-z0-9 _-]{0,40}$',
description: 'Letters, numbers, spaces, dashes, underscores (max 41 chars).' },
],
}),
model: Value.text({
name: 'Model Alias',
description: 'Which catalog model this reviewer uses (must match an alias from "Configure Models").',
required: true,
default: null,
placeholder: 'reviewer-a',
}),
persona: Value.textarea({
name: 'Persona / Lens',
description:
'How THIS reviewer should read the documents — its priorities and ' +
'weighting. Injected into its system prompt. e.g. "You are skeptical ' +
'legal counsel: weight liability, ambiguous obligations, and missing ' +
'clauses above everything." Leave empty for a neutral reviewer.',
required: false,
default: null,
minRows: 3,
maxRows: 16,
placeholder:
'You are a financial-controls reviewer. Focus on numbers that do not ' +
'reconcile, unstated assumptions behind projections, and anything that ' +
'would concern an auditor. Organize findings by severity.',
}),
temperature: Value.number({
name: 'Sampling Temperature (optional)',
description:
'Best-effort per-reviewer sampling temperature for extra diversity. ' +
'Persona is the primary lever. Leave empty to use the model default.',
required: false,
default: null,
integer: false,
min: 0,
max: 2,
}),
}),
},
),
),
})
export const configureReviewers = sdk.Action.withInput(
'configure-reviewers',
async ({ effects }) => ({
name: 'Configure Reviewers',
description: 'Define the review panel: which models and which personas, and how many reviews.',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return { reviewers: cfg.reviewers }
},
async ({ effects, input }) => {
await configFile.merge(effects, { reviewers: input.reviewers })
return {
version: '1',
title: 'Panel Configured',
message:
'Saved a panel of ' + input.reviewers.length + ' reviewer(s). Each model ' +
'alias must exist in "Configure Models". Set the rubric in "Configure ' +
'Review", then drop documents and run a review.',
result: {
type: 'single',
value: input.reviewers.map((r) => r.name).join(', '),
copyable: false,
qr: false,
masked: false,
},
}
},
)
+132
View File
@@ -0,0 +1,132 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
reviewInstructions: Value.textarea({
name: 'Review Rubric',
description:
'What every reviewer should look for and produce. Layered above each ' +
'reviewer\'s persona. Be concrete about the structure you want back.',
required: true,
default: null,
minRows: 5,
maxRows: 20,
placeholder:
'Review the attached document(s). Produce: a short summary, key findings, ' +
'risks/red flags, open questions, and recommendations. Cite the document and ' +
'section for each point. Never invent facts not present in the documents.',
}),
networkMode: Value.select({
name: 'Network Mode',
description:
'Air-gapped: reviewers reach ONLY the on-Spark model proxy — zero internet, ' +
'documents never leave your hardware (models must be pre-pulled into the ' +
'Spark HF cache, all on the head Spark). Local services: reviewers may also ' +
'reach LAN services like SearXNG and the second Spark (this network has ' +
'egress unless you firewall it).',
default: 'airgapped',
values: {
airgapped: 'Air-gapped (no network, recommended)',
local_services: 'Local services (SearXNG / 2nd Spark)',
},
}),
searxngUrl: Value.text({
name: 'SearXNG URL (local-services only)',
description: 'JSON-search endpoint to give reviewers a web_search tool. Ignored in air-gapped mode. Empty = no web search.',
required: false,
default: null,
placeholder: 'https://searxng.local',
}),
synthesisEnabled: Value.toggle({
name: 'Synthesize a Consolidated Report',
description:
'After the panel finishes, run a local "lead reviewer" that reads all the ' +
'individual reports and writes one consolidated report (themes, conflicts, ' +
'consensus, recommendation). No frontier model — stays on the Sparks.',
default: true,
}),
synthesisModel: Value.text({
name: 'Lead Reviewer Model (optional)',
description: 'Catalog alias of the model that writes the consolidated report. Empty = use the first model.',
required: false,
default: null,
placeholder: 'reviewer-a',
}),
synthesisPersona: Value.textarea({
name: 'Lead Reviewer Instructions (optional)',
description: 'Override how the consolidated report is written. Empty = a sensible built-in default.',
required: false,
default: null,
minRows: 3,
maxRows: 14,
}),
wipeRemoteDocs: Value.toggle({
name: 'Wipe Documents From Sparks After Review',
description:
'Delete the extracted document text from the Sparks when a job finishes. ' +
'Reports are always kept on this StartOS box. Recommended for confidential material.',
default: true,
}),
autoRunOnDrop: Value.toggle({
name: 'Auto-run When Documents Are Dropped',
description:
'Start a review automatically (after a short debounce) whenever new files ' +
'land in the inbox. Off by default so you trigger reviews explicitly.',
default: false,
}),
})
export const configureReview = sdk.Action.withInput(
'configure-review',
async ({ effects }) => ({
name: 'Configure Review',
description: 'Set the rubric, air-gap mode, synthesis, and document retention.',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return {
reviewInstructions: cfg.reviewInstructions,
networkMode: cfg.networkMode,
searxngUrl: cfg.searxngUrl || undefined,
synthesisEnabled: cfg.synthesisEnabled,
synthesisModel: cfg.synthesisModel || undefined,
synthesisPersona: cfg.synthesisPersona || undefined,
wipeRemoteDocs: cfg.wipeRemoteDocs,
autoRunOnDrop: cfg.autoRunOnDrop,
}
},
async ({ effects, input }) => {
await configFile.merge(effects, {
reviewInstructions: input.reviewInstructions,
networkMode: input.networkMode,
searxngUrl: input.searxngUrl ?? '',
synthesisEnabled: input.synthesisEnabled,
synthesisModel: input.synthesisModel ?? '',
synthesisPersona: input.synthesisPersona ?? '',
wipeRemoteDocs: input.wipeRemoteDocs,
autoRunOnDrop: input.autoRunOnDrop,
})
return {
version: '1',
title: 'Review Settings Saved',
message:
input.networkMode === 'airgapped'
? 'Saved. Reviewers will run air-gapped (no internet). Ensure all models are on the head Spark and pre-pulled into its HF cache.'
: 'Saved. Reviewers run in local-services mode and may reach the network — make sure that is acceptable for these documents.',
result: { type: 'single', value: input.networkMode, copyable: false, qr: false, masked: false },
}
},
)
+159
View File
@@ -0,0 +1,159 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
const { InputSpec, Value, List } = sdk
const inputSpec = InputSpec.of({
models: Value.list(
List.obj(
{
name: 'Model Catalog',
description:
'The local models this service can serve on your Sparks. Each reviewer ' +
'references one of these by its alias. The job runner loads models in ' +
'waves so you can run a panel across more models than fit in GPU memory ' +
'at once.',
default: [],
minLength: 1,
maxLength: 16,
},
{
uniqueBy: 'alias',
displayAs: '{{alias}} → {{hfModel}}',
spec: InputSpec.of({
alias: Value.text({
name: 'Alias',
description: 'Short name reviewers use to pick this model (e.g. "qwen-32b").',
required: true,
default: null,
placeholder: 'reviewer-a',
patterns: [
{ regex: '^[a-z0-9][a-z0-9-]{0,30}$',
description: 'Lowercase letters, numbers, dashes (max 31 chars).' },
],
}),
hfModel: Value.text({
name: 'Hugging Face Model ID',
description: 'The model vLLM serves. Must be present in the Spark HF cache for air-gapped mode.',
required: true,
default: null,
placeholder: 'Qwen/Qwen3-32B-FP8',
}),
spark: Value.select({
name: 'Served On',
description:
'Which Spark serves this model. Air-gapped review mode requires the ' +
'head (primary) Spark; the secondary is used only in local-services mode.',
default: 'primary',
values: { primary: 'Primary (head) Spark', secondary: 'Secondary Spark' },
}),
port: Value.number({
name: 'vLLM Port',
description: 'Host port the vLLM container for this model listens on. Unique per Spark.',
required: true,
default: 8001,
integer: true,
min: 1,
max: 65535,
}),
}),
},
),
),
gpuMemoryUtilization: Value.text({
name: 'GPU Memory Utilization',
description: 'vLLM --gpu-memory-utilization (01). Lower it if you co-resident multiple models per Spark.',
required: true,
default: '0.85',
}),
maxModelLen: Value.number({
name: 'Max Model Length',
description: 'vLLM --max-model-len (context window). Documents are chunked to fit.',
required: true,
default: 32768,
integer: true,
min: 2048,
}),
toolCallParser: Value.text({
name: 'Tool-Call Parser',
description:
'vLLM tool-call parser for the reviewer\'s read-file tool loop. Match the ' +
'served model family (Qwen3 → "hermes"). Empty disables native tool-calling.',
required: false,
default: 'hermes',
}),
maxConcurrentModels: Value.number({
name: 'Max Co-resident Models (head Spark)',
description:
'How many distinct models may load on the head Spark at once. The job runner ' +
'loads models in waves so it never exceeds this. 1 is safest.',
required: true,
default: 1,
integer: true,
min: 1,
max: 8,
}),
proxyPort: Value.number({
name: 'Model Proxy Port',
description: 'Port for the on-Spark LiteLLM router that exposes every model alias on one endpoint.',
required: true,
default: 4000,
integer: true,
min: 1,
max: 65535,
}),
})
export const configureModels = sdk.Action.withInput(
'configure-models',
async ({ effects }) => ({
name: 'Configure Models',
description: 'Define the local model catalog served on your Sparks and the serving knobs.',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return {
models: cfg.models,
gpuMemoryUtilization: cfg.gpuMemoryUtilization,
maxModelLen: cfg.maxModelLen,
toolCallParser: cfg.toolCallParser,
maxConcurrentModels: cfg.maxConcurrentModels,
proxyPort: cfg.proxyPort,
}
},
async ({ effects, input }) => {
await configFile.merge(effects, {
models: input.models,
gpuMemoryUtilization: input.gpuMemoryUtilization,
maxModelLen: input.maxModelLen,
toolCallParser: input.toolCallParser ?? '',
maxConcurrentModels: input.maxConcurrentModels,
proxyPort: input.proxyPort,
})
return {
version: '1',
title: 'Models Configured',
message:
'Saved ' + input.models.length + ' model(s). Make sure each is present in ' +
'the Spark HF cache for air-gapped runs, then set "Configure Reviewers".',
result: {
type: 'single',
value: input.models.map((m) => m.alias).join(', '),
copyable: false,
qr: false,
masked: false,
},
}
},
)
+161
View File
@@ -0,0 +1,161 @@
import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
import { sshKeyFile, hfTokenFile } from '../file-models/secrets'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
primarySparkHost: Value.text({
name: 'Primary Spark Host',
description: 'Hostname or IP of the head DGX Spark (reachable over SSH). Serves models, hosts the model proxy, and runs the reviewer panel.',
required: true,
default: null,
placeholder: 'spark-01.local',
}),
primarySparkUser: Value.text({
name: 'SSH User',
description: 'The login user on the Spark (DGX OS default is "nvidia").',
required: true,
default: 'nvidia',
}),
sshPort: Value.number({
name: 'SSH Port',
description: 'SSH port on the Spark.',
required: true,
default: 22,
integer: true,
min: 1,
max: 65535,
}),
sshPrivateKey: Value.textarea({
name: 'SSH Private Key',
description:
'A private key (PEM/OpenSSH) whose public half is in the Spark user\'s ' +
'~/.ssh/authorized_keys. Stored in this service\'s private volume and ' +
'used only to reach your Sparks. Paste the FULL key including header/footer.',
warning:
'This is a credential. It is written to the service volume and never ' +
'shown again. Use a dedicated key for this service.',
required: true,
default: null,
minRows: 6,
maxRows: 14,
placeholder: '-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----',
}),
useBothSparks: Value.toggle({
name: 'Use Both Sparks',
description:
'Allow models to be served on a second Spark (over ConnectX/200GbE) for ' +
'extra capacity. NOTE: in air-gapped review mode all models must run on the ' +
'head Spark; the second Spark is used only in local-services mode.',
default: false,
}),
secondarySparkHost: Value.text({
name: 'Secondary Spark Host',
description: 'Hostname/IP of the second Spark. Required only if "Use Both Sparks" is on.',
required: false,
default: null,
placeholder: 'spark-02.local',
}),
headInternalHost: Value.text({
name: 'Head Internal Host',
description:
'Address the head Spark uses for its own preflight checks against the local ' +
'model proxy. Single Spark: 127.0.0.1 is fine.',
required: true,
default: '127.0.0.1',
}),
remoteWorkDir: Value.text({
name: 'Remote Work Directory',
description: 'Absolute path on the head Spark for staged document text, the HF cache, and logs.',
required: true,
default: '/home/nvidia/boardroom-map',
}),
servingImage: Value.text({
name: 'vLLM Image Tag',
description: 'The vLLM serving image built on the Sparks (e.g. via spark-vllm-docker).',
required: true,
default: 'boardroom-vllm:latest',
}),
graderImage: Value.text({
name: 'Reviewer Image Tag',
description: 'The sandboxed reviewer image built on the head Spark from sandbox/build.sh.',
required: true,
default: 'boardroom-grader:latest',
}),
hfToken: Value.text({
name: 'Hugging Face Token (optional)',
description:
'Only needed for gated/private models or to warm a model the first time. ' +
'Leave empty to keep the existing token unchanged. Passed to the vLLM ' +
'container at serve time.',
required: false,
default: null,
masked: true,
}),
})
export const configureSparks = sdk.Action.withInput(
'configure-sparks',
async ({ effects }) => ({
name: 'Configure Sparks',
description: 'Set the DGX Spark connection details, SSH credentials, and image tags.',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
inputSpec,
// Prefill non-secret fields from existing config. Never prefill the key/token.
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return {
primarySparkHost: cfg.primarySparkHost || undefined,
primarySparkUser: cfg.primarySparkUser,
sshPort: cfg.sshPort,
useBothSparks: cfg.useBothSparks,
secondarySparkHost: cfg.secondarySparkHost ?? undefined,
headInternalHost: cfg.headInternalHost,
remoteWorkDir: cfg.remoteWorkDir,
servingImage: cfg.servingImage,
graderImage: cfg.graderImage,
}
},
async ({ effects, input }) => {
// Persist the private key to its own file (600 enforced in-container).
await sshKeyFile.write(effects, input.sshPrivateKey.trim() + '\n')
let hfTokenSet = (await configFile.read().const(effects))?.hfTokenSet ?? false
if (input.hfToken && input.hfToken.trim()) {
await hfTokenFile.write(effects, input.hfToken.trim())
hfTokenSet = true
}
await configFile.merge(effects, {
primarySparkHost: input.primarySparkHost,
primarySparkUser: input.primarySparkUser,
sshPort: input.sshPort,
useBothSparks: input.useBothSparks,
secondarySparkHost: input.secondarySparkHost,
headInternalHost: input.headInternalHost,
remoteWorkDir: input.remoteWorkDir,
servingImage: input.servingImage,
graderImage: input.graderImage,
hfTokenSet,
})
return {
version: '1',
title: 'Sparks Configured',
message:
'Saved. Use "Test Spark Connection" to verify SSH + GPU access, then set ' +
'"Configure Models" and "Configure Reviewers".',
result: { type: 'single', value: input.primarySparkHost, copyable: false, qr: false, masked: false },
}
},
)
+59
View File
@@ -0,0 +1,59 @@
import { startSdk } from '@start9labs/start-sdk'
import { sdk } from '../sdk'
/**
* Trigger a review of whatever is currently in the inbox. The running job-runner
* thread polls for /data/state/run_request and starts a job when it appears, so
* this action just drops that request file (decoupled from the daemon — no need
* to reach its HTTP port from the action's one-shot container).
*/
export const runReview = sdk.Action.withoutInput(
'run-review',
async ({ effects }) => ({
name: 'Run Review',
description: 'Convene the panel now over the documents currently in the inbox.',
warning: null,
allowedStatuses: 'only-running',
group: null,
visibility: 'enabled',
}),
async ({ effects }) => {
const mounts = sdk.Mounts.of().mountVolume({
volumeId: 'main',
mountpoint: '/data',
subpath: null,
readonly: false,
})
let output: string
try {
const { stdout } = await startSdk.runCommand<typeof sdk.manifest>(
effects,
{ imageId: 'main' },
[
'sh',
'-c',
'mkdir -p /data/state && date +%s > /data/state/run_request && ' +
'n=$(ls -1 /data/inbox 2>/dev/null | wc -l | tr -d " "); ' +
'echo "Review requested. $n file(s) in the inbox."',
],
{ mounts, env: { BM_DATA_DIR: '/data' } },
'run-review',
)
output = (stdout?.toString() || '').trim() || 'Review requested.'
} catch (e: any) {
output = 'Could not request a review: ' + (e?.message || String(e))
}
return {
version: '1',
title: 'Review Requested',
message:
output +
' Watch the Web UI for progress; reports appear there and via "View Latest Report".',
result: { type: 'single', value: output, copyable: false, qr: false, masked: false },
}
},
)
+17
View File
@@ -0,0 +1,17 @@
import { sdk } from '../sdk'
import { configureSparks } from './configure-sparks'
import { configureModels } from './configure-models'
import { configureReviewers } from './configure-reviewers'
import { configureReview } from './configure-review'
import { runReview } from './run-review'
import { testConnection } from './test-connection'
import { latestReport } from './latest-report'
export const actions = sdk.Actions.of()
.addAction(configureSparks)
.addAction(configureModels)
.addAction(configureReviewers)
.addAction(configureReview)
.addAction(runReview)
.addAction(testConnection)
.addAction(latestReport)
+51
View File
@@ -0,0 +1,51 @@
import { startSdk } from '@start9labs/start-sdk'
import { sdk } from '../sdk'
/**
* Returns the latest report as a copyable result, so you can read it straight
* from the StartOS service page without opening the Web UI. The job runner saves
* the most recent report (consolidated if synthesis is on, else the panel
* digest) to /data/reports/latest.md on the StartOS host — no Spark round-trip.
*/
export const latestReport = sdk.Action.withoutInput(
'latest-report',
async ({ effects }) => ({
name: 'View Latest Report',
description: 'Show the most recent review report produced by the panel.',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
async ({ effects }) => {
const mounts = sdk.Mounts.of().mountVolume({
volumeId: 'main',
mountpoint: '/data',
subpath: null,
readonly: true,
})
let report: string
try {
const { stdout } = await startSdk.runCommand<typeof sdk.manifest>(
effects,
{ imageId: 'main' },
['sh', '-c', 'cat /data/reports/latest.md 2>/dev/null || echo "(no report yet — drop documents in the inbox and run a review)"'],
{ mounts, env: { BM_DATA_DIR: '/data' } },
'latest-report',
)
report = (stdout?.toString() || '').trim() || '(no report yet)'
} catch (e: any) {
report = 'Could not read report: ' + (e?.message || String(e))
}
return {
version: '1',
title: 'Latest Boardroom Map Report',
message: 'The panel\'s most recent review.',
result: { type: 'single', value: report, copyable: true, qr: false, masked: false },
}
},
)
+55
View File
@@ -0,0 +1,55 @@
import { startSdk } from '@start9labs/start-sdk'
import { sdk } from '../sdk'
/**
* Runs a one-shot in the orchestrator image that SSHes into the configured
* Spark(s) and reports `nvidia-smi` plus whether the vLLM + reviewer images are
* built. Reuses orchestrator/spark_client.py so SSH logic lives in one place.
*/
export const testConnection = sdk.Action.withoutInput(
'test-connection',
async ({ effects }) => ({
name: 'Test Spark Connection',
description: 'SSH into the configured Spark(s) and verify GPU + serving/reviewer image access.',
warning: null,
allowedStatuses: 'any',
group: null,
visibility: 'enabled',
}),
async ({ effects }) => {
const mounts = sdk.Mounts.of().mountVolume({
volumeId: 'main',
mountpoint: '/data',
subpath: null,
readonly: true,
})
let output: string
try {
const { stdout, stderr } = await startSdk.runCommand<typeof sdk.manifest>(
effects,
{ imageId: 'main' },
['python3', '/app/spark_client.py', 'test'],
{ mounts, env: { BM_DATA_DIR: '/data' } },
'spark-test',
)
output =
(stdout?.toString() || '').trim() +
(stderr?.toString().trim() ? '\n\n[stderr]\n' + stderr.toString().trim() : '')
} catch (e: any) {
output =
'Connection test failed.\n\n' +
(e?.stdout?.toString() || '') +
(e?.stderr?.toString() || e?.message || String(e))
}
return {
version: '1',
title: 'Spark Connection Test',
message: 'Result of probing your Spark(s) over SSH.',
result: { type: 'single', value: output || '(no output)', copyable: true, qr: false, masked: false },
}
},
)
+132
View File
@@ -0,0 +1,132 @@
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 / Reviewers /
* Review) 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 run a panel of sandboxed
* "reviewer" containers over confidential documents you drop in. 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 reviewer's read-file
// tool loop relies on it). Must match the served model family — Qwen3 →
// 'hermes'. Empty disables native tool-calling (reviewers 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
// reviewer (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: 'reviewer-a', hfModel: 'Qwen/Qwen3-32B-FP8', spark: 'primary', port: 8001 },
]),
// --- The review panel: one entry per reviewer ("number of reviews") ---
// Each reviewer is a model + a persona (the lens it reads through) + an
// optional temperature. Mirrors bm_config.py CONFIG_DEFAULTS["reviewers"].
reviewers: 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([
{ name: 'reviewer-1', model: 'reviewer-a', persona: '', temperature: null },
]),
// --- Review job settings ---
// The rubric: what every reviewer should look for / produce. Layered above
// each reviewer's persona.
reviewInstructions: z.string().default(
'Review the attached document(s). Produce a structured report: a 3-5 sentence ' +
'summary, the key findings and insights, risks or red flags, open questions, ' +
'and concrete recommendations. Cite the document and section for each point. ' +
'Be honest about uncertainty; never invent facts not present in the documents.',
),
// Confidentiality posture for the reviewer containers:
// 'airgapped' — reviewers 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' — reviewers 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 reviewers can reach the network.
networkMode: z.enum(['airgapped', 'local_services']).default('airgapped'),
// SearXNG JSON endpoint, used ONLY in local_services mode to give reviewers a
// web_search tool. Empty = no web search.
searxngUrl: z.string().default(''),
// --- Synthesis (a local lead reviewer; no frontier model) ---
synthesisEnabled: z.boolean().default(true),
// Alias of the model that writes the consolidated report. Empty = first model.
synthesisModel: z.string().default(''),
// Optional persona/instructions for the lead reviewer. Empty = built-in default.
synthesisPersona: z.string().default(''),
// --- Document handling ---
// After a job, wipe the extracted document text from the Sparks. Reports are
// kept on the StartOS box regardless. Default true for confidentiality.
wipeRemoteDocs: z.boolean().default(true),
// Watch /data/inbox and auto-start a review when files land (debounced).
// Default false: you trigger reviews explicitly with "Run Review".
autoRunOnDrop: z.boolean().default(false),
// Name of the per-job Docker network created on the head Spark.
networkName: z.string().default('boardroom-net'),
// --- Auth flags (the secret itself lives in secrets.ts) ---
hfTokenSet: z.boolean().default(false),
})
export type Config = z.infer<typeof configShape>
export const configFile = FileHelper.json('./config.json', configShape)
+24
View File
@@ -0,0 +1,24 @@
import { FileHelper } from '@start9labs/start-sdk'
/**
* SSH private key used to reach the Sparks, stored as a standalone file in the
* `main` volume. Kept out of config.json so it is never returned in plaintext
* config reads. The container copies it to a 600 path at runtime.
*
* Volume path './ssh/id_spark' -> /data/ssh/id_spark inside the container.
*/
export const sshKeyFile = FileHelper.string('./ssh/id_spark')
/**
* Optional Hugging Face token (for gated/private model pulls on the Sparks).
* Passed to the vLLM serving container as HF_TOKEN at launch. In `airgapped`
* network mode models are served from a pre-pulled cache, so this is only used
* the first time you warm a model (or in local_services mode).
*
* Volume path './secrets/hf_token' -> /data/secrets/hf_token.
*
* NOTE: Boardroom Map has NO frontier/cloud key. There is deliberately no Anthropic
* key and no Gitea token — the whole point is that confidential documents and
* their reviews never leave your hardware.
*/
export const hfTokenFile = FileHelper.string('./secrets/hf_token')
+24
View File
@@ -0,0 +1,24 @@
import { buildManifest } from '@start9labs/start-sdk'
import { sdk } from './sdk'
import { versions } from './versions'
import { actions } from './actions'
import { setInterfaces } from './interfaces'
import { manifest as sdkManifest } from './manifest'
// Required ABI exports for a StartOS service package. The PUBLISHABLE manifest is
// the static manifest combined with version-graph metadata (version, release
// notes, migration ranges) — start-cli reads this `manifest` export, and it must
// include `version`, which buildManifest() supplies from the VersionGraph.
export const manifest = buildManifest(versions, sdkManifest)
export { main } from './main'
export { actions } from './actions'
// Back up the whole volume (config, ssh key, optional HF token, inbox, reports).
export const { createBackup, restoreInit } = sdk.setupBackups(async () =>
sdk.Backups.ofVolumes('main'),
)
// init composes: version migrations, action registration, interface export,
// and backup restore.
export const init = sdk.setupInit(versions, actions, setInterfaces, restoreInit)
export const uninit = sdk.setupUninit(versions)
+26
View File
@@ -0,0 +1,26 @@
import { sdk } from './sdk'
export const WEB_UI_PORT = 8080
/**
* Expose the orchestrator web UI as a StartOS interface (Tor + LAN), so the
* user can open the Boardroom Map control panel from the StartOS dashboard.
*/
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const multi = sdk.MultiHost.of(effects, 'web')
const origin = await multi.bindPort(WEB_UI_PORT, { protocol: 'http' })
const ui = sdk.createInterface(effects, {
name: 'Web UI',
id: 'webui',
description:
'The Boardroom Map control panel: drop documents in, convene the reviewer ' +
'panel, watch the job run on your Sparks, and read the reports.',
type: 'ui',
username: null,
path: '',
query: {},
schemeOverride: null,
masked: false,
})
return [await origin.export([ui])]
})
+49
View File
@@ -0,0 +1,49 @@
import { sdk } from './sdk'
import { WEB_UI_PORT } from './interfaces'
export const main = sdk.setupMain(async ({ effects }) => {
// Mount the persistent volume at /data: config.json, ssh key, optional HF
// token, the dropped-document inbox, job run state, and saved reports all live
// here.
const mounts = sdk.Mounts.of().mountVolume({
volumeId: 'main',
mountpoint: '/data',
subpath: null,
readonly: false,
})
const sub = await sdk.SubContainer.of(
effects,
{ imageId: 'main' },
mounts,
'boardroom-webui',
)
// The web UI runs the FastAPI app AND, in a background thread, the Boardroom Map job
// runner (which extracts dropped documents, serves the chosen models on the
// Sparks in waves, runs the reviewer panel, and synthesizes a report).
return sdk.Daemons.of(effects).addDaemon('webui', {
subcontainer: sub,
exec: {
command: [
'uvicorn',
'app:app',
'--host',
'0.0.0.0',
'--port',
String(WEB_UI_PORT),
],
cwd: '/app',
env: { BM_DATA_DIR: '/data' },
},
ready: {
display: 'Web Interface',
fn: () =>
sdk.healthCheck.checkPortListening(effects, WEB_UI_PORT, {
successMessage: 'The control panel is ready',
errorMessage: 'The control panel is not yet listening',
}),
},
requires: [],
})
})
+75
View File
@@ -0,0 +1,75 @@
import { setupManifest } from '@start9labs/start-sdk'
/**
* Boardroom Map manifest.
*
* Like the LLaMA-Factory and Nightshift services, this is a CONTROL PLANE — it
* does not run any GPU workload itself. It is a small web UI + job runner that
* SSHes into one or two NVIDIA DGX Sparks to:
* 1. serve a panel of local LLMs with vLLM (loaded in waves to fit GPU memory),
* 2. extract text from documents you drop in (PDF/DOCX/TXT/MD — done on the
* StartOS box), ship it to the Sparks, and launch a panel of sandboxed
* "reviewer" containers (each a model + a persona) that read the documents
* and write a report,
* 3. optionally run a local "lead reviewer" that synthesizes the panel's
* reports into one consolidated report.
*
* There is NO frontier model and NO cloud API key. In the default `airgapped`
* network mode the reviewer containers can reach ONLY the on-Spark model proxy —
* the documents and their reviews never touch the internet.
*
* NOTE: s9pk.mk extracts the package identifier from the single-quoted value on
* the line below, so keep that field on one line and avoid stray quotes above it.
*/
export const manifest = setupManifest({
id: 'boardroom-map',
title: 'Boardroom Map',
license: 'Apache-2.0',
packageRepo: 'https://github.com/ten31/boardroom-map',
upstreamRepo: 'https://github.com/ten31/boardroom-map',
marketingUrl: 'https://github.com/ten31/boardroom-map',
donationUrl: null,
description: {
short: 'A private panel of local LLMs that reviews your confidential documents on your DGX Sparks',
long:
'Boardroom Map lets you drop confidential documents in and convene a panel of ' +
'local LLMs running on your NVIDIA DGX Sparks to review them. You choose ' +
'which models and which personas (lenses) sit on the panel and how many ' +
'reviews to run. Each reviewer reads the documents and writes a report; an ' +
'optional local lead reviewer synthesizes them into one consolidated ' +
'report. There is no frontier model and no cloud key: in the default ' +
'air-gapped mode the reviewers reach only the on-Spark model endpoint, so ' +
'your documents and their reviews never leave your hardware. No GPU is ' +
'needed on the StartOS host.',
},
// Arch-agnostic orchestrator. Docker build paths are relative to the PROJECT
// ROOT (where the Makefile runs), matching the Start9 convention.
images: {
main: {
source: {
dockerBuild: {
dockerfile: './orchestrator.Dockerfile',
workdir: '.',
},
},
arch: ['x86_64', 'aarch64'],
// The orchestrator only SSHes out + extracts document text on CPU; it never
// touches a local GPU.
nvidiaContainer: false,
},
},
volumes: ['main'],
dependencies: {},
hardwareRequirements: {
ram: 2048,
},
alerts: {
install:
'Boardroom Map drives work on REMOTE machines (your DGX Sparks) over SSH; ' +
'nothing serves or runs on your StartOS server. After install: ' +
'(1) "Configure Sparks" for SSH access, (2) "Configure Models" for the ' +
'local models to serve, (3) "Configure Reviewers" for the panel + personas, ' +
'(4) "Configure Review" for the rubric and air-gap mode. Then drop ' +
'documents in the inbox and run "Run Review".',
},
})
+8
View File
@@ -0,0 +1,8 @@
import { StartSdk } from '@start9labs/start-sdk'
import { manifest } from './manifest'
/**
* The bound SDK facade. Import `sdk` everywhere else to reach actions, daemons,
* interfaces, health checks, file helpers, and the input-form builders.
*/
export const sdk = StartSdk.of().withManifest(manifest).build(true)
+8
View File
@@ -0,0 +1,8 @@
import { VersionGraph } from '@start9labs/start-sdk'
import { v_0_1_0 } from './v_0_1_0'
/** The current version MUST be the first argument (`current`). */
export const versions = VersionGraph.of({
current: v_0_1_0,
other: [],
})
+16
View File
@@ -0,0 +1,16 @@
import { VersionInfo } from '@start9labs/start-sdk'
/**
* Initial release. ExVer form `<upstream>:<downstream>` — we track our own
* packaging revision since Boardroom Map has no separate upstream semver.
*/
export const v_0_1_0 = VersionInfo.of({
version: '0.1.0:0',
releaseNotes:
'Initial release: drop confidential documents in and convene a panel of ' +
'local LLMs on your DGX Sparks to review them. Choose the models, the ' +
'personas, and how many reviews; an optional local lead reviewer ' +
'synthesizes a consolidated report. Default air-gapped mode keeps documents ' +
'and reviews entirely on your hardware.',
migrations: {},
})