Nous.CodeRuntime.JS (nous v0.17.1)

Copy Markdown View Source

Code Mode's JavaScript runtime: an embedded Deno isolate, one per run.

Requires tyrex ~> 0.4, an optional dependency. Add it to your own deps to unlock this provider:

{:tyrex, "~> 0.4"}

config :nous, :code_runtime, {Nous.CodeRuntime.JS, timeout_ms: 30_000}

Isolation, stated exactly

A program runs in a V8 isolate inside the BEAM, with Deno's permission model set to :none by default. It is not an OS sandbox and does not claim to be: there is no separate process, so a V8 escape is an escape into the BEAM. What it does enforce, and what was verified on this substrate rather than assumed:

  • No filesystem, network, env or subprocess. Deno denies each with NotCapable under permissions: :none.
  • No route into Elixir except the tools you granted. tyrex's arbitrary-module bridge is opt-in, and this provider narrows it to a single function - {Nous.CodeRuntime.JS.Bridge, :call, 1} - then removes the gateway from the isolate before any model-authored code runs. Afterwards a program cannot reach Deno.core, ext:core/ops, __bootstrap, or reconstruct the gateway through new Function.
  • A real kill. A deadline that leaves the program running is not a deadline. while (true) {} is terminated, and the worker's OS thread is reclaimed - not merely abandoned, which is the failure mode that makes a "timeout" quietly leak a core per run.

Budgets

Provider configuration, never per request, so a program cannot ask for more rope than the operator gave it:

  • :timeout_ms (default 30_000) - wall clock for the whole run, enforced by a BEAM timer that kills the isolate. This is the authoritative deadline because the substrate's own timeout does not cover time spent inside tool calls — an allowlisted bridge call runs inline on the runtime's own message loop, so a deadline the substrate owns cannot see a slow tool.
  • :max_heap_mb (default 256) - V8 heap cap. Exceeding it ends the run with an :abort failure; the BEAM is unaffected.
  • :max_output_bytes (default 1_000_000) - byte-accurate ledger over everything the program logs. On overflow the fitting prefix is kept and a truncation notice is appended.
  • :permissions (default :none) - passed through to tyrex. Widening this hands model-authored code the capability you name.
  • :poll_backoff_ms (default [1, 2, 5, 10, 25]) - how the guest waits for a sub-call to finish. The last value is the worst-case delay between a tool completing and the program seeing it.

There is deliberately no instruction budget. This substrate has no fuel metering, and rather than imply otherwise: the wall-clock deadline is the only bound on a compute-bound program, and it is a real one.

Cost

A fresh isolate per run costs ~172ms (measured, median 174ms over 12 runs). Nothing is pooled, so no state survives a run - not a patched prototype, not a cached module, not a globalThis the last program wrote to. Reuse would buy back that 172ms at the price of the isolation this provider exists to give.

Summary

Functions

Whether the optional tyrex dependency is available.

The defaults every budget falls back to.

Validate provider configuration, filling in defaults.

Functions

available?()

@spec available?() :: boolean()

Whether the optional tyrex dependency is available.

defaults()

@spec defaults() :: keyword()

The defaults every budget falls back to.

validate(config)

@spec validate(keyword() | nil) ::
  {:ok, keyword()} | {:error, {:contract, String.t()}}

Validate provider configuration, filling in defaults.

Budgets are checked here - when the provider is configured - rather than when a run starts, because a typo in a budget should fail before a model is ever handed a tool that depends on it.