Skip to content

Add direct Jupyter kernel bridge - #1280

Open
jlewi wants to merge 8 commits into
runmedev:mainfrom
jlewi:dev/jlewi/jupyter
Open

jlewi wants to merge 8 commits into
runmedev:mainfrom
jlewi:dev/jlewi/jupyter

Conversation

@jlewi

@jlewi jlewi commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a Runme-owned kernel manager and a direct Go-to-kernel ZeroMQ bridge
  • replace the Jupyter Server registry and proxy routes with runner-owned /v1/jupyter/kernels lifecycle and channels routes
  • let an authorized client provide the complete kernelspec-style argv used to start a kernel, including a Python virtual-environment executable
  • generate and own the Jupyter connection file inside Runme, substitute its path for exactly one {connection_file} placeholder, and launch without a shell
  • protect kernel lifecycle and channels routes with RunnerUserRole
  • support Jupyter JSON channels messages while explicitly rejecting binary buffers and the v1.kernel.websocket.jupyter.org binary subprotocol
  • instrument kernel lifecycle and bridge health with OpenTelemetry instruments and bounded attributes

Why

The old model depended on a separately managed Jupyter Server, token discovery, and an HTTP/WebSocket proxy. Runme now owns the kernel process and bridges the browser channels WebSocket directly to the kernel's loopback ZeroMQ sockets, so the server registry and proxy are unnecessary.

Kernel commands are selected by the client instead of being fixed in Runme server configuration. This lets a user start ipykernel from a particular Python or virtual environment without reconfiguring or restarting the runner.

Design document

The architecture, protocol decisions, threat model, rollout constraints, and implementation goal are documented in the direct WebSocket-to-kernel Runme notebook.

Related work and issues

API

POST   /v1/jupyter/kernels
GET    /v1/jupyter/kernels
GET    /v1/jupyter/kernels/{kernel_id}
DELETE /v1/jupyter/kernels/{kernel_id}
POST   /v1/jupyter/kernels/{kernel_id}/interrupt
POST   /v1/jupyter/kernels/{kernel_id}/restart
WS     /v1/jupyter/kernels/{kernel_id}/channels

The legacy /v1/jupyter/servers/... routes are removed.

The start request contains a display name and the complete argument vector:

{
  "name": "project-venv",
  "argv": [
    "/workspace/.venv/bin/python",
    "-m",
    "ipykernel_launcher",
    "-f",
    "{connection_file}"
  ]
}

argv[0] is resolved on the runner host. Runme validates the argument count and size, rejects NUL bytes, requires exactly one {connection_file} placeholder outside the executable, and invokes the process directly without shell expansion. Restart reuses the original launch specification.

Decisions that deserve careful review

These are the API, security, and compatibility choices that will become expensive to change after clients depend on them:

  1. Authorization is also process-launch authority. RunnerUserRole permits the caller to supply an arbitrary executable and arguments that run as the runner's OS user. This is intentional; it must not be exposed to a weaker role without introducing a separate policy or allowlist.
  2. The client owns argv; Runme owns connectivity. The client selects the environment and kernel command. Runme selects ports, creates the HMAC key and connection file, controls its path and permissions, and only substitutes {connection_file}.
  3. No shell is involved. Arguments are passed directly to exec.Command. Shell strings, interpolation, pipelines, and environment-variable expansion are not part of the API.
  4. Kernel identity is server-generated and opaque. A client-supplied name is only a display name. Lifecycle and channels operations address the kernel by generated kernel_id, allowing multiple kernels with the same name.
  5. Restart preserves identity and launch intent. Restart reuses the stored launch specification and keeps the same kernel ID while replacing the process generation.
  6. JSON channels are the compatibility boundary for this PR. Arbitrary binary buffers and widget parity are not claimed. The binary Jupyter WebSocket subprotocol is rejected explicitly rather than partially implemented.
  7. The transport boundary is internal. Browser clients speak the standard channels WebSocket shape; the Runme-owned bridge handles Jupyter multipart framing, HMAC verification, routing identities, heartbeat, backpressure, and ZeroMQ sockets.

Reviewer guide

Validation

  • SHELL=/bin/bash ./runme run check
  • focused Jupyter kernel-manager, protocol, bridge, server-route, and configuration tests
  • direct authenticated Runme Web browser verification: list, start with a client-supplied virtual-environment command, restart, execute against, and stop runner-owned kernels
  • comparison against the previous PR head for the locally flaky reconnect soak

Current limitations

  • binary WebSocket buffers and widgets that depend on them are not supported yet
  • Windows uses the isolated fallback process-control implementation
  • the reconnect soak can time out under constrained local macOS runs; the same timeout reproduces at the previous PR head and is not introduced by the OpenTelemetry conversion

Comment thread pkg/agent/config/config.go Outdated
// ParserService starts the Runme parser service if true otherwise it doesn't start the parser service.
ParserService bool `json:"parserService" yaml:"parserService"`

// Jupyter configures Runme-managed kernels. Runme always exposes the direct

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should we just allow the client to send the start command?

Comment thread pkg/agent/jupyter/kernel_manager.go Resolved
@jlewi
jlewi marked this pull request as ready for review August 14, 2026 12:56
jlewi added a commit to runmedev/web that referenced this pull request Aug 14, 2026
## Summary

- migrate Runme Web from the Jupyter Server registry/proxy API to
runner-owned `/v1/jupyter/kernels` routes
- attach the current OIDC ID token to kernel lifecycle requests and
authenticated channels WebSockets
- let the client send a Jupyter kernelspec-style `argv` when starting a
kernel, including a virtual-environment Python path
- validate kernel commands in the start UI as JSON argv with exactly one
`{connection_file}` placeholder
- list and select kernels by `(runner name, kernel ID)`, while
preserving optional browser-local display names
- add a **Kernels** column to Notebook Runner Status and a runner-scoped
management tab with refresh, start, restart, and stop controls
- document kernel identity, client-supplied commands, `RunnerUserRole`
authorization, lifecycle behavior, direct routes, and current
JSON-channels limitations

## Why

The previous web integration assumed a separately managed Jupyter Server
and proxy routes. Runme now owns kernel processes and bridges browser
WebSockets directly to kernel ZeroMQ channels, so the web app uses the
direct lifecycle API and authenticates those requests itself.

Kernel commands belong to the client rather than runner configuration. A
user can select a virtual environment by changing the executable in the
launch argv, for example `/workspace/.venv/bin/python`, without
restarting or reconfiguring the Runme server.

## Server dependency

Depends on draft server PR
[runmedev/runme#1280](runmedev/runme#1280),
which replaces the Jupyter Server proxy with the direct kernel API and
authorizes client-supplied launches with `RunnerUserRole`.

## User impact

- Jupyter cells can select live kernels from configured runners without
a Jupyter Server registry.
- Users can launch `ipykernel` from a specific Python virtual
environment on the runner host.
- Multiple kernels are distinguished by runner and kernel ID; optional
display names improve readability.
- Users can manage kernels from Notebook Runner Status and share one
kernel across cells when state should persist.

## Validation

- `runme run build test`
- focused Vitest coverage for the direct Jupyter manager and
kernel-management UI
- `pnpm -C app run check:documentation`
- live authenticated browser verification from the original
direct-kernel rollout: list, start, restart, execute against, and stop
runner-owned kernels

---------

Signed-off-by: Jeremy lewi <jeremy@lewi.us>
@jlewi
jlewi force-pushed the dev/jlewi/jupyter branch from d9e1aa3 to 6f66c8f Compare August 30, 2026 00:22
jlewi added 6 commits October 22, 2026 07:41
Signed-off-by: Jeremy Lewi <jeremy@lewi.us>
Signed-off-by: Jeremy lewi <jeremy@lewi.us>
Signed-off-by: Jeremy lewi <jeremy@lewi.us>
Signed-off-by: Jeremy lewi <jeremy@lewi.us>
Signed-off-by: Jeremy lewi <jeremy@lewi.us>
Signed-off-by: Jeremy lewi <jeremy@lewi.us>
@jlewi
jlewi force-pushed the dev/jlewi/jupyter branch from c67dba5 to 7c28608 Compare October 22, 2026 14:52
Signed-off-by: Jeremy lewi <jeremy@lewi.us>
@jlewi
jlewi force-pushed the dev/jlewi/jupyter branch from 7c28608 to 5920278 Compare October 22, 2026 14:57
@jlewi jlewi closed this Oct 22, 2026
@jlewi jlewi reopened this Oct 22, 2026
@jlewi
jlewi requested a review from sourishkrout October 23, 2026 00:48
@sourishkrout

Copy link
Copy Markdown
Contributor

The Jupyter kernel API runs commands chosen by the client and is always on. It needs a switch, off by default and dependent on runnerService, so operators choose whether to allow it. Mind adding a assistantServer.jupyterService or something along those lines, please, @jlewi?

Signed-off-by: Jeremy lewi <jeremy@lewi.us>
@jlewi

jlewi commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

@sourishkrout Done in 194b88c. I added assistantServer.jupyterService, which is disabled by default. Enabling it requires assistantServer.runnerService: true; invalid configurations fail validation/server startup. When disabled, Runme does not initialize the kernel manager or register /v1/jupyter/kernels. The existing RunnerUserRole authorization remains in place when the service is enabled. I also added config/route tests and updated the agent documentation.

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