Skip to content

Artifacts and sandboxed code execution: tools, workflows, and document I/O#2568

Merged
dartpain merged 60 commits into
mainfrom
artifacts-and-code-executors
Jul 10, 2026
Merged

Artifacts and sandboxed code execution: tools, workflows, and document I/O#2568
dartpain merged 60 commits into
mainfrom
artifacts-and-code-executors

Conversation

@dartpain

@dartpain dartpain commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an artifacts + code-execution system to DocsGPT. Agents and workflows can run sandboxed Python, generate and version documents, extract content from uploaded files, and pass results between steps by reference. Artifacts are first-class, versioned, access-controlled objects; code runs in a pluggable sandbox (a self-hosted Jupyter Kernel Gateway runner or Daytona Cloud).

What's added

Artifacts

  • artifacts / artifact_versions tables (migration 0025_artifacts) + repository; append-only versioning; parent-derived authorization (a conversation or a workflow run); shared-link access inherits.
  • REST: list / get / download (with S3 presign) / restore.
  • Frontend: an artifact sidebar with version history, download, and inline preview (text / markdown / csv / json / html / images; mermaid rendered in a sandboxed iframe).

Code execution

  • A CodeSandbox abstraction with two backends: a self-hosted Jupyter Kernel Gateway runner (always-on container under deployment/sandbox/) and Daytona Cloud (per-session VM). SandboxManager enforces a per-process session cap, LRU-idle eviction, and a Celery-beat reaper.
  • Tools: code_executor (run code → persisted artifacts, approval-gated, agent-selectable TTL), artifact_generator (create / edit / rewrite presentations, documents, spreadsheets, pdf, html from an injection-safe spec), and document_extractor (Docling).

Workflows

  • A code node plus an artifacts.* templating namespace (pass-by-reference between steps).
  • Document I/O: uploaded attachments are bridged into run-scoped artifacts that nodes read as agent.input_documents and short refs A1, A2; code nodes read prior state from a state.json data file. The builder surfaces a run's outputs in an "Artifacts" panel, and the Preview can attach input documents.
  • Together these enable multi-step document workflows — e.g. a compliance check that classifies wire-transfer documents, extracts fields, runs rule checks, decides allow / decline / needs-human, and emits a rule-table report document.

Security

  • Sandbox: path-traversal-safe file I/O, output / time / size caps, illegal-session rejection, per-session 0700 workspaces, and kernels run under a scrubbed environment so no API keys, tokens, database URL, or gateway token reach kernel code.
  • Code nodes pass state as data (state.json), never templated into the program, so untrusted document content cannot become executed code.
  • Egress SSRF is blocked at the network layer (a k8s NetworkPolicy / compose overlay denying RFC1918, link-local, and metadata ranges).
  • Artifact access is parent-derived; attachment and workflow-run ownership are enforced server-side.
  • Isolation note: the Jupyter runner is a single trust domain — all kernels share one container and uid, so sibling-workspace reads are possible. For per-tenant isolation use the Daytona backend (per-session VM); run the runner under gVisor for host protection. This is documented in deployment/sandbox/README.md.

Config & deployment

  • New settings: SANDBOX_BACKEND (jupyter | daytona), SANDBOX_GATEWAY_URL, SANDBOX_KERNEL_NAME (set to docsgpt-python to select the env-scrubbing kernel), DAYTONA_API_KEY, plus artifact quota / TTL caps.
  • New docsgpt-sandbox service in deployment/docker-compose.yaml, the k8s manifests, and the egress NetworkPolicy. Docling install is gated behind an INSTALL_DOCLING build arg.
  • Run alembic upgrade head for migration 0025_artifacts.

Testing

ruff clean repo-wide; backend unit + integration suites pass (including a live Jupyter-gateway integration suite and the sandbox / workflow / artifact tests); frontend lint, build, and vitest pass. Each slice was reviewed for code, security, and end-to-end behavior before being committed.

Demo

A mock compliance workflow runs end-to-end (classify → extract → compute → decide → emit a rule-table .docx). A demo GIF will be attached to this PR.

dartpain added 20 commits June 24, 2026 10:21
Introduce artifacts and artifact_versions tables: an append-only,
pass-by-reference store with a stable identity row and immutable
versions that atomically bump current_version. Access is parent-derived
(conversation or workflow run); a CHECK enforces at least one parent and
a nullable team_id is reserved for future team scoping.

Adds the 0025_artifacts migration, ArtifactsRepository with a
parent-scoped safe getter alongside the unscoped one, and repo tests.
Serve artifact metadata and bytes over HTTP with parent-derived
authorization: conversation-parented artifacts inherit conversation
access (owner, shared_with, or a public share token whose conversation
matches the parent), workflow-run artifacts check run ownership, and
access fails closed when the parent is missing or deleted.

Adds list/get/versions/download/restore routes, an authenticated
storage-agnostic download (sanitized Content-Disposition, 302 to a
short-lived private S3 presigned URL when that strategy is configured),
a generate_presigned_url primitive on the storage base and S3 backend,
and generalizes the tools artifact endpoint for documents/files. Shared
authorization helpers live in a dedicated module used by both surfaces.
…nner)

Introduce a pluggable CodeSandbox abstraction with a SandboxManager and a
Jupyter Kernel Gateway backend: the app is a client of a single always-on
runner that executes code in stateful in-process kernels (no child-container
spawning, no docker socket). Sessions bind to a conversation or workflow run
with an agent-selectable TTL clamped by a global cap; execution enforces a
wall-clock deadline with interrupt-on-timeout and capped output, and file
transfer is workspace-contained with size and integrity checks. Adds a
docsgpt-sandbox docker-compose service (resource-capped, read-only, internal
network) plus settings, with a real local-gateway integration test.
Extend the artifact sidebar with a document/file view: a version selector,
authenticated download honoring Content-Disposition (following the backend
redirect to a presigned URL when configured), and version restore. Preview is
chosen by kind - html, svg, and mermaid render inside a sandboxed iframe with a
restrictive CSP (mermaid via its own sandboxed render so untrusted diagram text
never reaches the app DOM), office documents and PDFs show a download card, and
code or data render as text. Wires the new artifact endpoints into the API
service and leaves the existing notes/todo path unchanged.
Add a pluggable Daytona backend (SANDBOX_BACKEND=daytona) that runs code on
Daytona Cloud via the Apache-2.0 SDK and DAYTONA_API_KEY, conforming to the
CodeSandbox interface. Sessions reattach to a labelled cloud sandbox by id
(waking a stopped one) and are created crash-safe so a failed setup never
orphans a paid sandbox; a configurable ceiling caps concurrent sandboxes and
auto-delete is clamped on so orphans always self-reap. File transfer is
workspace-contained with a size guard. Includes mocked unit tests and an
opt-in live smoke test.
Add a code_executor agent tool with a run_code action that runs agent-provided
code in the per-conversation sandbox session and captures produced files as
artifacts. Inputs are materialized only from artifacts the caller can access
(parent-scoped); produced files are stored under the user's namespace with
server-computed size and sha256, and the storage write is ordered last in the
transaction so a failure cannot orphan bytes. Output is a compact payload with
no raw bytes, and the produced artifact lights up the existing tool artifact
rail. Execution honors a wall-clock timeout and an agent-selectable session TTL
clamped by the global cap, and the action can be gated behind approval.
Tool-call argument logging is redacted so code bodies are not written to logs.
Add a code workflow node that runs code in the run-scoped sandbox session and
writes produced files as artifact references into workflow state, passing them
by reference (only id and metadata, never bytes) so downstream nodes and CEL
conditions can branch on them. Add an artifacts.* templating namespace that
resolves those references to metadata via a run-scoped lookup, available to
both the workflow engine and the prompt renderer. Extract the sandbox-to-
artifact persistence into a shared helper reused by the code node and the
code_executor tool.
Add an artifact generator tool with create, edit, and rewrite actions that
render presentations, documents, spreadsheets, and PDFs from a validated JSON
spec by running a fixed program in the sandbox (the spec travels as data, so
its contents can never execute). The spec is stored as the source of truth on
each artifact version; edits apply a JSON merge-patch and re-render, appending
a new version while earlier versions stay intact.
Add a document extractor tool with an extract_document action that converts an
input artifact (PDF, docx, pptx, ...) into structured JSON by running a fixed
Docling program in the sandbox (the document and parameters travel as data, so
nothing in them can execute). Output is a compact payload bounded by an input
size cap, a head-and-tail markdown window, and per-table caps, can be validated
against a JSON schema, and is persisted as a data artifact by reference. Docling
is heavy, so it stays out of the base image and ships in an opt-in sandbox image
variant.
Add runtime governance for the sandbox and artifact store: a per-process
concurrent-session cap with least-recently-used eviction of idle sessions and a
periodic idle reaper (Celery beat), so sandbox kernels do not accumulate. The
session manager performs all backend start/stop outside its lock and tears down
the captured handle, so eviction never closes a concurrently re-opened session.
Add per-user artifact quotas (count, total bytes, and per-file size) enforced at
persistence time as a soft cap, and best-effort cleanup of per-render scratch
directories in the sandbox workspace.
Expose a user's artifacts through the MCP server as readable resources: each is
listed under an artifact:// URI with its mime type and read on demand as inline
text or a base64 blob, bounded by a size cap and scoped strictly to the owning
principal resolved from the request's API key, so no artifact is served across
tenants. Ship the network-level egress controls the sandbox runner needs but
cannot self-apply: a Kubernetes NetworkPolicy that allows public egress while
denying RFC1918, link-local, and cloud-metadata ranges, an optional
docker-compose egress overlay, and runner network-hardening docs.
The output-cap test flooded ~100MB through a 20s wall-clock timeout, so under
heavy parallel load the timeout could fire before enough output accumulated to
trip the cap, intermittently failing. Emit a bounded ~200 KiB (4x the cap)
under a generous timeout so the truncation path triggers deterministically.
Render produced-artifact bytes inline when a version has no embedded spec:
HTML and SVG in the existing sandboxed iframe (with the restrictive CSP),
images inline via a blob URL, and Markdown/CSV/JSON/plain text as escaped text;
office documents and PDFs keep the download card. Bytes are fetched through the
existing authenticated download path, version-aware, with object URLs revoked on
change. Untrusted HTML/SVG renders only inside the scriptless sandboxed iframe
and Markdown is rendered without raw HTML, so artifact content cannot script the
app.
Give each produced artifact a short virtual handle - A1, A2, ... - the n-th
artifact in the conversation or workflow run (case-insensitive, not stored). The
tools return it in their results, and the edit, rewrite, and input parameters
accept either the handle or the full id. A handle resolves only within the
caller's own conversation or run, and the resolved id is still checked against
that parent before any read, so it cannot reach another tenant's artifact. This
fixes the model creating a duplicate instead of a new version when asked to
edit an artifact.
Expose the workflow code node in the builder UI: a draggable palette entry, a
canvas node, and a config panel for the code, inputs, output variable, timeout,
and an optional JSON schema (validated before save). The panel serializes to the
same config the engine reads, so a code node built in the UI runs and produces an
artifact reference; workflows without code nodes load unchanged.
Add an html kind to the artifact generator: it renders a styled HTML report from
a stored JSON spec (title + heading/paragraph/list/table/code blocks), producing
text/html bytes the sidebar already previews inline in the sandboxed iframe,
while keeping the spec as the source of truth so edit and rewrite make real
versions. So a single artifact both renders inline and versions conversationally.
The renderer is a fixed program fed the spec as data, and every spec text value
is HTML-escaped into the output.
Stamp the engine's workflow_run_id into a workflow agent node's tool config, so
artifact_generator, code_executor, and document_extractor address artifacts by
the workflow run. An artifact (and its short ref like A1) created by one agent
node is then visible to a later code node and editable by a later agent node in
the same run - enabling an 'agent generates, a code node transforms, a second
agent edits' flow. Workflow agent nodes carry no conversation id, so only the
run scope applies.
Let workflow runs consume and produce documents end to end: bridge uploaded
attachments into run-scoped artifacts so nodes receive the input documents
(with a per-run cap and server-computed size/sha256, and the run row pre-created
so produced artifacts are authorized during the run); emit the run id to the
client and add a builder panel that lists, previews, and downloads a run's
artifacts; and allow attaching documents to a Preview run via the existing
upload flow.

Also fixes issues a compliance workflow surfaced: attachment ownership now keys
on the raw identity instead of a sanitized one (the sanitized form could not be
read back and could collide across users); workflow code nodes read prior state
from a state.json data file instead of templating it into the program, so
untrusted document content can never be interpolated into executed code;
structured node output wrapped in code fences is recovered; and the live
speech-to-text ownership check compares the raw identity.
When an executed tool call raises, append the assistant message carrying the
tool_calls before the tool error result, mirroring the success path. Previously
only the tool result was appended, leaving a tool message with no preceding
tool_calls parent, which made the next provider completion reject the request.
Every tool_call id in a batch is now answered even when some calls fail.
Run each kernel under a scrubbed environment so untrusted code can never read
the host's secrets. A custom 'docsgpt-python' kernelspec launches ipykernel
through a wrapper that keeps only what the kernel needs (PATH, HOME, LANG, and
the Jupyter runtime/data dirs), dropping API keys, tokens, the database URL, and
the gateway token. The app selects this kernel by name via SANDBOX_KERNEL_NAME,
so the distinct name is never shadowed by the stock python3 spec. Per-session
workspaces are created mode 0700 (defense in depth under the shared uid). The
README documents the runner as a single trust domain and points to the Daytona
backend for per-tenant isolation.
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nextra-docsgpt Building Building Preview, Comment Jul 9, 2026 8:00pm
oss-docsgpt Ready Ready Preview, Comment Jul 9, 2026 8:00pm

Request Review

Comment thread application/sandbox/jupyter_gateway.py Fixed
Comment thread tests/agents/test_workflow_input_documents.py Fixed
Comment thread tests/sandbox/test_sandbox_manager.py Fixed
Comment thread tests/api/user/test_artifacts_routes.py Fixed
Comment thread tests/agents/tools/test_document_extractor_unit.py Fixed
Comment thread application/sandbox/artifacts_capture.py Dismissed
Comment thread application/sandbox/artifacts_capture.py Fixed
Comment thread application/alembic/versions/0025_artifacts.py Fixed
Comment thread application/alembic/versions/0025_artifacts.py Fixed
Comment thread application/alembic/versions/0025_artifacts.py Fixed
Comment thread application/alembic/versions/0025_artifacts.py Fixed
Add the run input-documents variable to the workflow builder's variable
dropdown (workflow input) and the artifact-by-id lookup to both the workflow
builder and the agent prompt builder, so the variables exposed by the shared
template namespaces are discoverable. agent.input_documents is workflow-only
(injected from run state) and stays out of the agent builder, which would
render it empty.
Comment thread application/alembic/versions/0025_artifacts.py Fixed
Comment thread tests/agents/tools/test_artifact_generator_unit.py Dismissed
Comment thread application/alembic/versions/0025_artifacts.py Dismissed
Comment thread application/alembic/versions/0025_artifacts.py Dismissed
Comment thread application/alembic/versions/0025_artifacts.py Dismissed

@pabik pabik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants