8000 feat(chat): show the code sandbox's runs in the conversation and download the files it makes by adeelahmad · Pull Request #497 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

feat(chat): show the code sandbox's runs in the conversation and download the files it makes - #497

Merged
adeelahmad merged 27 commits into
mainfrom
claude/project-thread-r960xc-c7-ui
Oct 17, 2026
Merged

adeelahmad merged 27 commits into
mainfrom
claude/project-thread-r960xc-c7-ui

Conversation

@adeelahmad
@adeelahmad adeelahmad commented Oct 17, 2026 •
Copy link
Copy Markdown
Owner

Description

Before: when the assistant ran Python or bash, the conversation showed only a one-line summary such as "Ran Python (2 lines), ok". The code and what it printed weren't shown. A file the sandbox made could reach the person only as an import into a namespace, after an approval.

After: each run is a folded step under the answer. Opening it shows the code, then the start of stdout, the end of stderr (in red) and how it ended (exit code, timed out, cut short). The new assistant tool share_sandbox_file hands the person a file the sandbox made, like a chart or a converted document, as a download button on the answer. Lens keeps the file encrypted with the conversation and deletes it with the conversation or the person's account.

How:

  • Toolbox.detail carries what a tool adds to its step. chat.tool_answer puts it on the step event, so runs get output (sandbox.shown, 4,000 characters of each stream) and shares get file ({id, name, size}). The steps are saved with the answer as before, and ToolStep gains output and file.
  • sandbox.share copies the file out (25 MB at most) and writes it to data_dir/chat-files/<chat>/<id>, mode 0600. It's AES-256-GCM encrypted with a key derived from the server's secret and bound to the file and its conversation. A chat_file row records it, at most 20 a conversation.
  • GET /api/v1/chats/{cid}/files/{fid} serves the file only to the conversation's person, as an attachment with nosniff and a sandbox content policy.
  • Deleting the conversation (sandbox.forget) or removing the account deletes the files.
  • In the web app, RunDetail in components/chat/steps.tsx shows the code and output, and SharedFiles (components/chat/shared-files.tsx) adds the download chips to the chat page and the page assistant.

This is PR 3 of 3 for the code sandbox, after #473 (merged). It's based on main and includes the gateway PR's commits (PR 2, #495) until that merges, so review the commit feat(chat): show the code sandbox's runs in the conversation and download the files it makes.

Motivation and Context

Adeel asked for standard Python and bash tools for the chat app (chat gaps, module C7). This PR is the part people see: the code that ran, its output, and the files it made.

Screenshots (if appropriate):

n/a (a step that opens to code and output, and download chips on the answer, in the existing chat styles).

Steps to reproduce (if appropriate):

  1. Turn on Settings → Code sandbox.
  2. Ask the assistant "plot y = x² for x from 0 to 10 as an SVG and give it to me".
  3. Open the Ran Python step to see the code and its output, then click the chart.svg chip on the answer to download it.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change; ! in the title)

Checklist:

  • The title is a Conventional Commit.
  • My code follows the code style of this project (make hooks checks it on commit).
  • I added or updated tests.
  • My change requires documentation updates, and I have updated docs/ accordingly.
  • Users need more than the title to understand it, so I wrote it under ## Unreleased in CHANGELOG.md.

Done questions (AGENTS.md)

  • Goal: the code sandbox is complete across its three PRs. The host agent's sandbox profile is C6's PR. Merging is the coordinator's call.
  • Agents: the step's output and file are in the SSE stream and in GET /api/v1/chats/{cid}. An agent downloads the file from GET /api/v1/chats/{cid}/files/{fid}.
  • Zero setup: nothing new to install or configure. The chat_file table is defined at start.
  • Fewest clicks: one click opens a step and one click downloads a file. The model shares files itself, so the person doesn't ask for them.
  • Configurable: no new setting. The 20-file cap is fixed for now and listed as refine later.
  • Light: no new dependency. Files are kept only when the model shares one.
  • Private and secure: files are encrypted at rest, served only to the conversation's person and never rendered by Lens. They're deleted with the conversation or the account. The threat model gains the download entry point and the updated P-SANDBOX. ci/validate_threat_model.py passes.
  • Cost: no model calls. A run's output goes on the step the person sees, not back to the model.
  • User decides: sharing only hands the person a download and changes nothing in their archive. Importing still waits for their approval.
  • Upgrades: answers saved earlier have no output or file and show as before. The new table needs no migration.
  • Honest: docs label the step and downloads Built. Inline images and the cap as a setting are listed as refine later.
  • Tests, docs and generated files: 2 new backend tests and more checks in tests/api/test_sandbox.py (output on the step, live and saved; share and download; owner only; encrypted on disk; the cap; deletion with the conversation and the account). __tests__/sandbox-steps.test.tsx covers the stream, the step and the chips. docs/assistant.md (Code sandbox) is updated, and the OpenAPI files come from make openapi.
  • Open: no new dependency (cryptography was already required).
  • Reported: the refine-later items are in docs/assistant.md#code-sandbox: inline images and the cap as a setting.

Local CI (ci/local-ci.sh 7611ee8b --jobs backend,frontend,pre-commit,title): FastAPI pass (1025s), Next.js pass (277s), pre-commit pass (87s), PR title pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B

claude added 17 commits October 17, 2026 00:09
…ersation

The assistant gets run_python, run_bash, sandbox_files and import_sandbox_file
once an admin turns on Settings > Code sandbox (sandbox.
10BC8
enabled, off by
default) and points Lens at an NVIDIA OpenShell gateway. Each conversation
gets its own sandbox (lens-chat-<id>) with none of Lens's data or keys, no
egress unless sandbox.network allows the package registries or public
addresses, and its files kept between runs. Attachments go in by id; a file
comes out only through an import_files approval. With the network on, a run
after outside text asks first. Idle sandboxes are stopped and unused ones
deleted by the routines loop, and deleting a conversation deletes its sandbox.

OpenShell's Python client is the `sandbox` extra, fetched as a component when
the sandbox is turned on; tests run it against a fake gRPC gateway.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
The last check asked for 2026-10-17's note after the call without a date had made today's note, so on that day it
found the note the deleted root had left behind. It now asks for a day no earlier call makes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…-r960xc-c7-sandbox

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…ings

The Docker Compose install gets a sandbox profile: openshell-init makes the
gateway's certificates once, openshell-key copies the client's to a volume
only Lens reads, and openshell-gateway runs NVIDIA OpenShell 0.1.4 with the
Docker driver (no capabilities, read-only, telemetry off). Lens signs in to it
over mutual TLS.

Settings > Code sandbox > Run the gateway on this machine asks the upgrade
agent to start it (action=services, profile=sandbox) and turns the sandbox on
at openshell-gateway:17670, after a preflight (the Compose install, a 64-bit
machine, Landlock 3). Off deletes every sandbox first. The same over
GET/PUT /api/v1/admin/sandbox and the MCP tools sandbox_gateway and
set_sandbox_gateway. An agent that knows only monitoring says to start it by
hand, with the line to run.

The network setting web becomes hosts: OpenShell resolves only names its
policy lists, so the sandbox reaches the package registries and the names an
admin lists in sandbox.hosts, at public addresses only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…sandbox' into claude/project-thread-r960xc-c7-gateway
…network

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
# Conflicts:
#	docs/assistant.md
#	fastapi_backend/app/domain/ai_tools.py
#	fastapi_backend/app/domain/components.py
#	fastapi_backend/app/domain/settings.py
#	fastapi_backend/pyproject.toml
#	fastapi_backend/uv.lock
#	threat-model.md
#	threat-model.yaml
…load the files it makes

Each run_python or run_bash step carries the start of its stdout, the end of its stderr and how it ended; the web app shows it folded under the answer with the code. share_sandbox_file copies a file out of the sandbox, kept encrypted with the conversation, as a download button on the answer (GET /api/v1/chats/{cid}/files/{fid}), deleted with the conversation or the account.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…sandbox' into claude/project-thread-r960xc-c7-gateway

# Conflicts:
#	fastapi_backend/app/domain/ai_tools.py
#	threat-model.yaml
…oject-thread-r960xc-c7-ui

# Conflicts:
#	threat-model.yaml
…-r960xc-c7-gateway

# Conflicts:
#	CHANGELOG.md
#	docs/assistant.md
#	docs/configuration.md
#	fastapi_backend/app/domain/ai_tools.py
#	fastapi_backend/app/domain/sandbox.py
#	fastapi_backend/app/domain/settings.py
#	fastapi_backend/app/domain/store.py
#	fastapi_backend/tests/api/test_sandbox.py
#	fastapi_backend/tests/fake_openshell.py
#	nextjs-frontend/components/settings/bodies.tsx
#	nextjs-frontend/components/settings/model.ts
#	threat-model.json
#	threat-model.md
#	threat-model.yaml
…-r960xc-c7-gateway

# Conflicts:
#	threat-model.yaml
…oject-thread-r960xc-c7-ui

# Conflicts:
#	fastapi_backend/app/api/v1/routes/chats.py
#	fastapi_backend/app/domain/ai_tools.py
#	fastapi_backend/app/domain/chat.py
#	fastapi_backend/app/schemas/chats.py
#	nextjs-frontend/app/openapi-client/sdk.gen.ts
#	nextjs-frontend/app/openapi-client/types.gen.ts
#	nextjs-frontend/components/chat/stream.ts
#	nextjs-frontend/openapi.json
#	threat-model.yaml
…-r960xc-c7-gateway

# Conflicts:
#	CHANGELOG.md
#	fastapi_backend/app/api/mcp_tools.py
…-r960xc-c7-gateway

# Conflicts:
#	threat-model.yaml
@adeelahmad adeelahmad self-assigned this Oct 17, 2026
…oject-thread-r960xc-c7-ui

# Conflicts:
#	CHANGELOG.md
#	threat-model.yaml
…-r960xc-c7-gateway

# Conflicts:
#	threat-model.yaml
…oject-thread-r960xc-c7-ui

# Conflicts:
#	threat-model.yaml
Ported from #512 so this PR's upgrade check passes while that lands: the
entity-types step re-types weekday Terms as Dates, which the default entity
list leaves out, so the check read them as gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…gateway' into port-c7u

# Conflicts:
#	threat-model.yaml
Copy link
Copy Markdown
Owner Author

"Upgrade from the last release" failed on 7611ee8 for the same reason as on #495, and the cause isn't this PR. On main, #395's entity-types step re-types "Monday" and "Friday" as Dates, and the check's entity list leaves Dates out. 33fd96f merges #495's branch, which carries #512's fix to the check and main. With that fix, the upgrade check passes locally from v0.13.0.


Generated by Claude Code

# Conflicts:
#	CHANGELOG.md
#	docs/assistant.md
#	fastapi_backend/app/domain/sandbox.py
#	fastapi_backend/tests/api/test_sandbox.py
#	nextjs-frontend/app/openapi-client/index.ts
#	nextjs-frontend/app/openapi-client/types.gen.ts
#	nextjs-frontend/openapi.json
#	threat-model.json
#	threat-model.md
#	threat-model.yaml
@adeelahmad
adeelahmad merged commit 59184e2 into main Oct 17, 2026
12 checks passed
@adeelahmad
adeelahmad deleted the claude/project-thread-r960xc-c7-ui branch October 17, 2026 07:44
adeelahmad pushed a commit that referenced this pull request Oct 18, 2026
…ay and clipper sign-in as built

Since the last README refresh, #474, #484, #510, #511, #497, #495, #406, #516, #517
and #491 merged. The README's built list now says a conversation exports as Markdown,
PDF or JSON and its answers, questions and code copy; a Tools chip picks an answer's
tools, limit and namespaces; sandbox runs open under the answer with downloads, and
Settings starts the gateway; deep research plans, runs and returns a cited report;
ready-made MCP servers (the browser, GitHub) and the optional containers start from
the web app; and the web clipper signs in or takes an API token. Each has its checks
in docs/requirements.md. In progress drops the merged PRs and lists the open feature
PRs; the Planned proof item keeps only the Raspberry Pi timing (#481 and #482 are
closed); the docs index links requirements and checks, and published feeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS
adeelahmad pushed a commit that referenced this pull request Oct 18, 2026
…ay and clipper sign-in as built

Since the last README refresh, #474, #484, #510, #511, #497, #495, #406, #516, #517,
#491, #400 and #506 merged. The README's built list now says the assistant answers
questions about Lens itself; a conversation exports as Markdown, PDF or JSON and its
answers, questions and code copy; a Tools chip picks an answer's tools, limit and
namespaces; sandbox runs open under the answer with downloads, and Settings starts the
gateway; deep research plans, runs and returns a cited report; ready-made MCP servers,
the MCP servers page and the optional containers start from the web app; and the web
clipper signs in or takes an API token. Each has its checks in docs/requirements.md.
In progress lists only open PRs; the Nebula mesh (#546, merged, partly built) moves to
Planned with what's next; the Planned proof item keeps only the Raspberry Pi timing;
the docs index links requirements and checks, and published feeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS
adeelahmad added a commit that referenced this pull request Oct 18, 2026
…ay and clipper sign-in as built (#536)

Since the last README refresh, #474, #484, #510, #511, #497, #495, #406, #516, #517,
#491, #400 and #506 merged. The README's built list now says the assistant answers
questions about Lens itself; a conversation exports as Markdown, PDF or JSON and its
answers, questions and code copy; a Tools chip picks an answer's tools, limit and
namespaces; sandbox runs open under the answer with downloads, and Settings starts the
gateway; deep research plans, runs and returns a cited report; ready-made MCP servers,
the MCP servers page and the optional containers start from the web app; and the web
clipper signs in or takes an API token. Each has its checks in docs/requirements.md.
In progress lists only open PRs; the Nebula mesh (#546, merged, partly built) moves to
Planned with what's next; the Planned proof item keeps only the Raspberry Pi timing;
the docs index links requirements and checks, and published feeds.


Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS

Co-authored-by: Adeel Ahmad <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0