feat(ide): add marimo notebook IDE support#457
Conversation
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.
✅ Deploy Preview for devsydev canceled.
|
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis 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. ChangesMarimo IDE Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ 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. Comment |
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
desktop/src/renderer/public/icons/ides/marimo.svgis excluded by!**/*.svgdesktop/src/renderer/public/icons/ides/marimo_dark.svgis excluded by!**/*.svg
📒 Files selected for processing (9)
cmd/agent/container/setup.godesktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.sveltedesktop/src/renderer/src/pages/WorkspaceDetailPage.sveltepkg/config/ide.gopkg/ide/ideparse/parse.gopkg/ide/jupyter/jupyter.gopkg/ide/marimo/marimo.gopkg/ide/opener/opener.gopkg/ide/types.go
| 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:]...) |
There was a problem hiding this comment.
🧩 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 80Repository: 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 80Repository: 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 120Repository: 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 80Repository: 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 200Repository: 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 120Repository: 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 200Repository: 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 80Repository: 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).
…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.
Summary
pip3 install marimoorpip install marimo) and launchesmarimo 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.marimo.svg/marimo_dark.svgicon 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 →Startbackground-launches the headless server).pkg/config/ide.go—IDEMarimo IDE = "marimo".pkg/ide/ideparse/parse.go—AllowedIDEsentry inIDEGroupOther.cmd/agent/container/setup.go—installIDEdispatcher case.pkg/ide/opener/opener.go—openMarimoBrowserusingbrowserIDESpecwithhttp://localhost:<port>/target URL.pkg/ide/types.go— marimo flagged inReusesAuthSock(browser IDE).WorkspaceWizard.svelte+WorkspaceDetailPage.svelteIDE lists updated.Auth model
Auth disabled with
--no-tokenon the same rationale as Jupyter's--NotebookApp.token='': the SSH tunnel is the security boundary. marimo's CLI also has no env-var equivalent ofJUPYTER_TOKEN, so passing a token would require captured-secret plumbing not present in the analogous Jupyter integration.Out of scope / follow-ups
pip install marimofail with the upstream resolver error if the base image ships an older Python — same behavior as Jupyter.Summary by CodeRabbit
Release Notes