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
+27 -2
View File
@@ -79,6 +79,19 @@ _RENDER_PREAMBLE = (
"CSS/JS, no network.\n"
)
# The execution track's hint, on the same terms as the render one: offered only
# when the tool behind it is, for the same contamination reason. The distinction
# it has to carry is which track a request belongs to — a model that reaches for
# run_snippet to "preview" an HTML page gets a compile error, and one that
# reaches for render_preview to run a C program gets a plain code block.
_RUN_PREAMBLE = (
"\n\n---\nCode runner: when the answer depends on what code actually does, call "
f"the `run_snippet` tool with a complete {_tools.code_run.lang_prose()} program, "
"then paste the returned `fence` into your reply. It really runs, in a throwaway "
"directory with no network and a few seconds of CPU. Describe only the output it "
"returned.\n"
)
_CODING_KEYWORDS = frozenset({
"code", "coding", "function", "class", "method", "variable", "bug", "error",
@@ -416,8 +429,16 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
# is implemented using a tool called render_preview... renders it live in
# a sandbox" — this text, recited as fact. A hint for a tool that isn't
# being offered is pure contamination.
# Read once here rather than at the tools block below: the run-track
# hint and the run-track schema have to agree about whether the tool is
# on offer, and a second lookup is a second thing to keep in step.
_policy = app_settings.get("action_tool_policy", "off")
allow_actions = _policy != "off"
if _tools.wants_render_preview(message):
system_prompt = (system_prompt + _RENDER_PREAMBLE) if system_prompt else _RENDER_PREAMBLE.lstrip()
if allow_actions and _tools.wants_code_run(message):
system_prompt = (system_prompt + _RUN_PREAMBLE) if system_prompt else _RUN_PREAMBLE.lstrip()
# ── MindTrace pre-flight ──────────────────────────────────────────
_trace_intent = _detect_intent(message) if message else "chat"
@@ -479,13 +500,17 @@ async def chat_stream_endpoint(payload: Dict[str, Any]):
# Tools: playbook allowlist, plus render_preview only when this turn
# looks like a visual ask (always advertising it forced a non-stream
# tool round on every chat and felt like "stuck thinking").
_policy = app_settings.get("action_tool_policy", "off")
allow_actions = _policy != "off"
_pb_tools = list(getattr(_main_pb, "tools", None) or []) if _main_pb else []
schemas_by_name: dict = {}
if _tools.wants_render_preview(message) or "render_preview" in _pb_tools:
for s in _tools.standing_schemas():
schemas_by_name[s["function"]["name"]] = s
# run_snippet rides the same cue mechanism but stays behind the action
# gate: it executes code on this machine. With the policy "off", "run
# this" gets an explanation and a code block, never a subprocess.
if allow_actions and _tools.wants_code_run(message):
for s in _tools.run_schemas():
schemas_by_name[s["function"]["name"]] = s
for s in _tools.schemas_for(_pb_tools, allow_actions):
schemas_by_name[s["function"]["name"]] = s
schemas = list(schemas_by_name.values())