# code-runner

> Run untrusted Node.js and Python in-process — V8 isolates and CPython-on-WebAssembly behind one run/register_function/teardown API, with no microVM and no /dev/kvm. Code gets a global `iii` and a private scratch directory.

| field | value |
|-------|-------|
| version | 0.1.14 |
| type | binary |
| license | Apache-2.0 |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl |
| author | iii |

## installation

```sh
iii trigger compose::add worker=code-runner@0.1.14
```

## dependencies

- `configuration` @ `latest`

## readme

# code-runner

Run untrusted Node.js and Python **in-process**: iterate with
`code-runner::run`, publish working functions to the bus with
`code-runner::register_function`, clean up with `code-runner::teardown`.

Same wire contract as
[sandbox-code-runner](../sandbox-code-runner/), so a caller written against
that worker needs no changes — but no microVM, no daemon, and **no
`/dev/kvm`**. JavaScript runs in a V8 isolate; Python runs as CPython compiled
to WebAssembly inside wasmtime.

```bash
iii trigger compose::add worker=code-runner
```

## Quickstart

```bash
iii trigger code-runner::run lang=node code='console.log("hi"); return 2 + 2'
```

```json
{ "stdout": "hi\n", "stderr": "", "exit_code": 0, "success": true,
  "duration_ms": 12, "result": 4 }
```

`run` is **one-shot by default**: it creates a runtime, runs your code,
returns, and destroys it. Nothing persists, and the response carries no
`runtime_id` because there is nothing left to address.

Pass `keep: true` to leave it running. The response's `runtime_id` then
addresses it on later calls — same globals, same scratch directory — and is
the capability `teardown` needs.

**A failing script is a response, not an error.** `stdout`, `stderr` and
`exit_code` come back verbatim; a thrown exception is `exit_code: 1` with the
message on `stderr`, not a failed bus call. Errors are reserved for
infrastructure: timeouts, resource kills, unknown runtimes.

`result` is this worker's one addition to sandbox-code-runner's response: the
value your code returned, so a caller does not have to print JSON and parse
`stdout`.

## Output caps

`result`, `stdout` and `stderr` are each capped before the response leaves
this worker, so one runaway script cannot flood your context window with its
own output. Both caps are on by default and apply the same way to node and
python, and to a thrown/raised response exactly as to a successful one —
`exit_code`, `success`, `duration_ms` and `runtime_id` are never touched: a
capped response is still `success: true` when the run itself succeeded, only
the echo is bounded.

**`result`** — capped at `max_result_bytes` (default `32768` bytes, **`0`
disables it**). An oversized value is not truncated or reshaped, it is
*replaced whole* by a string marker naming what was dropped:

```text
<omitted: result was ~{KB} KB ({N array elements | N object keys | a N-char
string}); returning it whole would flood the model context — print a slice,
aggregate in code, or write it to iii.files and return a summary>
```

For example, `for (i=0;i<100000;i++) result.push(i); return result` returns
`result` as `<omitted: result was ~575 KB (100000 array elements); ...>`
instead of the 100,000-element array. This is deliberate: `result` is often
consumed by code (an `fp::pipe` step, another `iii.trigger` call), so a step
that receives a string where it expected an array fails loudly with the
marker text in the error — the teaching failure you want, not data quietly
gone missing.

**`stdout` / `stderr`** — each capped independently at `max_stream_bytes`
(default `16384` bytes, **`0` disables it**). An oversized stream keeps its
first 60% and its last 40% — so both the first failure and the final summary
line survive — with a marker spliced into the middle:

```text
[head: ~60% of the budget]
[…stdout truncated: was ~{KB} KB; middle omitted]
[tail: ~40% of the budget]
```

(`stderr`'s marker names itself the same way.)

**If you hit a cap:** the full value is gone at this layer, not stored
anywhere for a later look — re-running is the only way to get it back. Print
a slice instead of the whole value, aggregate in code (counts, sums,
first/last N) instead of returning or logging everything, or write the full
data to `iii.files` and return a short summary.

## The `iii` global

Code you run gets a global `iii`:

- `await iii.trigger({ function_id, payload })` — invoke any bus function.
- `iii.registerFunction(id, handler, opts?)` — publish one for the life of
  this runtime. For one that outlives it, use `code-runner::register_function`.
  `opts` takes `description`, and `request_format` / `response_format` — JSON
  Schema objects `engine::functions::info` shows callers in place of "any".
- `iii.files` — a **private scratch directory** that lives exactly as long as
  the runtime: `write(name, contents)`, `read(name)`, `readText(name)`,
  `list()`, `remove(name)`. Names are one file each — no paths, no
  subdirectories — and the directory is bounded by `scratch_mb`/`scratch_files`.
- `iii.namespace` — the prefix this runtime may register under.

Python's `iii` is narrower: `iii.trigger({...})` and `iii.namespace`, and it
is **synchronous** — the call blocks until the answer comes back. Registration
from inside guest Python is not available; use `code-runner::register_function`
(node) or trigger a worker that registers for you. A guest timeout is clamped
to what is left of the run's own budget, so `iii.trigger` inside a 5s run
cannot ask for 30s and take the whole run down with it.

```python
answer = iii.trigger({"function_id": "state::get", "payload": {"key": "k"}})
result = answer["value"]
```

## What this cannot do

The in-process Node and Python bus bridges refuse `state::claim-namespace`,
`harness::state::*`, `provider-openai-codex::state::*`, and
`provider::openai-codex::{login,auth}::*`. The checks apply before dispatch,
including queued/fire-and-forget calls and trigger-registration targets.
Trigger bindings cannot target another trigger registration, which would
otherwise install an unchecked second binding. Ordinary bus calls keep their
existing behavior and authority; these checks are not a general sandbox for
arbitrary code or for other workers' capabilities.

`sandbox-code-runner` has a different boundary: its microVM guests connect
directly to the engine using the real SDK. They do not pass through these host
bridges. A check on that runner's generated `iii` global could restrict normal
helper calls, but guests could bypass it by importing the SDK or opening a raw
engine connection. Such connections remain trusted under the existing engine
model; this change does not claim to restrict them.

Read this before choosing between the two workers.

| | `code-runner` | `sandbox-code-runner` |
|---|---|---|
| Isolation | V8 isolate / wasm sandbox, in-process | microVM per runtime |
| Requires `/dev/kvm` or Apple Silicon | **no** | yes |
| Outbound network | **no** | yes |
| `npm install` / `pip install` | **no** | yes |
| Real filesystem | no — only `iii.files` | yes, a whole OS |
| Guest `iii` | host-implemented surface | the real iii-sdk client |
| Per-call cost | milliseconds | VM boot |

**There is no network, so package installation cannot work at all** — not
slowly, not with a flag. If your code needs a third-party package, use
`sandbox-code-runner`. What you can do here is write your own modules into
`iii.files` and load them from there.

## Configuration

Served by the **`configuration` worker** (entry id `code-runner`) — edit it in
the Console's global Settings modal (the worker ships its own form) or via
`configuration::set`; the fs adapter persists it under
`./data/configuration/code-runner.yaml`. There is no committed config file;
`--config <path>` seeds the initial value on first boot only. Reload tier 1:
**`max_result_bytes`, `max_stream_bytes`, `default_timeout_ms`,
`max_timeout_ms`, and `inject_guidance` hot-apply on save**; the remaining fields are captured when
the engines boot and cannot be rebound under live guest runtimes — a change to
them is saved immediately and applies at the next worker restart (the reload
log says so).

| key | default | meaning |
|---|---|---|
| `max_runtimes` | 32 | live runtimes across both engines |
| `default_timeout_ms` | 5000 | per run when unspecified |
| `max_timeout_ms` | 30000 | ceiling a request's `timeout_ms` is clamped to |
| `idle_ttl_secs` | 900 | reap runtimes idle this long |
| `heap_mb` | 128 | V8 object-heap cap per node runtime |
| `external_mb` | 64 | off-heap cap per node runtime; `heap_mb` does not cover it |
| `scratch_mb` | 8 | `iii.files` quota per **node** runtime; **0 disables it entirely**. Python's `/work` budget is fixed by its engine (256 MiB, 20k files) |
| `scratch_files` | 64 | max files per node runtime |
| `scratch_root` | unset | where node scratch directories live |
| `inject_guidance` | true | append the code-runner usage guidance to agent system prompts (the `pre_generate` hook); off answers the hook with a no-op |
| `max_result_bytes` | 32768 | ceiling on the serialized `result`; over it becomes an omission marker (see [Output caps](#output-caps)). **0 disables it** |
| `max_stream_bytes` | 16384 | ceiling on each of `stdout`/`stderr`; over it keeps head+tail around a truncation marker. **0 disables it** |

**Worst-case host footprint is `max_runtimes * scratch_mb`** — 256 MiB at the
defaults — and the system temp directory is tmpfs, i.e. host RAM, on most
Linux hosts. Set `scratch_root` to real disk if that matters.

## Errors

| code | meaning |
|---|---|
| `code-runner::invalid_request` | malformed request, or a `lang` that contradicts the runtime |
| `code-runner::runtime_not_found` | unknown `runtime_id`, or a namespace with nothing behind it |
| `code-runner::expired` | the runtime was reaped or killed; get a fresh one |
| `code-runner::capacity` | all runtime slots in use — retry later |
| `code-runner::timeout` | the run blew its deadline |
| `code-runner::resource_exhausted` | a cap was hit mid-run (memory, scratch quota). Unlike `capacity`, retrying will not help — shrink the workload |
| `code-runner::handler_error` | a registered handler threw |
| `code-runner::engine` | infrastructure failure |

## Console UI

The worker ships an injected console UI (the injectable-UI protocol,
`iii/tech-specs/2026-07-17-injectable-ui`) — two assets, built from `ui/` by
esbuild and embedded in the binary, so there is nothing to install and nothing
to serve separately:

| Asset | Slot | What it does |
|---|---|---|
| `code-runner/page.js` | `console:script` | how `run` / `register_function` / `teardown` render in chat and the traces span view |
| `code-runner/styles.css` | `console:style` | the stylesheet, every rule scoped under `[data-iii-ui="code-runner"]` |

One renderer per op, under `ui/src/function-trigger-message/`, sharing the card
frame in `ui/src/lib/shared.tsx`. They replace the console's raw-JSON card,
which turns `code` into one escaped line and buries the verdict.

What the cards are for:

- **`run`** leads with the verdict (`exit`, duration), then the **completion
  value**, then the source highlighted as the language it runs as, then
  stdout/stderr as terminal output. A **non-zero exit renders as a warning, not
  an alert** — errors are for infrastructure, and a script that throws is an
  ordinary response.
- A **null `result`** is shown explicitly, with the engine's return convention
  beside it: node code is a function body (`return 2 + 2`), python code is a
  module (assign `result`). That mismatch is the usual reason a call "worked"
  and returned nothing.
- **`register_function`** puts the namespace the id claims on its own line, and
  advises when the source defines no `handler` — on python that fails the
  registration outright, not the first call.
- **`teardown`** answers the one question the payload does not: which function
  ids stopped resolving.

**`runtime_id` is never rendered in full.** It is a capability — whoever holds
one can run into or tear down that runtime — so it appears only as a truncated
chip whose full value is handed over on an explicit click-to-copy. Every other
string on a card is filtered first: stdout, stderr, error messages (which quote
the id by design — see `src/error.rs`), the submitted source, the completion
value, and the `raw json` tab the console mounts regardless of what a card does.

### Developing it

```sh
cd ui && pnpm install && pnpm build   # cargo build does this for you
pnpm test                             # vitest, renders the cards server-side
```

`build.rs` runs `pnpm install && pnpm build` when `ui/dist/` is missing or
stale, so a plain `cargo build` is enough. `SKIP_UI_BUILD=1` uses the existing
`ui/dist/` as-is. For a hot-reload loop, run `pnpm watch` and start the worker
with `III_CODE_RUNNER_UI_WATCH=1` — every open console tab swaps the changed
asset.

## Status

| | node | python |
|---|---|---|
| one-shot `run` | yes | yes |
| `keep` / `runtime_id` | yes | **yes** |
| what a kept runtime persists | globals **and** files | globals **and** files |
| `register_function` | yes | **yes** |
| `iii.trigger` from guest code | yes | **yes** |
| `iii.registerFunction` from guest code | yes | no |
| `iii.files` | yes | use `/work` |

**A kept python runtime persists its working directory *and* its
interpreter.** `python.wasm` is a WASI *command* module — the artifact exports
`memory` and `_start` and nothing else — so the interpreter cannot be re-entered
once `_start` returns. Instead it never returns: the wrapper parks on stdin
between calls and the host hands it one turn at a time. Globals stay bound,
modules stay imported, and files under `/work` stay put.

That is a **superset** of what `sandbox-code-runner` promises for its own
`keep`, which is *"same filesystem, fresh interpreter process each time"*. Code
written against that worker still behaves correctly here; code written against
this one may not port back.

Two consequences worth knowing before you rely on it:

- **A call that overruns its `timeout_ms` takes the interpreter with it.** The
  only kill that reaches a guest parked in a host call unwinds `_start`, so
  there is nothing left to resume. The working directory survives, and the next
  call on that `runtime_id` boots a fresh interpreter on the same files.
- **`memory_mb` is fixed when the runtime is created.** Wasm linear memory only
  ever grows, so a per-call ceiling would be a promise the sandbox cannot keep —
  a later caller would inherit whatever an earlier one grew to. Sending
  `memory_mb` alongside a `runtime_id` is refused rather than ignored.

Only `result` is cleared between turns. It is the output slot, not state: a
turn that assigns no `result` returns `null`, never the previous turn's value.

### `register_function` with `lang: "python"`

Your `source` must define `handler(payload)`; anything else it defines stays
available to every later invocation, because the namespace runs on one pinned
interpreter.

```python
# code-runner::register_function, lang=python, function_id="my-app::greet"
import json

_greetings = {"en": "hello", "pt": "olá"}

def handler(payload):
    return _greetings.get(payload.get("lang", "en"), "hello")
```

Four things worth knowing:

- **Source that leaves no callable `handler` fails the registration**, not the
  first call. The id never reaches the catalog, so a corrected redeploy of the
  same id works.
- **Re-registering an id replaces it.** The previous registration is retired
  before the new one is published.
- **The interpreter is exempt from the idle sweep.** A registration's lifetime
  is the registration's, not its traffic's; `teardown` with `namespace` is the
  only way to remove one.
- **The namespace is shared with node.** Function ids are claimed in one
  registry, so `my-app::greet` cannot exist in both languages, and the same
  charset rule applies to the namespace in both.
- **Schemas are optional but shown.** `request_format` / `response_format`
  (JSON Schema objects, max 16 KiB, must actually constrain something) ride
  the registration in either language, and `engine::functions::info` shows
  them in place of "any".

Unlike node, guest Python cannot register a function *from inside* a run —
`python.wasm` exports `_start` and nothing else, so there is no
`iii.registerFunction` to call. The host publishes on the guest's behalf and
dispatches each invocation as one turn on the namespace interpreter.

```python
# lang=python, keep=true — then reuse the runtime_id
open("/work/state.json", "w").write('{"n": 1}')
result = "saved"
```

## api reference

```json
{
  "functions": [
    {
      "description": "Internal pre_generate hook: appends code-runner usage guidance to the agent system prompt. Bound to harness::hook::pre-generate at worker startup; not called directly. Silenced by inject_guidance: false in the code-runner configuration.",
      "metadata": {
        "internal": true
      },
      "name": "code-runner::inject-guidance",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "GenerateContext": {
            "properties": {
              "system_prompt": {
                "default": "",
                "description": "The system prompt assembled so far (base + any prior hook's mutation).",
                "type": "string"
              }
            },
            "type": "object"
          }
        },
        "description": "The slice of the `pre_generate` hook envelope this hook reads; other fields are ignored.",
        "properties": {
          "generate": {
            "$ref": "#/definitions/GenerateContext"
          }
        },
        "title": "PreGenerateEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "PreGenerateMutations": {
            "description": "The harness applies `system_prompt` only when the key is present, so `None` serializes to an empty object: the safe no-op that preserves the harness's assembled prompt.",
            "properties": {
              "system_prompt": {
                "description": "Full replacement system prompt (base + appended guidance). The harness overwrites, it does not merge.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "type": "object"
          }
        },
        "description": "Hook envelope returned to the harness: the mutations to apply to the generation.",
        "properties": {
          "mutations": {
            "$ref": "#/definitions/PreGenerateMutations"
          }
        },
        "required": [
          "mutations"
        ],
        "title": "PreGenerateResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: reload code-runner settings from the authoritative configuration on change. Output caps and timeouts hot-apply; runtime/memory/scratch limits apply at the next worker restart.",
      "metadata": {
        "internal": true
      },
      "name": "code-runner::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "OnConfigChangeRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    },
    {
      "description": "Publish a bus function whose handler runs in a sandbox; no runtime_id needed. source must define handler(payload) in lang; each call runs it and returns its JSON result. The namespace (function_id before `::`) shares one runtime per lang; its functions stop resolving once torn down or idle-reaped.",
      "metadata": {},
      "name": "code-runner::register_function",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Lang": {
            "description": "Which engine backs a runtime.\n\nOnly `node` and `python` are accepted; anything else fails deserialization before a handler runs, so the schema itself is the validation.",
            "enum": [
              "node",
              "python"
            ],
            "type": "string"
          }
        },
        "properties": {
          "description": {
            "default": null,
            "description": "What `engine::functions::info` shows a caller — write one.",
            "type": [
              "string",
              "null"
            ]
          },
          "function_id": {
            "description": "Bus id, e.g. `my-app::greet`; the segment before the first `::` is the namespace, which holds one persistent runtime per lang.",
            "type": "string"
          },
          "lang": {
            "allOf": [
              {
                "$ref": "#/definitions/Lang"
              }
            ],
            "description": "Language of the namespace runtime this handler runs in."
          },
          "request_format": {
            "default": null,
            "description": "Optional JSON Schema object (≤ 16 KiB, must constrain: `type`/`properties`/`$ref`) for the payload; shown by `engine::functions::info`, not enforced."
          },
          "response_format": {
            "default": null,
            "description": "Optional JSON Schema for the handler's return value; same rules as `request_format`."
          },
          "source": {
            "description": "Source that defines `handler(payload)` in `lang`: `export function handler(payload) {…}` (node) or `def handler(payload): …` (python).",
            "type": "string"
          }
        },
        "required": [
          "function_id",
          "lang",
          "source"
        ],
        "title": "RegisterRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "function_id": {
            "type": "string"
          },
          "registered": {
            "type": "boolean"
          }
        },
        "required": [
          "function_id",
          "registered"
        ],
        "title": "RegisterResponse",
        "type": "object"
      }
    },
    {
      "description": "Run code in a sandbox (no network, host filesystem or packages). One-shot by default: nothing persists and no runtime_id is returned. keep: true keeps the runtime and returns its runtime_id to reuse until code-runner::teardown or the idle TTL reaps it. A failing script is a response, not an error.",
      "metadata": {},
      "name": "code-runner::run",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Lang": {
            "description": "Which engine backs a runtime.\n\nOnly `node` and `python` are accepted; anything else fails deserialization before a handler runs, so the schema itself is the validation.",
            "enum": [
              "node",
              "python"
            ],
            "type": "string"
          }
        },
        "properties": {
          "code": {
            "description": "Node: an async function body (`return x` sets `result`; `await` works). Python: a module (assign `result = x`). A global `iii` is in scope.",
            "type": "string"
          },
          "keep": {
            "default": false,
            "description": "When `runtime_id` is omitted: `true` leaves the runtime running and returns its `runtime_id`; `false` destroys it after the run.",
            "type": "boolean"
          },
          "lang": {
            "anyOf": [
              {
                "$ref": "#/definitions/Lang"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "description": "Required when `runtime_id` is omitted; on an existing runtime omit it or pass the runtime's own language."
          },
          "runtime_id": {
            "default": null,
            "description": "Run in this existing runtime (from a keep: true run), sharing its globals and scratch; it is not destroyed afterwards. Omit to run one-shot.",
            "type": [
              "string",
              "null"
            ]
          },
          "timeout_ms": {
            "default": null,
            "description": "Wall-clock budget in milliseconds, clamped to the configured maximum.",
            "format": "uint64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "code"
        ],
        "title": "RunRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "duration_ms": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "exit_code": {
            "format": "int64",
            "type": "integer"
          },
          "result": {
            "description": "The completion value: what the code returned (node) or assigned to `result` (python). Null when it returned nothing, when it returned something JSON cannot represent, or when the run failed.\n\nAlways present, never skipped — unlike `runtime_id`, a null result is information (\"the code returned nothing\") rather than an absence."
          },
          "runtime_id": {
            "description": "Present when this run addresses a runtime that outlives the call — a `keep: true` run that SUCCEEDED, or a `runtime_id` echoed back. Absent, not null, otherwise: there is nothing to address. Treat a present value as a secret.",
            "type": [
              "string",
              "null"
            ]
          },
          "stderr": {
            "type": "string"
          },
          "stdout": {
            "type": "string"
          },
          "success": {
            "type": "boolean"
          }
        },
        "required": [
          "duration_ms",
          "exit_code",
          "result",
          "stderr",
          "stdout",
          "success"
        ],
        "title": "RunResponse",
        "type": "object"
      }
    },
    {
      "description": "Destroy a runtime: unregister its bus functions, kill it and free its slot. Pass exactly one of runtime_id (from a keep: true run) or namespace (a register_function namespace, e.g. \"app\" for app::greet).",
      "metadata": {},
      "name": "code-runner::teardown",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Exactly one of `runtime_id` or `namespace` must be set.",
        "properties": {
          "namespace": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "runtime_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "TeardownRequest",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "namespace": {
            "description": "Set when addressed by `namespace` — echoed in its canonical `app::` form, since more than one runtime (one per language) can back it and there is no single `runtime_id` to report.",
            "type": [
              "string",
              "null"
            ]
          },
          "runtime_id": {
            "description": "Set when this teardown was addressed by `runtime_id`.",
            "type": [
              "string",
              "null"
            ]
          },
          "torn_down": {
            "type": "boolean"
          },
          "unregistered": {
            "description": "Bus function ids this teardown unregistered, across every runtime it destroyed.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "torn_down",
          "unregistered"
        ],
        "title": "TeardownResponse",
        "type": "object"
      }
    },
    {
      "description": "Serve the code-runner worker's injected console UI assets (content function for its console:script / console:style / console:module triggers).",
      "metadata": {
        "internal": true
      },
      "name": "code-runner::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    }
  ],
  "triggers": []
}
```
