forked from enderofwings/NexusOS
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:
co-authored by
Claude Opus 5
parent
425184a30b
commit
affba1805c
+27
-2
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user