feat(chat): add run_snippet, an execution track beside the render track

render_preview validates markup and hands it to the browser, which renders it
in an opaque-origin sandboxed iframe. Nothing executes server-side. That model
fits HTML/SVG/JSX and cannot fit C, Rust or Erlang, which need a real
toolchain - so those get a second tool instead of a widened first one.

The split is the feature: the model picks a track by picking a tool, rather
than picking a `lang` value from an enum where half the entries run
server-side and half do not.

synapse/code_run.py compiles and runs one file in a throwaway directory and
returns a ```nexus-run fence carrying the source and its captured output
together, so a model cannot paste output without the code that produced it.
Backticks in the source are re-encoded as ` - still valid JSON, and it
cannot close the fence early.

It is not a sandbox, and the module docstring says so up front. What it gives
is containment by layers: consent (an action tool, gated by
action_tool_policy, per-call Approve/Deny on "ask"), static screening, a
scrubbed environment in a temp dir, wall-clock and POSIX rlimits, and a
network namespace on Linux where unprivileged userns are available. Screening
is a tripwire against a model reaching for `requests` out of habit, not a
boundary against an adversary; layers 1 and 3-5 are the load-bearing ones.

Backend RUN_LANGS and frontend run-langs.js are separate registries because
the two sides need different things - one executes, one labels - and neither
should depend on the other at runtime. tests/test_tools.py asserts the key
sets and the fence tag stay equal, so drift fails the gate instead of
rendering a run result under the wrong language.

tests/snippet_probes/ is a data catalog rather than inlined cases, so adding a
language is a data change and the meta-tests can assert every RUN_LANGS key
has both a smoke probe and a screening probe. Probes skip cleanly on hosts
without the toolchain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Athena Kaminsky
2026-08-20 14:30:03 -05:00
co-authored by Claude Opus 5
parent 425184a30b
commit affba1805c
14 changed files with 2016 additions and 14 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"test": "node --test src/preview/jsx-transform.test.js",
"test": "node --test \"src/preview/*.test.js\"",
"preview": "vite preview"
},
"dependencies": {
+128 -2
View File
@@ -6,6 +6,9 @@ import { useEffect, useRef, useState } from "react";
// auto-executing bare script isn't this feature's job (see RenderBlock's doc
// comment for the sandboxing model).
import { PREVIEW_LANGS, RENDERABLE_LANGS } from "./preview/languages.js";
// The execution track's display half: a ```nexus-run fence is a run result
// (source + captured output), not a program to execute here. See run-langs.js.
import { RUN_FENCE_LANG, parseRunResult } from "./preview/run-langs.js";
// Parse content into an array of {type, value, lang, streaming} blocks.
// Handles:
@@ -29,9 +32,11 @@ function parseBlocks(content) {
let j = fenceStart + 3;
let lang = "";
// Language specifier is valid only when word-chars are followed by a newline.
// Language specifier is valid only when tag-chars are followed by a newline.
// If there's no newline (e.g. ```pythonprint(...)) treat everything as code.
const langMatch = content.slice(j).match(/^(\w+)(\r?\n)/);
// Hyphens count: `nexus-run` is a tag this file dispatches on, and real
// languages spell themselves that way too (objective-c, c-sharp).
const langMatch = content.slice(j).match(/^([\w-]+)(\r?\n)/);
if (langMatch) {
lang = langMatch[1];
j += langMatch[0].length;
@@ -61,6 +66,12 @@ export function Markdown({ content }) {
{blocks.map((block, i) => {
if (block.type !== "code") return <TextBlock key={i} text={block.value} />;
const lang = (block.lang || "").toLowerCase();
if (lang === RUN_FENCE_LANG) {
// A half-streamed envelope is not parseable JSON, so the block shows
// as code until the fence closes and then becomes the run panel.
const run = block.streaming ? null : parseRunResult(block.value);
if (run) return <RunBlock key={i} run={run} />;
}
return RENDERABLE_LANGS.has(lang)
? <RenderBlock key={i} lang={lang} value={block.value} streaming={block.streaming} />
: <CodeBlock key={i} lang={block.lang} value={block.value} streaming={block.streaming} />;
@@ -126,6 +137,121 @@ function CodeBlock({ lang, value, streaming }) {
);
}
// A finished run: the source that was executed and what it printed, in one
// block with an Output/Code toggle.
//
// Nothing executes in the browser here, which is the whole difference from
// RenderBlock below. The program already ran on the host (synapse/code_run.py)
// under the user's per-call approval; by the time this renders, the result is
// history. So there is no iframe, no CSP and no sandbox in this component - the
// only untrusted thing present is *text*, and React escapes it.
//
// Output and stderr are shown together rather than on separate tabs: a program
// that printed three lines and then panicked is telling one story, and splitting
// it hides which half the reader needs. Exit code sits in the header because a
// silent non-zero exit is otherwise invisible.
function RunBlock({ run }) {
const [tab, setTab] = useState("output");
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(run.source.trimEnd()).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};
const failed = run.exitCode !== null && run.exitCode !== 0;
const empty = !run.stdout.trim() && !run.stderr.trim();
return (
<div style={{
background: "#0d0d0d",
border: "1px solid #2a2a2a",
borderRadius: "6px",
margin: "0.5rem 0",
overflow: "hidden",
}}>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0.3rem 0.75rem",
background: "#161616",
borderBottom: "1px solid #2a2a2a",
}}>
<div style={{ display: "flex", alignItems: "center", gap: "0.25rem" }}>
<TabButton active={tab === "output"} onClick={() => setTab("output")}>
Output
</TabButton>
<TabButton active={tab === "code"} onClick={() => setTab("code")}>
Code
</TabButton>
<span style={{ fontSize: "0.7rem", color: "#555", fontFamily: "monospace", marginLeft: "0.25rem" }}>
{run.label}
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
{run.exitCode !== null && (
<span style={{
fontSize: "0.7rem",
fontFamily: "monospace",
color: failed ? "#ff8a80" : "#4caf50",
}}>
exit {run.exitCode}
</span>
)}
<button onClick={copy} style={_chromeButtonStyle(copied ? "#4caf50" : "#555")}>
{copied ? "Copied!" : "Copy"}
</button>
</div>
</div>
{tab === "output" ? (
<div style={{
padding: "0.75rem 1rem",
fontSize: "0.85rem",
lineHeight: "1.5",
fontFamily: "monospace",
maxHeight: "24rem",
overflow: "auto",
}}>
{empty && (
<span style={{ color: "#555" }}>
(the program printed nothing)
</span>
)}
{run.stdout && (
<pre style={{ margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", color: "#ddd" }}>
{run.stdout.replace(/\n$/, "")}
</pre>
)}
{run.stderr && (
<pre style={{
margin: run.stdout ? "0.5rem 0 0" : 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
color: "#ff8a80",
}}>
{run.stderr.replace(/\n$/, "")}
</pre>
)}
</div>
) : (
<pre style={{
padding: "0.75rem 1rem",
overflowX: "auto",
fontSize: "0.85rem",
lineHeight: "1.5",
margin: 0,
fontFamily: "monospace",
}}>
<code>{run.source.trimEnd()}</code>
</pre>
)}
</div>
);
}
// Content-Security-Policy for the rendered preview. Together with the iframe's
// `sandbox` attribute below, this is the entire trust boundary for model-
// authored HTML/SVG, so it stays conservative rather than convenient:
+60
View File
@@ -0,0 +1,60 @@
/*
* run-langs.js — what the chat can display a *run result* for, one entry per
* language.
*
* This is the display half of the execution track, and the counterpart to
* languages.js. The distinction between the two is where the code runs:
*
* languages.js the fence IS the program; the browser runs it in a sandboxed
* iframe, and this side has to build a document for it.
* run-langs.js the program already ran, on the host, in synapse/code_run.py.
* Nothing executes here — the fence carries source and captured
* output as JSON, and this side only labels and lays it out.
*
* So an entry needs far less than a preview entry does: no toBody, no line
* offsets, no runtime to inline. Just how to name the language to the reader.
*
* The backend keeps a matching registry (RUN_LANGS in synapse/code_run.py) that
* says how each language is *executed*. Neither depends on the other at runtime;
* tests/test_tools.py asserts the key sets stay equal.
*/
export const RUN_LANGS = {
python: { label: "Python" },
c: { label: "C" },
cpp: { label: "C++" },
rust: { label: "Rust" },
erlang: { label: "Erlang" },
};
// The fence tag run_snippet emits. Not a real language: the block's body is the
// JSON envelope { lang, source, stdout, stderr, exit_code }, which keeps a run's
// output attached to the source that produced it. A model pasting the fence
// cannot paste output without the code, or code with output it never produced.
export const RUN_FENCE_LANG = "nexus-run";
/**
* Parse a nexus-run fence body. Returns null for anything that isn't a
* well-formed envelope naming a known language — the caller then falls back to
* showing the block as plain code, which is the honest thing to do with a
* result we can't vouch for.
*/
export function parseRunResult(text) {
let data;
try {
data = JSON.parse(text);
} catch {
return null;
}
if (!data || typeof data !== "object") return null;
if (!Object.prototype.hasOwnProperty.call(RUN_LANGS, data.lang)) return null;
if (typeof data.source !== "string") return null;
return {
lang: data.lang,
label: RUN_LANGS[data.lang].label,
source: data.source,
stdout: typeof data.stdout === "string" ? data.stdout : "",
stderr: typeof data.stderr === "string" ? data.stderr : "",
exitCode: Number.isInteger(data.exit_code) ? data.exit_code : null,
};
}
@@ -0,0 +1,83 @@
/*
* The run-result envelope: what parseRunResult will and won't accept.
*
* Everything this parses arrived as text a language model chose to paste into
* its reply, so the interesting cases are all the malformed ones. A bad envelope
* has to return null - the caller then shows the raw block as code, which is
* ugly but honest - rather than yield a half-built object that renders as a run
* that never happened.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { RUN_LANGS, RUN_FENCE_LANG, parseRunResult } from "./run-langs.js";
const envelope = (over = {}) => JSON.stringify({
lang: "python",
source: "print(1)",
stdout: "1\n",
stderr: "",
exit_code: 0,
...over,
});
test("the fence tag is the one the backend emits", () => {
assert.equal(RUN_FENCE_LANG, "nexus-run");
});
test("every language has a display label", () => {
for (const [name, spec] of Object.entries(RUN_LANGS)) {
assert.equal(typeof spec.label, "string", name);
assert.ok(spec.label.length, name);
}
});
test("a well-formed envelope parses into display fields", () => {
const run = parseRunResult(envelope());
assert.equal(run.lang, "python");
assert.equal(run.label, "Python");
assert.equal(run.source, "print(1)");
assert.equal(run.stdout, "1\n");
assert.equal(run.exitCode, 0);
});
test("a backtick-escaped source round-trips through JSON", () => {
// run_snippet re-encodes ` as ` so the source cannot close the fence.
const run = parseRunResult('{"lang":"python","source":"x = \\u0060a\\u0060","exit_code":0}');
assert.equal(run.source, "x = `a`");
});
test("a non-zero exit code is preserved, not coerced away", () => {
// `exitCode || null` would turn 0 into null and hide a clean exit; a plain
// falsy check on the other side would call a failing program successful.
assert.equal(parseRunResult(envelope({ exit_code: 2 })).exitCode, 2);
assert.equal(parseRunResult(envelope({ exit_code: 0 })).exitCode, 0);
});
test("a missing exit code becomes null rather than a guess", () => {
assert.equal(parseRunResult(envelope({ exit_code: undefined })).exitCode, null);
assert.equal(parseRunResult(envelope({ exit_code: "0" })).exitCode, null);
});
test("absent streams read as empty, never undefined", () => {
const run = parseRunResult('{"lang":"c","source":"int main(){}","exit_code":0}');
assert.equal(run.stdout, "");
assert.equal(run.stderr, "");
});
test("malformed or foreign envelopes are rejected", () => {
assert.equal(parseRunResult("not json at all"), null);
assert.equal(parseRunResult("null"), null);
assert.equal(parseRunResult("[1,2,3]"), null);
assert.equal(parseRunResult('"a string"'), null);
assert.equal(parseRunResult(envelope({ lang: "haskell" })), null);
assert.equal(parseRunResult(envelope({ source: undefined })), null);
assert.equal(parseRunResult(envelope({ source: 42 })), null);
});
test("a prototype key is not mistaken for a supported language", () => {
// `data.lang in RUN_LANGS` would be true for "toString" and read the label
// off Object.prototype - a run panel titled with a function body.
assert.equal(parseRunResult(envelope({ lang: "toString" })), null);
assert.equal(parseRunResult(envelope({ lang: "constructor" })), null);
});