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
+18
-5
@@ -139,6 +139,10 @@ async def _normalize_to_async_generator(maybe_iterable) -> AsyncGenerator[str, N
|
||||
pending_approvals: Dict[str, Dict[str, Any]] = {}
|
||||
_APPROVAL_TIMEOUT = 300 # seconds; a timeout is treated as "deny all"
|
||||
|
||||
# Tools whose success is a fenced block the model must paste unchanged, and whose
|
||||
# rejections are worth retrying (with `_attempt`) rather than abandoning.
|
||||
_FENCE_TOOLS = frozenset({"render_preview", "run_snippet"})
|
||||
|
||||
|
||||
def _as_tool_calls(obj) -> list:
|
||||
"""Normalize a parsed JSON value into Ollama-style tool_calls entries."""
|
||||
@@ -385,13 +389,16 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
|
||||
# complete, styled demo handed to a struggling model gets pasted
|
||||
# rather than adapted, and then persists into the conversation as a
|
||||
# template for later requests.
|
||||
if name == "render_preview" and isinstance(call_args, dict):
|
||||
if name in _FENCE_TOOLS and isinstance(call_args, dict):
|
||||
call_args = {**call_args, "_attempt": render_rejects}
|
||||
result = await _tools.dispatch(name, call_args)
|
||||
messages.append({"role": "tool", "content": result})
|
||||
# Cap render_preview reject loops — each retry is another full
|
||||
# non-stream generation and looks like the UI is "stuck thinking".
|
||||
if name == "render_preview":
|
||||
# Cap reject loops — each retry is another full non-stream
|
||||
# generation and looks like the UI is "stuck thinking". The counter
|
||||
# is shared across both fence tools on purpose: two failed attempts
|
||||
# in a turn is two too many whether they were previews, runs, or one
|
||||
# of each.
|
||||
if name in _FENCE_TOOLS:
|
||||
try:
|
||||
body = _json.loads(result)
|
||||
except Exception:
|
||||
@@ -408,7 +415,13 @@ async def _run_tool_loop(manager, messages, model, tool_schemas, temperature, nu
|
||||
|
||||
|
||||
def _last_ok_render_fence(messages: list) -> tuple[str | None, dict]:
|
||||
"""Return (fence, tool_payload) from the latest successful render_preview."""
|
||||
"""Return (fence, tool_payload) from the latest successful fence tool.
|
||||
|
||||
Covers render_preview and run_snippet alike — both return {ok, fence, title}
|
||||
and both are pasted verbatim rather than reconstructed. Small models rewrite
|
||||
a fence they were told to copy, which for a preview means a demo that no
|
||||
longer runs and for a run means output the program never actually produced.
|
||||
"""
|
||||
for m in reversed(messages or []):
|
||||
if m.get("role") != "tool":
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user