@nexusbloom/core
v1.0.0
Published
The NexusBloom tool engine — type compatibility, pipelines, validation and sandboxed execution. Shared by the CLI, MCP server, VS Code extension and embed.
Readme
@nexusbloom/core
The NexusBloom tool engine. Type compatibility, pipeline composition, validation, and sandboxed execution — the parts of the platform that must behave identically no matter which surface is calling.
npm install @nexusbloom/coreWhy this package exists
A tool's manifest is the contract. What accepts a value, what a conversion costs, whether two tools can be wired together — those rules are not presentation, and they cannot differ per surface.
Before this package, those rules lived inside the CLI. Any other surface — MCP, VS Code, embed — would have had to reimplement them, and the surfaces would slowly disagree about whether a string fits an integer field. That is the failure schema-first is supposed to prevent.
So the engine lives here, and the CLI is one consumer of it.
The boundary
In core: pure logic and the sandbox. No terminal, no prompts, no config.
| Module | What it is |
|---|---|
| types.js | Type inference, validation, compatibility rules |
| values.js | $path accessors and transforms |
| pipe.js | Parse, plan and run a pipeline |
| pipelines.js | Saved pipeline files |
| runs.js | Recorded runs (--trace) |
| compile.js | Manifest source → callable coreLogic |
| sandbox.js, runner.js | Disposable child-process execution |
| jsonish.js | Lenient JSON parsing |
| paths.js | XDG config and cache locations |
Not in core: anything that draws or prompts. renderPlan, describeOutput and the whole ui.js belong to the CLI, which is why pipe.js splits at the chalk boundary.
Usage
import { validateInput, compatibility, runPipeline } from "@nexusbloom/core";Submodules are addressable when you only need one:
import { validateInput } from "@nexusbloom/core/types";
import { runPipeline } from "@nexusbloom/core/pipe";
import { execInSandbox } from "@nexusbloom/core/sandbox";Validating input
const { valid, errors, data } = validateInput(input, manifest.input_schema);
if (!valid) throw new Error(errors.join("; "));Deciding whether two tools connect
const { level, note, suggestion } = compatibility("integer", ["number"]);
// → { level: "ok", note: "lossless widening" }
compatibility("string", ["integer"]);
// → { level: "lossy", ... } — needs an explicit transformThis is the rule the whole pipeline design rests on: conversions that cannot lose information happen silently, conversions that can require a transform. $max → count just works; $text → count does not.
Running a pipeline
const steps = parsePipeline(["env-validator env_content=@in", "| base64-tool input=$result"]);
const plan = await buildPlan(steps, getManifest); // resolves wiring, reports problems
const out = await runPipeline({ steps, getTool, execute, stdin });buildPlan reports type errors before anything runs. runPipeline re-checks against real values, because declared schemas can be wrong.
Executing untrusted source
const { ok, result, error } = await execInSandbox({
source: toolSource,
input,
timeoutMs: 5000,
});A child process, killed with SIGKILL at the deadline. Promise.race cannot stop an infinite loop — the event loop never gets a turn — so a forkable process is the only thing that can.
This is isolation, not a security sandbox: the child runs with the same user and filesystem permissions as the host. Treat downloaded tool source as trusted-but-unverified, and prefer server-side execution for tools you do not control.
Consistency
Every surface imports this package rather than its own copy, so a change to a type rule or a pipeline lands once and every surface inherits it. If you find yourself copying logic out of here to use it, the logic probably belongs in here.
License
MIT
