diff --git a/fern/docs/pages/tools/code-execution.mdx b/fern/docs/pages/tools/code-execution.mdx
index b192699b..5f48769a 100644
--- a/fern/docs/pages/tools/code-execution.mdx
+++ b/fern/docs/pages/tools/code-execution.mdx
@@ -15,7 +15,7 @@ The code execution subsystem provides:
| Type identifier | Role |
|---|---|
-| `code_execution_v1` | Top-level wrapper — expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` |
+| `code_execution_v1` | Top-level wrapper — expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` and `present_server_v1` |
| `bash_v1` | Run arbitrary bash commands in a sandboxed subprocess |
| `text_editor_v1` | View, create, and edit files in the workspace |
| `view_v1` | View file or directory contents |
@@ -23,11 +23,12 @@ The code execution subsystem provides:
| `create_v1` | Create a new file |
| `insert_v1` | Insert text after a given line |
| `present_files_v1` | Expose generated files to the caller |
+| `present_server_v1` | Expose an HTTP service running in the sandbox to the caller |
Standalone endpoints are not provided for code execution tools. Use them through `/v1/messages`.
- `present_files_v1` is a server-only metadata tool that surfaces created files to API consumers. It is enabled automatically when `code_execution_v1` is used and does not require explicit configuration.
+ `present_files_v1` and `present_server_v1` are server-only metadata tools that surface generated artifacts and running services to API consumers. They are enabled automatically when `code_execution_v1` is used and do not require explicit configuration.
---
@@ -36,7 +37,7 @@ Standalone endpoints are not provided for code execution tools. Use them through
When the model submits a `code_execution_v1` tool call, the server expands it into its constituent tools, creates or reuses a per-session sandbox, and executes the requested operations:
-1. **Tool expansion** — `code_execution_v1` expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1`
+1. **Tool expansion** — `code_execution_v1` expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` and `present_server_v1`
2. **Session management** — each chat session gets an isolated workspace on the host filesystem
3. **Path translation** — canonical LLM-visible paths (`/home/agent/workspace/`) are transparently mapped to real host paths
4. **Subprocess isolation** — bash commands run as child processes with OS-level resource limits (`setrlimit`)
@@ -189,3 +190,23 @@ Code execution sessions are managed automatically:
| Windows | None | None | Subprocesses run without resource limits |
All platforms support the same canonical path translation, output scrubbing, and session lifecycle management.
+
+---
+
+## `present_server` tool
+
+`present_server_v1` exposes an HTTP service running inside the sandbox to the caller. Call it after starting a server (for example a web app, Jupyter, or Streamlit) so the user can open or interact with it.
+
+The model invokes it with:
+
+| Field | Type | Required | Description |
+|---|---|---|---|
+| `port` | `int` | Yes | Port the service is listening on inside the sandbox. |
+| `service_name` | `str` | No | Human-readable name for the service (defaults to `"App"`). |
+| `initial_path` | `str \| null` | No | Optional path appended to the URL to deep-link to a specific route. |
+
+The tool result contains a `resource_link` block whose `uri` is the publicly reachable URL of the tunneled service, plus a `text` block describing the service. If the sandbox backend does not support HTTP ingress, the tool returns a `text` block explaining that no endpoint is available.
+
+
+ HTTP ingress support depends on the configured `code_execution.provider`. Only sandbox backends that support per-session endpoint tunneling can fulfill `present_server_v1` calls.
+
diff --git a/ui/docs/SOURCE_OF_TRUTH.md b/ui/docs/SOURCE_OF_TRUTH.md
index 107380ff..4bd7f36e 100644
--- a/ui/docs/SOURCE_OF_TRUTH.md
+++ b/ui/docs/SOURCE_OF_TRUTH.md
@@ -55,7 +55,8 @@ These are things not obvious from reading `index.html` that future agents should
- **Scroll fades** — `.chat-list-wrap` and `.messages` both use `mask-image` with `--fade-top-stop`/`--fade-bot-stop` custom properties updated on scroll by `updateChatListFade()` and `updateMessagesFade()`.
- **Toggle switches** — all `input[type="checkbox"]` elements are styled as custom CSS pill toggles with no native appearance.
- **Floating panel frost** — `.modal-card`, `.menu-panel`, and `.model-dropdown` override the shared glass group with a near-solid dark background (`rgba(10,12,22,0.82–0.94)`), `blur(72px) saturate(1.4)`, and a `to bottom` gradient (lighter at top, denser at bottom) for readability and visual grounding.
-- **Code Execution tools** — the Code Execution toggle in the Tools menu sends `{ name: "code_execution", type: "code_execution_v1" }` in the tools array. The backend expands this into `bash`, `text_editor` (view/str_replace/create/insert), and `present_files` (Zylon only). Tool use blocks for these tools render as styled `.code-exec-block` details elements with terminal output, line-numbered file views, diff highlighting, and exit-code badges. Adjacent tool_use + tool_result blocks are combined into a single block via blocks pairing in `blocksToHtml`. The toggle is stored per-chat in `chat.settings.enabledCodeExecution`.
+- **Code Execution tools** — the Code Execution toggle in the Tools menu sends `{ name: "code_execution", type: "code_execution_v1" }` in the tools array. The backend expands this into `bash`, `text_editor` (view/str_replace/create/insert), `present_files`, and `present_server`. Tool use blocks for these tools render as styled `.code-exec-block` details elements with terminal output, line-numbered file views, diff highlighting, and exit-code badges. Adjacent tool_use + tool_result blocks are combined into a single block via blocks pairing in `blocksToHtml`. The `isCodeExecTool` allowlist (`bash`, `view`, `str_replace`, `create`, `insert`, `present_files`, `present_server`) drives pairing and per-tool rendering. The toggle is stored per-chat in `chat.settings.enabledCodeExecution`.
- **Code Execution session continuity** — when code execution is enabled, `chat.id` is sent as `container` in `ChatBody` so the backend reuses the same sandbox session across all messages in a chat. The `container` field must be set whenever code execution tools are active.
- **Code Execution file upload** — when Code Execution is enabled, a **Files** button appears in the composer toolbar. It opens a file picker that uploads directly to `POST /v1/files?scope_id={chat.id}` (multipart/form-data). Uploaded files land in the session workspace and are accessible to the model's bash/file tools.
- **Code Execution file downloads** — `present_files` tool results that contain `local_resource` blocks are rendered as `.code-exec-download` anchor elements linking to `GET /v1/files/{file_id}/content?scope_id={chat.id}`. The `file_id` and `mime_type` come from the `local_resource` block schema.
+- **Code Execution server links** — `present_server` tool results that contain `resource_link` blocks are rendered as `.code-exec-server-link` anchor elements (globe icon + service name + tunneled URL) opening in a new tab. The `uri`, `name`, and `description` come from the `resource_link` block schema. The tool_use summary shows `service_name:port` (and an optional `initial_path` deep-link).
diff --git a/ui/index.html b/ui/index.html
index 04e5e4eb..1444cb65 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -2407,6 +2407,11 @@
color: rgba(200, 170, 255, 0.92);
}
+ .code-exec-block .code-exec-tool-label.server {
+ background: rgba(112, 183, 255, 0.14);
+ color: rgba(140, 200, 255, 0.92);
+ }
+
/* ── Code execution file downloads ──────────────────────────────────── */
.code-exec-download {
display: inline-flex;
@@ -2447,6 +2452,46 @@
flex: 0 0 auto;
}
+ /* ── Code execution server links (present_server) ──────────────────── */
+ .code-exec-server-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border: 1px solid rgba(112, 183, 255, 0.22);
+ border-radius: 10px;
+ background: rgba(112, 183, 255, 0.08);
+ color: rgba(140, 200, 255, 0.92);
+ font-size: 13px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ text-decoration: none;
+ transition: background 150ms ease, border-color 150ms ease;
+ cursor: pointer;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .code-exec-server-link:hover {
+ background: rgba(112, 183, 255, 0.16);
+ border-color: rgba(112, 183, 255, 0.36);
+ }
+ .code-exec-server-link svg {
+ flex: 0 0 auto;
+ width: 14px;
+ height: 14px;
+ }
+ .code-exec-server-link .code-exec-server-name {
+ font-weight: 600;
+ flex: 0 0 auto;
+ }
+ .code-exec-server-link .code-exec-server-url {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ opacity: 0.85;
+ }
+
/* ── Modal backdrop blur ─────────────────────────────────────────────── */
.modal-backdrop {
backdrop-filter: blur(14px);
@@ -5191,11 +5236,11 @@
}
function isCodeExecTool(name) {
- return ["bash", "view", "str_replace", "create", "insert", "present_files"].includes(name);
+ return ["bash", "view", "str_replace", "create", "insert", "present_files", "present_server"].includes(name);
}
function codeExecToolLabel(name) {
- const map = { bash: "Bash", view: "Read", str_replace: "Edit", create: "Create", insert: "Insert", present_files: "Present" };
+ const map = { bash: "Bash", view: "Read", str_replace: "Edit", create: "Create", insert: "Insert", present_files: "Present", present_server: "Server" };
return map[name] || name;
}
@@ -5205,6 +5250,7 @@
if (name === "create") return "create";
if (name === "str_replace") return "edit";
if (name === "present_files") return "present";
+ if (name === "present_server") return "server";
return "file";
}
@@ -5231,6 +5277,11 @@
} else if (name === "present_files") {
const files = Array.isArray(input.files) ? input.files : [];
detail = `
${files.map(f => `
${icons.file}${escapeHtml(typeof f === "string" ? f : f.path || f.name || "")}
`).join("")}
`;
+ } else if (name === "present_server") {
+ const port = input.port != null ? `:${input.port}` : "";
+ const service = input.service_name || "App";
+ const initialPath = input.initial_path ? ` → ${input.initial_path}` : "";
+ detail = `${icons.globe}${escapeHtml(service)}${escapeHtml(port)}${escapeHtml(initialPath)}
`;
}
// If we have a result block, render the result content in the same body
@@ -5279,6 +5330,28 @@
} else {
resultHtml = rawText ? `${escapeHtml(rawText)}
` : "";
}
+ } else if (name === "present_server") {
+ const content = resultBlock.content;
+ if (Array.isArray(content)) {
+ const linkItems = content.filter(b => b?.type === "resource_link");
+ const textItems = content.filter(b => b?.type === "text" || typeof b === "string");
+ const textHtml = textItems.map(b => escapeHtml(typeof b === "string" ? b : (b.text || ""))).join(" ");
+ if (linkItems.length > 0) {
+ resultHtml = `${linkItems.map(l => {
+ const url = l.uri || "";
+ const svc = l.name || "App";
+ const desc = l.description ? escapeHtml(l.description) : "";
+ if (url) {
+ return `
${icons.globe}${escapeHtml(svc)}${escapeHtml(url)}${desc ? `
${desc}
` : ""}`;
+ }
+ return `
${icons.globe}${escapeHtml(svc)}
`;
+ }).join("")}
${textHtml ? `${textHtml}
` : ""}`;
+ } else {
+ resultHtml = textHtml ? `${textHtml}
` : "";
+ }
+ } else {
+ resultHtml = rawText ? `${escapeHtml(rawText)}
` : "";
+ }
} else {
resultHtml = renderFileView(rawText);
}
@@ -5287,7 +5360,7 @@
}
}
- const summaryLabel = name === "bash" ? (input.command ? input.command.split("\n")[0].slice(0, 60) + (input.command.length > 60 ? "…" : "") : "") : (input.path || "");
+ const summaryLabel = name === "bash" ? (input.command ? input.command.split("\n")[0].slice(0, 60) + (input.command.length > 60 ? "…" : "") : "") : name === "present_server" ? `${input.service_name || "App"}${input.port != null ? `:${input.port}` : ""}` : (input.path || "");
return `${label}${escapeHtml(summaryLabel)}
${detail}
`;
}