Skip to content

feat(ide): add marimo notebook IDE support#457

Merged
skevetter merged 5 commits into
mainfrom
add-marimo-ide
May 28, 2026
Merged

feat(ide): add marimo notebook IDE support#457
skevetter merged 5 commits into
mainfrom
add-marimo-ide

Conversation

@skevetter

@skevetter skevetter commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds marimo as a selectable browser-based IDE, sitting next to Jupyter Notebook in the "Other" group of the workspace wizard.
  • The agent installs marimo via pip (pip3 install marimo or pip install marimo) and launches marimo edit --headless --host 0.0.0.0 --port 10800 --no-token <workspace>, exposed to the user through the same SSH-tunneled browser-opener flow as Jupyter/RStudio.
  • Restores marimo.svg / marimo_dark.svg icon assets that were dropped in fix(desktop): swap IDE icons to dark variants in dark mode #455 (they're now actually used).

Architecture notes

Closely mirrors pkg/ide/jupyter:

  • pkg/ide/marimo/marimo.go — new IDE package (Install → pip install → Start background-launches the headless server).
  • pkg/config/ide.goIDEMarimo IDE = "marimo".
  • pkg/ide/ideparse/parse.goAllowedIDEs entry in IDEGroupOther.
  • cmd/agent/container/setup.goinstallIDE dispatcher case.
  • pkg/ide/opener/opener.goopenMarimoBrowser using browserIDESpec with http://localhost:<port>/ target URL.
  • pkg/ide/types.go — marimo flagged in ReusesAuthSock (browser IDE).
  • Frontend: WorkspaceWizard.svelte + WorkspaceDetailPage.svelte IDE lists updated.

Auth model

Auth disabled with --no-token on the same rationale as Jupyter's --NotebookApp.token='': the SSH tunnel is the security boundary. marimo's CLI also has no env-var equivalent of JUPYTER_TOKEN, so passing a token would require captured-secret plumbing not present in the analogous Jupyter integration.

Out of scope / follow-ups

  • marimo requires Python ≥ 3.10. The installer just lets pip install marimo fail with the upstream resolver error if the base image ships an older Python — same behavior as Jupyter.
  • No automated tests added (Jupyter/RStudio packages don't have them either).

Summary by CodeRabbit

Release Notes

  • New Features
    • Marimo IDE integration now available. Users can select marimo as an IDE option when setting up or configuring their workspaces. The application automatically handles marimo installation, manages background server instances, provides browser-based access for interactive notebook development, supports secure port-forwarding configurations, and integrates with SSH authentication for remote development environments.

Review Change Stack

Adds marimo (https://marimo.io) as a selectable browser-based IDE,
modeled on the existing Jupyter Notebook integration. The agent
installs marimo via pip and launches `marimo edit --headless --host
0.0.0.0 --port 10800 --no-token <workspace>`, exposed to the user
through the same SSH-tunneled browser-opener flow as Jupyter/RStudio.

Auth is disabled (--no-token) on the same rationale as Jupyter's
--NotebookApp.token='': the SSH tunnel is the security boundary.
@netlify

netlify Bot commented May 28, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit 0b45d55
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a18a982f0e12d0008fa7f66

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@skevetter, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 2 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c22cc18a-2143-40ef-b1ea-a3214b1d46f1

📥 Commits

Reviewing files that changed from the base of the PR and between ec3f517 and 0b45d55.

📒 Files selected for processing (2)
  • desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
  • pkg/ide/types.go
📝 Walkthrough

Walkthrough

This PR adds Marimo notebook IDE as a supported IDE option throughout the Devsy system. It defines the Marimo server implementation, registers it in IDE discovery, refactors Jupyter to use shared Python installation logic, wires browser launching and container setup, and exposes the option in desktop UI.

Changes

Marimo IDE Integration

Layer / File(s) Summary
IDE Config and Type Support
pkg/config/ide.go, pkg/ide/types.go
Introduces IDEMarimo constant and extends type utilities to handle Marimo's auth socket reuse and Python package installation support.
Marimo Server Implementation
pkg/ide/marimo/marimo.go
Implements MarimoServer with Install() and Start() methods that manage Python package installation, workspace folder binding, user/port configuration, and headless marimo edit server startup.
IDE Registration
pkg/ide/ideparse/parse.go
Registers Marimo in the AllowedIDEs registry with display name, icon URLs, port defaults, and bind address options for discovery in IDE selection flows.
Jupyter Installation Refactoring
pkg/ide/jupyter/jupyter.go
Refactors JupyterNotebookServer to use PythonInstallCommand utility for detecting and running Python package installation, extracting common logic while preserving the install/start flow.
Browser Launcher Wiring
pkg/ide/opener/opener.go
Adds openMarimoBrowser function to launch Marimo in the browser via detached tunnel, using Marimo options for bind address and port configuration.
Container Setup Integration
cmd/agent/container/setup.go
Extends container setup to instantiate and install MarimoServer when IDEMarimo is selected, following the pattern for other supported IDEs.
Desktop UI Option Addition
desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte, desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte
Adds Marimo to IDE selection options in workspace wizard and detail page, enabling users to select Marimo during workspace configuration.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • devsy-org/devsy#455: Refactors IDE icon rendering in the wizard to use dark mode variants—the new Marimo icon entry would use the added _dark.svg icon path.
  • devsy-org/devsy#450: Implements devsy ide set command that validates IDE choices against the AllowedIDEs registry, which now includes the Marimo entry added by this PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(ide): add marimo notebook IDE support' clearly and accurately describes the main change in the changeset—adding marimo as a new selectable IDE.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

skevetter added 3 commits May 28, 2026 11:58
Extracts a shared ide.PythonInstallCommand helper that returns the
install command in this preference order:
  uv pip install --system <pkg>
  pip3 install <pkg>
  pip install <pkg>

Used by both the jupyter and marimo installers. uv is dramatically
faster than pip and is increasingly common in dev containers; falling
back to pip preserves behavior on images that don't ship uv.

Also reorders jupyter.go to satisfy funcorder and wraps its two
exec.Command calls with the same gosec nolint comment used in
marimo.go, since touching the file re-triggered the diff-only
pre-commit lint.
marimo notebooks commonly embed subprocess apps (FastAPI, Streamlit,
etc.) on additional ports. Expose the same FORWARD_PORTS option that
code-server and openvscode use so the SSH tunnel auto-forwards any
in-container listeners through to the host. Default 'true', matching
the VS Code-derived browser IDEs.

jupyter and rstudio still omit this — their precedent stands.
@skevetter
skevetter marked this pull request as ready for review May 28, 2026 20:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte`:
- Line 96: The dark-mode icon mapping is missing "marimo" so the UI never uses
marimo_dark.svg; update the IDE_ICON_DARK_VARIANTS constant to include "marimo"
(matching the new entry added to the workspace options list) so that the
component uses the corresponding dark variant; locate IDE_ICON_DARK_VARIANTS in
the same module or imported constants and add "marimo" to that set/array.

In `@pkg/ide/marimo/marimo.go`:
- Around line 63-75: The current code builds a shell command string (runCommand)
embedding o.workspaceFolder inside single quotes and then calls
exec.Command(args[0], args[1:]...), which is vulnerable if workspaceFolder
contains quotes or shell metacharacters; update the launch so you do not need
sh/su -c with an embedded quoted string: build the command and args directly and
pass the workspace path as its own argument (e.g., instead of "marimo edit ...
'%s'" create []string{"marimo","edit","--headless","--host","0.0.0.0","--port",
strconv.Itoa(DefaultServerPort),"--no-token", o.workspaceFolder} and call
exec.Command with those elements), and if you must use su/sh -c when o.userName
is set, either avoid su -c by using a user-switching mechanism that accepts argv
(or use sudo -u with argument vector) or safely escape single quotes in
o.workspaceFolder by replacing each ' with '\'' before embedding; change
runCommand construction and the exec.Command invocation accordingly (refer to
runCommand, cmd := exec.Command(...), o.workspaceFolder, and o.userName).

In `@pkg/ide/types.go`:
- Around line 17-25: The installer selection currently prefers the "uv pip
install --system %s" path which can fail for non-root users; update the
selection logic in the block using command.ExistsForUser to avoid
unconditionally returning the "--system" command: first prefer a per-user or
virtualenv-friendly install (e.g., "python3 -m pip install --user" or a
venv-based installer) when the user is non-root, only return the "uv pip install
--system %s" form if the caller is running as root or the target user has
writable system site-packages (detect via UID or a writable check), otherwise
fall back to "pip3 install %s" / "pip install %s"; adjust the branches that
return fmt.Sprintf("uv pip install --system %s", pkg), fmt.Sprintf("pip3 install
%s", pkg), and fmt.Sprintf("pip install %s", pkg) accordingly so non-root
installs use the safe per-user/venv command.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c78de3d6-d218-469d-94e8-1563f6a82624

📥 Commits

Reviewing files that changed from the base of the PR and between eccf0a6 and ec3f517.

⛔ Files ignored due to path filters (2)
  • desktop/src/renderer/public/icons/ides/marimo.svg is excluded by !**/*.svg
  • desktop/src/renderer/public/icons/ides/marimo_dark.svg is excluded by !**/*.svg
📒 Files selected for processing (9)
  • cmd/agent/container/setup.go
  • desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte
  • desktop/src/renderer/src/pages/WorkspaceDetailPage.svelte
  • pkg/config/ide.go
  • pkg/ide/ideparse/parse.go
  • pkg/ide/jupyter/jupyter.go
  • pkg/ide/marimo/marimo.go
  • pkg/ide/opener/opener.go
  • pkg/ide/types.go

Comment thread pkg/ide/marimo/marimo.go
Comment on lines +63 to +75
runCommand := fmt.Sprintf(
"marimo edit --headless --host 0.0.0.0 --port %s --no-token '%s'",
strconv.Itoa(DefaultServerPort),
o.workspaceFolder,
)
args := []string{}
if o.userName != "" {
args = append(args, "su", o.userName, "-w", "SSH_AUTH_SOCK", "-l", "-c", runCommand)
} else {
args = append(args, "sh", "-l", "-c", runCommand)
}
//nolint:gosec // args are constructed from trusted inputs
cmd := exec.Command(args[0], args[1:]...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect where workspace folder values originate and whether they are sanitized.
rg -n -C3 '\bworkspaceFolder\b|NewMarimoServer\(|NewJupyterNotebookServer\(|-c", runCommand|runCommand :='

Repository: devsy-org/devsy

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find GetWorkspaceDir implementation(s)
rg -n --no-heading "func GetWorkspaceDir\\b|GetWorkspaceDir\\(" pkg | head -n 50

# 2) Locate where --workspace-folder is parsed/validated and how it becomes workspaceFolder
rg -n --no-heading --glob='*.go' "\\-\\-workspace-folder|workspace-folder" cmd desktop pkg docs | head -n 80

Repository: devsy-org/devsy

Length of output: 4246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show GetWorkspaceDir implementation
sed -n '1,220p' pkg/provider/dir.go
echo "----"
sed -n '220,520p' pkg/provider/dir.go

# Show how `--workspace-folder` is resolved/validated in CLI flow
rg -n --no-heading "WorkspaceFolder" cmd/up cmd/ssh.go cmd/readconfiguration.go cmd/exec.go pkg/devcontainer pkg/workspace pkg/client pkg/provider | head -n 80
echo "----"
rg -n --no-heading "resolve.*WorkspaceFolder|resolve.*workspaceFolder|WorkspaceFolder.*Trim|WorkspaceFolder.*Abs|workspaceFolder.*Abs|workspaceFolder.*Clean" cmd pkg | head -n 80

Repository: devsy-org/devsy

Length of output: 18018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect full implementations around Start() for marimo/jupyter
sed -n '1,200p' pkg/ide/marimo/marimo.go
echo "----"
sed -n '1,220p' pkg/ide/jupyter/jupyter.go

# 2) Find where NewMarimoServer/NewJupyterNotebookServer get their workspaceFolder
rg -n --no-heading "NewMarimoServer\\b|NewJupyterNotebookServer\\b" pkg cmd desktop | head -n 80

# 3) Look for any validation/sanitization of --workspace-folder / workspaceFolder (especially quote handling)
rg -n --no-heading --glob='*.go' "(--workspace-folder|workspace-folder|workspaceFolder)" cmd pkg | rg -n "regex|sanit|escape|quote|'\"\\''|Contains\\(|Invalid|error|forbid|disallow|TrimSpace|filepath\\.Abs|filepath\\.Clean" | head -n 120

Repository: devsy-org/devsy

Length of output: 5670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how workspaceFolder is computed/passed into Marimo/Jupyter
sed -n '560,700p' cmd/agent/container/setup.go

# Show the exact call sites for both servers
rg -n "jupyter\.NewJupyterNotebookServer|marimo\.NewMarimoServer" cmd/agent/container/setup.go

# Search for any shell-escaping / quote-handling helpers used for workspaceFolder (or for general shell -c strings)
rg -n --glob='*.go' "(shellquote|ShellEscape|shell escape|shell-escape|QuoteShell|strconv\\.Quote|printf %q|EscapeShell|shlex|EscapeArg|ReplaceAll\\(.*workspaceFolder.*'\\''|Invalid.*workspaceFolder)" pkg cmd | head -n 80

Repository: devsy-org/devsy

Length of output: 5535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect how setupInfo.SubstitutionContext.ContainerWorkspaceFolder is produced/used (closest to marimo/jupyter call sites)
sed -n '560,690p' cmd/agent/container/setup.go

# 2) Inspect CLI handling of --workspace-folder: where filepath.Abs/clean/validation occurs
sed -n '120,230p' cmd/readconfiguration.go

# 3) Search for any escaping/sanitization specific to workspaceFolder or substitution context (quote/metachar filtering)
rg -n --no-heading --glob='*.go' "(ContainerWorkspaceFolder|LocalWorkspaceFolder|workspaceFolder)" pkg/devcontainer pkg/cmd cmd | head -n 200

# 4) Search for disallowed quote characters or shell escaping helpers
rg -n --no-heading --glob='*.go' "(shellescape|shell-escape|EscapeShell|QuoteShell|strconv\\.Quote|printf %q|ReplaceAll.*\\''|Invalid.*workspace|forbid.*workspace|disallow.*workspace|regexp.*workspace)" pkg cmd | head -n 200

Repository: devsy-org/devsy

Length of output: 21730


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) How workspaceFolder is chosen for ContainerWorkspaceFolder (host->container mapping)
sed -n '220,330p' cmd/up/up.go
echo "----"

# 2) How devcontainer substitution computes ContainerWorkspaceFolder / resolves ${...}
sed -n '1,260p' pkg/devcontainer/config/substitute.go
echo "----"

# 3) Any validation around WorkspaceFolder / ContainerWorkspaceFolder mismatch
sed -n '1,260p' pkg/devcontainer/config.go
echo "----"

# 4) Ensure we didn't miss any workspace-folder sanitization utilities
rg -n --no-heading --glob='*.go' "(saniti|escape|quote|shell|injection|metachar|ReplaceAll\\(|strings\\.ReplaceAll|shellescape|ShellEscape|EscapeShell|Invalid.*workspace|disallow.*workspace)" pkg/devcontainer cmd | head -n 120

Repository: devsy-org/devsy

Length of output: 30840


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any workspace-folder validation that rejects quotes or shell metacharacters
rg -n --no-heading --glob='*.go' "(workspace-folder|WorkspaceFolder|ContainerWorkspaceFolder)" cmd pkg | rg -n "forbid|invalid|reject|disallow|sanitize|escape|quote|\\'|metachar|shell|treat.*quote|\"'\"" | head -n 200

# Also check for any generic validation helpers around paths
rg -n --no-heading --glob='*.go' "func (.*sanitize|.*escape|.*validate).*path|sanitize.*path|escape.*path|validate.*path" pkg cmd | head -n 200

Repository: devsy-org/devsy

Length of output: 266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect getWorkspace implementation used to compute ContainerWorkspaceFolder
rg -n --no-heading "func getWorkspace\\b" pkg/devcontainer/setup.go pkg/devcontainer/*.go cmd pkg | head -n 20

# Read around the first match
file="$(rg -l --no-heading "func getWorkspace\\b" pkg/devcontainer/setup.go pkg/devcontainer/*.go 2>/dev/null | head -n 1)"
if [ -n "${file}" ]; then
  echo "---- file: $file ----"
  rg -n "func getWorkspace\\b" "$file"
  # Print a reasonable window around the function
  line="$(rg -n "func getWorkspace\\b" "$file" | head -n 1 | cut -d: -f1)"
  start=$((line-40))
  end=$((line+180))
  sed -n "${start},${end}p" "$file"
fi

# Also inspect if any escaping/sanitization is applied to ContainerWorkspaceFolder in host/container setup paths
rg -n --no-heading --glob='*.go' "ContainerWorkspaceFolder.*(shellescape|Quote|escape|sanitize|replace|ReplaceAll|gosec)" cmd pkg | head -n 80

Repository: devsy-org/devsy

Length of output: 2339


Harden shell -c command construction for workspaceFolder (Marimo/Jupyter)

pkg/ide/marimo/marimo.go (Start) and pkg/ide/jupyter/jupyter.go (Start) interpolate workspaceFolder into a single-quoted sh/su -c string (... '%s' / ... '%s'). ContainerWorkspaceFolder ultimately comes from --workspace-folder / devcontainer substitution and is only normalized via filepath.Abs + os.Stat (no shell metacharacter/quote escaping), so a path containing ' (or other shell metacharacters) can break quoting and become shell-injection prone.

Fix by avoiding su/sh -c string-building when possible, or by applying robust shell escaping to the workspace path (e.g., single-quote escaping like '\'') before embedding it; apply the same fix to both Marimo and Jupyter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ide/marimo/marimo.go` around lines 63 - 75, The current code builds a
shell command string (runCommand) embedding o.workspaceFolder inside single
quotes and then calls exec.Command(args[0], args[1:]...), which is vulnerable if
workspaceFolder contains quotes or shell metacharacters; update the launch so
you do not need sh/su -c with an embedded quoted string: build the command and
args directly and pass the workspace path as its own argument (e.g., instead of
"marimo edit ... '%s'" create
[]string{"marimo","edit","--headless","--host","0.0.0.0","--port",
strconv.Itoa(DefaultServerPort),"--no-token", o.workspaceFolder} and call
exec.Command with those elements), and if you must use su/sh -c when o.userName
is set, either avoid su -c by using a user-switching mechanism that accepts argv
(or use sudo -u with argument vector) or safely escape single quotes in
o.workspaceFolder by replacing each ' with '\'' before embedding; change
runCommand construction and the exec.Command invocation accordingly (refer to
runCommand, cmd := exec.Command(...), o.workspaceFolder, and o.userName).

Comment thread pkg/ide/types.go
…tem'

uv tool install is the uv-native path for CLI tools: creates an
isolated env per tool, drops a shim in ~/.local/bin, doesn't touch
system Python, and sidesteps PEP 668 externally-managed-environment
errors. The pip3/pip fallbacks keep existing behavior.

Also adds 'marimo' to IDE_ICON_DARK_VARIANTS in the wizard so the
restored marimo_dark.svg actually loads in dark mode.
@skevetter
skevetter merged commit 5893d16 into main May 28, 2026
59 checks passed
@skevetter
skevetter deleted the add-marimo-ide branch May 28, 2026 21:27
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.

1 participant