Skip to main content

๐Ÿš€ JupyterPilot

Enterprise-grade AI coding partner & secure multi-team notebook isolation for JupyterHub

Python JupyterHub License: MIT Tests AWS EC2

One Hub. Many teams. Every user isolated. AI in every cell.


๐Ÿ“– Table of Contents

Section What you'll learn
๐Ÿ—บ๏ธ What is JupyterPilot? High-level purpose and design philosophy
๐Ÿ—๏ธ Architecture: 1 Hub โ†’ Many Worker EC2s How a single hub routes 100s of users across multiple EC2s
๐Ÿ‘ฅ Use Case 1 โ€” Multi-Team EC2 Routing Group A on EC2-1, Group B on EC2-2, automatically
๐Ÿ” Use Case 2 โ€” Per-User Isolation & RBAC Each user gets their own OS account, process & resource cap
๐Ÿค– Use Case 3 โ€” AI Magic Commands %do, %fix, %review, %rework inside notebooks
๐Ÿ”‘ Use Case 4 โ€” Vault Secret Injection Per-user API keys injected at spawn, zero disk I/O
๐Ÿ“Š Use Case 5 โ€” Live Monitoring Dashboard Real-time CPU/RAM/network for every worker EC2
๐Ÿ”„ Use Case 6 โ€” Crash Recovery Hub restarts do not lose running user sessions
๐Ÿ› ๏ธ Installation Guide Full step-by-step Hub & Worker EC2 setup
๐Ÿงช Running Tests Full test suite, no real SSH or AWS needed
๐Ÿ—บ๏ธ Roadmap What is built and what is next

๐Ÿ—บ๏ธ What is JupyterPilot?

JupyterPilot solves two problems that every data team running JupyterHub eventually hits:

Problem 1 โ€” "How do we run notebooks for 100+ users without one user crashing everyone else?"

JupyterPilot's SSH spawner teleports each user's notebook server onto an isolated EC2 worker, wraps it in cgroup resource limits, and tracks all sessions in SQLite so nothing is lost if the Hub reboots.

Problem 2 โ€” "How do we get AI coding assistance inside notebooks without a brittle plugin?"

JupyterPilot's IPython extension adds four magic commands (%do, %fix, %review, %rework) that speak to any LLM backend โ€” local Ollama, OpenAI, Claude, Gemini, or an MCP server โ€” and inject clean Python directly into the next cell.


๐Ÿ—๏ธ Architecture: 1 Hub โ†’ Many Worker EC2s

This is the core deployment model JupyterPilot is built for.

                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                        โ”‚          Hub EC2  (always on)             โ”‚
                        โ”‚                                           โ”‚
                        โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
                        โ”‚  โ”‚ JupyterHub  โ”‚   โ”‚  SQLite DB        โ”‚  โ”‚
                        โ”‚  โ”‚ + Google    โ”‚โ”€โ”€โ–ถโ”‚  team_mappings    โ”‚  โ”‚
                        โ”‚  โ”‚   OAuth     โ”‚   โ”‚  user_sessions    โ”‚  โ”‚
                        โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
                        โ”‚         โ”‚ CustomSpawner                   โ”‚
                        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€-โ”€โ”˜
                                  โ”‚  SSH (Paramiko, per spawn)
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚                                    โ”‚
                    โ–ผ                                    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Worker EC2 โ€” group_a   โ”‚          โ”‚   Worker EC2 โ€” group_b   โ”‚
โ”‚   IP: 10.0.1.10          โ”‚          โ”‚   IP: 10.0.1.20          โ”‚
โ”‚                          โ”‚          โ”‚                          โ”‚
โ”‚  alice  โ†’ port 8901      โ”‚          โ”‚  carol  โ†’ port 8901      โ”‚
โ”‚  bob    โ†’ port 8902      โ”‚          โ”‚  dave   โ†’ port 8902      โ”‚
โ”‚  ...                     โ”‚          โ”‚  ...                     โ”‚
โ”‚  (up to 50 users)        โ”‚          โ”‚  (up to 50 users)        โ”‚
โ”‚                          โ”‚          โ”‚                          โ”‚
โ”‚  metrics_agent.py โ”€โ”€WSโ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ–ถ /monitoring/ws       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key properties:

  • Each group maps to exactly one worker EC2 โ€” all group members share that machine
  • Each user on a worker gets their own OS account, process, and TCP port โ€” completely isolated
  • The Hub uses SQLite to remember who is on which EC2 โ€” survives hub restarts
  • Worker EC2s send live metrics back to the hub's monitoring dashboard over WebSocket
  • Adding a new worker EC2 requires zero hub restarts โ€” just insert a row in SQLite

๐Ÿ‘ฅ Use Case 1 โ€” Multi-Team EC2 Routing

Goal: Group A's 50 users run on EC2-1. Group B's 50 users run on EC2-2. The Hub routes automatically.

โ†’ Back to Table of Contents

How it works

When a user logs in, the spawner:

  1. Reads the user's JupyterHub group (e.g., group_a)
  2. Looks up group_a in the SQLite team_mappings table โ†’ gets 10.0.1.10
  3. SSHes into 10.0.1.10 as that user
  4. Launches jupyterhub-singleuser on a free port
  5. Stores the session in SQLite (username, vm_ip, port, group_name)

The Hub's proxy then routes https://hub.yourco.com/user/alice/ โ†’ 10.0.1.10:8901.

Setup

Step 1 โ€” Define your groups in user_mapping.json:

{
  "group_a": {
    "server_ip":      "10.0.1.10",
    "server_ssh_key": "/etc/jupyterhub/keys/group_a.pem",
    "admin_ssh_user": "ubuntu"
  },
  "group_b": {
    "server_ip":      "10.0.1.20",
    "server_ssh_key": "/etc/jupyterhub/keys/group_b.pem",
    "admin_ssh_user": "ubuntu"
  },
  "group_c": {
    "server_ip":      "10.0.1.30",
    "server_ssh_key": "/etc/jupyterhub/keys/group_c.pem",
    "admin_ssh_user": "ubuntu"
  }
}

Step 2 โ€” Seed the routing table into SQLite (run once on deploy):

python -m jupyterpilot.seed_sqlite \
    --db      /var/lib/jupyterhub/jupyterpilot_state.db \
    --mapping /etc/jupyterhub/user_mapping.json
# Output:
#   โœ“ group_a    โ†’ 10.0.1.10
#   โœ“ group_b    โ†’ 10.0.1.20
#   โœ“ group_c    โ†’ 10.0.1.30
# [DONE] Seeded 3 team(s), skipped 0.

Step 3 โ€” Assign users to groups in the JupyterHub Admin Panel:

JupyterHub Admin โ†’ Groups โ†’ group_a โ†’ Add Users: alice, bob, charlie, ...
JupyterHub Admin โ†’ Groups โ†’ group_b โ†’ Add Users: carol, dave, eve, ...

That's it. From this point on:

  • alice logs in โ†’ spawned on 10.0.1.10
  • carol logs in โ†’ spawned on 10.0.1.20
  • Neither can see or affect the other

Adding a new worker EC2 without any downtime

# On the Hub VM โ€” no JupyterHub restart needed:
python3 - <<'EOF'
from jupyterpilot.session_store import SessionStore
store = SessionStore("/var/lib/jupyterhub/jupyterpilot_state.db")
store.set_mapping("group_d", "10.0.1.40", "/etc/jupyterhub/keys/group_d.pem", "ubuntu")
print("group_d is live!")
EOF

โ†’ See full AWS deployment guide


๐Ÿ” Use Case 2 โ€” Per-User Isolation & RBAC

Goal: On a shared EC2, user A's notebook cannot see user B's files, processes, or memory. Admins can manage any session; regular users can only manage their own.

โ†’ Back to Table of Contents

Per-User OS Isolation

Every new user is automatically provisioned on the worker EC2 on their first login (Just-In-Time provisioning). No manual adduser needed:

alice logs in for the first time
  โ†“
spawner calls _provision_user_jit()
  โ†“ SSH as admin (ubuntu) to 10.0.1.10
  โ†“ sudo adduser --disabled-password alice
  โ†“ sudo loginctl enable-linger alice      โ† keeps process alive after SSH closes
  โ†“ sudo mkdir /home/alice/notebook
  โ†“ sudo pip3 install jupyterhub notebook jupyterpilot[ai]
  โ†“ copy Hub SSH key โ†’ /home/alice/.ssh/authorized_keys
  โ†“
spawner SSHes as alice, finds a free port, launches singleuser

After provisioning:

  • alice owns /home/alice/ โ€” Bob cannot read it
  • alice's notebook process runs as OS user alice โ€” Bob's kernel cannot signal it
  • Each process is wrapped in a cgroup scope โ€” memory/CPU cannot bleed across

cgroups v2 Resource Limits

Configure per-user hard limits in hub_settings.json:

{
  "resource_limits": {
    "memory_max": "2G",
    "cpu_quota":  "100%"
  }
}

The spawner automatically wraps the launch command with systemd-run:

# What actually runs on the worker EC2:
systemd-run --user --scope \
  --unit=jupyterhub-alice-1722000000 \
  --property=MemoryMax=2G \
  --property=CPUQuota=100% \
  -- nohup jupyterhub-singleuser --ip=0.0.0.0 --port=8901 ...

If Alice's notebook eats 2.1GB of RAM, the kernel OOM-kills her process โ€” not Bob's, not the hub's.

โš ๏ธ Required on each worker EC2: sudo loginctl enable-linger <username> for every user, otherwise Ubuntu kills the process 7 seconds after the SSH session closes. JIT provisioning handles this automatically.

2-Tier RBAC

Role What they can do How to grant
user Start/stop/poll their own server only Default for all users
admin Start/stop/poll any user's server JupyterHub Admin Panel โ†’ Users โ†’ โ˜‘ Make Admin

No code changes, no restarts โ€” promote a user to admin live via the UI.


๐Ÿค– Use Case 3 โ€” AI Magic Commands

Goal: Data scientists get an AI pair-programmer that understands their notebook context, fixes their errors, and refactors their code โ€” all without leaving Jupyter.

โ†’ Back to Table of Contents

Load the extension

# In any Jupyter cell:
%load_ext jupyterpilot.extension
# โœ… JupyterPilot loaded. Available: %do, %fix, %review, %rework

Or auto-load in every session:

mkdir -p ~/.ipython/profile_default/startup/
echo "%load_ext jupyterpilot.extension" > ~/.ipython/profile_default/startup/00-jupyterpilot.ipy

%do โ€” Generate code from natural language

%do load the CSV at data/sales.csv, parse the date column, and plot monthly revenue

JupyterPilot injects into the next cell:

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('data/sales.csv', parse_dates=['date'])
df['month'] = df['date'].dt.to_period('M')
monthly = df.groupby('month')['revenue'].sum()

monthly.plot(kind='bar', title='Monthly Revenue', figsize=(12, 5))
plt.tight_layout()
plt.show()

Run immediately instead of injecting:

%do --run create a sample dataframe with 100 rows and columns name, age, score

The AI always sees the last 3 cells of your notebook as context โ€” it knows what variables you have already defined.


%fix โ€” Auto-heal the last exception

# You ran this and it crashed:
df.groupby('date')['revenue'].sum().plot()
# KeyError: 'date'
# Just run:
%fix
# ๐Ÿ”ง JupyterPilot โ€” analysing error and generating fix โ€ฆ
# Injects into next cell:
df['date'] = pd.to_datetime(df['date'])
df.groupby('date')['revenue'].sum().plot()

%fix strips all JupyterHub/IPython internal stack frames before sending to the LLM โ€” only your code's error reaches the model.


%review โ€” AST-aware code quality review

%review          # review last cell
%review -n 3     # review last 3 cells
%review --all    # review the entire notebook session
============================================================
๐Ÿ“‹ JupyterPilot Code Review
============================================================

๐Ÿ” Static Analysis (AST):
  - Line 3: bare `except:` โ€” catches all exceptions including KeyboardInterrupt
  - Line 7: `process_data()` missing return type annotation
  - Line 7: argument `df` in `process_data()` missing type hint

๐Ÿค– LLM Review:
  โ€ข Overall: The code works but lacks defensive programming and type safety.
  โ€ข Use `except Exception as e:` instead of bare `except`
  โ€ข Add type hint `df: pd.DataFrame` and return `-> pd.DataFrame`
  โ€ข Consider extracting date parsing into a helper for reusability
  โ€ข No security concerns found
============================================================

%rework โ€” LLM-powered refactoring

%rework add type hints and a docstring
%rework --diff convert this to method chaining   # preview before applying

With --diff:

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
๐Ÿ“ Proposed changes (use %rework without --diff to apply):
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
--- original
+++ reworked
-def process(df):
-    result = df[df['age'] > 18]
-    result = result.groupby('city')['score'].mean()
-    return result
+def process(df: pd.DataFrame) -> pd.Series:
+    """Return mean score by city for adult users."""
+    return (
+        df[df['age'] > 18]
+        .groupby('city')['score']
+        .mean()
+    )
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

LLM Backend Configuration (~/.jupyterpilot/config.json)

{
  "mode": "local",

  "local": {
    "url":     "http://localhost:11434/api/generate",
    "model":   "qwen2.5-coder:7b",
    "timeout": 30
  },

  "cloud": {
    "provider": "openai",
    "model":    "gpt-4o-mini",
    "api_key":  "sk-...",
    "timeout":  30
  },

  "mcp": {
    "server_url": "http://localhost:3000",
    "tools":      ["generate_code"],
    "timeout":    30
  },

  "rate_limit": {
    "requests_per_minute": 20
  }
}
Mode Best for Notes
local Air-gapped teams, low latency Requires Ollama running on the worker EC2
cloud Best quality (GPT-4, Claude, Gemini) Requires API key; costs per token
mcp Custom enterprise tool servers MCP protocol over HTTP

๐Ÿ”‘ Use Case 4 โ€” Vault Secret Injection

Goal: Each user's notebook automatically gets their personal API keys (OpenAI key, database URL, S3 credentials) injected as environment variables at spawn time โ€” no secrets on disk, no manual .env files.

โ†’ Back to Table of Contents

How it works

User spawns โ†’
  pre_spawn_hook() โ†’
    VaultClient.get_user_secrets("alice") โ†’
      GET https://vault.internal/v1/secret/data/jupyterpilot/alice โ†’
        {"OPENAI_API_KEY": "sk-alice-...", "DATABASE_URL": "postgres://..."}
  โ†“
  self.environment.update(secrets)
  โ†“
JupyterHub passes environment to jupyterhub-singleuser at launch
  โ†“
Alice's kernel: os.environ["OPENAI_API_KEY"] == "sk-alice-..."

Alice's keys are never written to disk. Bob's kernel cannot see Alice's environment.

Setup

# On the Hub VM before starting JupyterHub:
export VAULT_ADDR=http://10.0.0.5:8200
export VAULT_TOKEN=your-policy-scoped-token
// hub_settings.json
{
  "vault_enabled": true,
  "vault_secret_path": "secret/jupyterpilot"
}
# Store each user's secrets in Vault:
vault kv put secret/jupyterpilot/alice \
    OPENAI_API_KEY="sk-alice-..." \
    DATABASE_URL="postgres://alice:pass@db.internal/mydb"

vault kv put secret/jupyterpilot/bob \
    OPENAI_API_KEY="sk-bob-..." \
    S3_BUCKET="bob-data-bucket"

Graceful degradation: If Vault is unreachable, the spawn continues uninterrupted โ€” Vault is opt-in. A warning is logged, but no exception is raised.


๐Ÿ“Š Use Case 5 โ€” Live Monitoring Dashboard

Goal: The hub admin can see real-time CPU, memory, disk, and network I/O for every connected worker EC2 โ€” without SSH-ing into anything.

โ†’ Back to Table of Contents

Architecture

Worker EC2 (metrics_agent.py)
  โ†“  WebSocket every 2 seconds
Hub EC2 (AgentWebSocketHandler at /monitoring/ws)
  โ†“  stores latest snapshot per hostname in _WORKER_SNAPSHOTS
  โ†“  broadcasts to all connected browser tabs
Browser (monitoring.html)
  โ†“  dark-mode real-time dashboard with per-worker panels

Start the agent on each worker EC2

python3 /opt/jupyterpilot/jupyterpilot/metrics_agent.py \
    --hub      ws://10.0.0.10:8000/monitoring/ws \
    --interval 2

Or as a systemd service so it survives reboots:

# /etc/systemd/system/jupyterpilot-agent.service
[Unit]
Description=JupyterPilot Metrics Agent
After=network.target

[Service]
ExecStart=/usr/bin/python3 /opt/jupyterpilot/jupyterpilot/metrics_agent.py \
    --hub ws://10.0.0.10:8000/monitoring/ws --interval 2
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now jupyterpilot-agent

Visit http://<HUB-PUBLIC-IP>:8000/monitoring to see the live dashboard.

What you see per worker EC2:

  • Live CPU % (total + per-core) and 1/5/15-min load averages
  • RAM: used / total / percent
  • Disk: used / total / percent (root filesystem)
  • Network: upload/download Mbps (rolling 2-second rate)

Access control:

  • All authenticated JupyterHub users can view the dashboard
  • Regular users see only their own worker EC2's panel
  • Admins see all connected workers

๐Ÿ”„ Use Case 6 โ€” Crash Recovery

Goal: The Hub EC2 reboots at 3am. When it comes back up, every user's running notebook server is automatically detected and re-linked โ€” no user needs to restart their server.

โ†’ Back to Table of Contents

How SQLite makes this safe

Every spawn writes a row to user_sessions:

username vm_ip port pid status group_name
alice 10.0.1.10 8901 4821 running group_a
bob 10.0.1.10 8902 4830 running group_a
carol 10.0.1.20 8901 5012 running group_b

The SQLite DB lives at /var/lib/jupyterhub/jupyterpilot_state.db โ€” outside the Python process. After a Hub restart:

  1. poll() reads vm_ip from SQLite (not from self.ip, which is empty after restart)
  2. SSHes to the worker EC2 and checks if the PID is still alive
  3. If alive โ†’ None returned โ†’ Hub re-attaches the proxy โ†’ user session continues
  4. If dead โ†’ 0 returned โ†’ JupyterHub prompts user to restart their server

The JSON fallback

If SQLite itself is corrupted or the disk is full, the spawner automatically falls back to user_mapping.json for group-to-EC2 routing. A warning is logged, but no exception is raised and no user loses connectivity.


๐Ÿ› ๏ธ Installation Guide

The full installation guide lives in a dedicated document โ€” it covers every step for both the Hub EC2 and each Worker EC2, including SSH key exchange, SQLite seeding, optional Vault secrets, the metrics agent, and a verification checklist.

๐Ÿ“– Read the full Installation Guide โ†’

Quick outline of what it covers:

Step What you are doing
Step 1 โ€” Hub EC2 Setup Install JupyterPilot, configure hub_settings.json, generate SSH key
Step 2 โ€” Worker EC2 Setup Install jupyterhub-singleuser, get_port.py, enable lingering
Step 3 โ€” SSH Key Exchange Paste Hub public key into each worker's authorized_keys
Step 4 โ€” Register Workers Seed user_mapping.json into SQLite
Step 5 โ€” Start JupyterHub Assign users to groups, start hub, switch to production auth
Step 6 โ€” Metrics Agent systemd service for live monitoring on each worker
Step 7 โ€” Vault Secrets Per-user secret injection at spawn
Step 8 โ€” AI Magic Commands Auto-load %do, %fix, %review, %rework
Verification Checklist End-to-end smoke tests
Troubleshooting Common errors and fixes

๐Ÿ“ฆ Repository Structure

JupyterPilot/
โ”œโ”€โ”€ spawner.py                      # SSH spawner โ€” RBAC, cgroups, Vault, crash recovery
โ”œโ”€โ”€ jupyterhub_config.py            # JupyterHub daemon config
โ”œโ”€โ”€ jupyterpilot_extension.py       # Standalone IPython extension entry point
โ”œโ”€โ”€ hub_settings.json               # Network, auth, resource limits & Vault config
โ”œโ”€โ”€ user_mapping.json               # Worker EC2 routing (seed source / SQLite fallback)
โ”‚
โ”œโ”€โ”€ jupyterpilot/                   # Core package
โ”‚   โ”œโ”€โ”€ __init__.py                 #   Version & lazy extension loader
โ”‚   โ”œโ”€โ”€ admin.py                    #   RBACManager โ€” 2-tier role enforcement
โ”‚   โ”œโ”€โ”€ session_store.py            #   SQLite: team_mappings + user_sessions tables
โ”‚   โ”œโ”€โ”€ seed_sqlite.py              #   CLI: bootstrap SQLite from JSON mapping
โ”‚   โ”œโ”€โ”€ vault_client.py             #   HashiCorp Vault KV-v2 client (pure requests)
โ”‚   โ”œโ”€โ”€ env_setup.py                #   AI venv bootstrapper (user-venv isolation)
โ”‚   โ”œโ”€โ”€ extension.py                #   %do / %fix / %review / %rework implementations
โ”‚   โ”œโ”€โ”€ provider.py                 #   LLM provider (Ollama / LiteLLM / MCP + rate limit)
โ”‚   โ”œโ”€โ”€ metrics_agent.py            #   psutil agent โ€” runs on each worker EC2
โ”‚   โ”œโ”€โ”€ monitoring_handler.py       #   Tornado WS + HTTP handlers for live dashboard
โ”‚   โ””โ”€โ”€ static/
โ”‚       โ””โ”€โ”€ monitoring.html         #   Dark-mode real-time monitoring dashboard
โ”‚
โ”œโ”€โ”€ scripts/
โ”‚   โ”œโ”€โ”€ install_hub.sh              #   Automated Hub EC2 setup script
โ”‚   โ””โ”€โ”€ install_worker.sh           #   Automated Worker EC2 setup script
โ”‚
โ”œโ”€โ”€ AWS_EC2_DEPLOYMENT.md           #   End-to-end AWS deployment walkthrough
โ””โ”€โ”€ tests/                          #   107-test mock-based unit suite
    โ”œโ”€โ”€ conftest.py                 #     Module mock bootstrap (no real SSH/DB needed)
    โ”œโ”€โ”€ test_magics.py              #     Magic command unit tests
    โ”œโ”€โ”€ test_provider.py            #     LLM provider backend tests
    โ”œโ”€โ”€ test_spawner.py             #     SSH spawner lifecycle tests
    โ”œโ”€โ”€ test_spawner_lifecycle.py   #     RBAC, SessionStore & crash recovery tests
    โ””โ”€โ”€ test_config.py              #     Hub config & security handler tests

๐Ÿ” Security Model

Control How it works
OAuth Domain Lock hosted_domain in hub_settings.json restricts login to one Google Workspace domain
Admin Isolation admin_access = False โ€” admins cannot enter user containers via JupyterHub
2-Tier RBAC user.admin from JupyterHub panel; PermissionError raised before any cross-user action
Cross-User Blocking BlockOtherUsersHandler returns 403 on /user/<other>/ path access attempts
OOM Enforcement oom_score_adj = 500 written to /proc/self/oom_score_adj inside each kernel at startup
cgroups v2 Hard Limits systemd-run --scope caps every kernel's memory and CPU at the OS level
Vault Secret Zero-Disk Secrets fetched from Vault at spawn time, passed via environment โ€” never written to disk

๐Ÿงช Running Tests

All 107 tests run with zero real infrastructure โ€” no SSH, no EC2, no Vault:

# Install dev dependencies
pip install -e ".[dev]"

# Run the full suite
pytest -v tests/

# Individual suites
pytest -v tests/test_magics.py              # Magic commands (32 tests)
pytest -v tests/test_provider.py            # LLM provider backends (18 tests)
pytest -v tests/test_spawner.py             # SSH spawner lifecycle (11 tests)
pytest -v tests/test_spawner_lifecycle.py   # RBAC + SQLite + crash recovery (43 tests)
pytest -v tests/test_config.py             # Hub config & security (3 tests)

Expected:

107 passed, 1 warning in 2.90s

๐Ÿ—บ๏ธ Roadmap

Task Status Description
Task 1 โ€” Admin Core & Lifecycle โœ… Done SQLite session state, 2-tier RBAC, start/stop/poll/clear_state
Task 2 โ€” cgroups v2 Isolation โœ… Done systemd-run hard MemoryMax + CPUQuota via pre_spawn_hook
Task 3 โ€” Vault Secret Injection โœ… Done KV-v2 client; zero-disk env injection at spawn; graceful degradation
Task 4 โ€” Live Monitoring Dashboard โœ… Done psutil agent โ†’ WebSocket โ†’ dark-mode real-time dashboard
Task 5 โ€” Controlled Env Strategy โœ… Done User venv at ~/.jupyterpilot/venv; [ai] optional package extra
Task 6 โ€” LLM & MCP Connectivity โœ… Done 3 backends (Ollama, LiteLLM, MCP) with retry, rate-limiting & Vault key reads
Task 7 โ€” Core Magics โœ… Done %do, %fix with cell context, traceback stripping, --run flag
Task 8 โ€” Agentic Magics โœ… Done %review (AST + LLM) and %rework --diff (unified diff preview)
Task 9 โ€” JIT User Provisioning โœ… Done Auto-creates OS users on Worker VMs on first login
Future โ€” Redis/Postgres State ๐Ÿ”œ Planned Replace SQLite for multi-hub horizontal scaling
Future โ€” Auto-scaling Workers ๐Ÿ”œ Planned Spin up new EC2 workers via AWS SDK when group capacity is low

๐Ÿค Contributing

  1. Fork the repository
  2. Create a branch: git checkout -b feat/your-feature
  3. Make changes and ensure tests pass: pytest -v tests/
  4. Open a Pull Request with a clear description
# Development setup:
git clone https://github.com/wajoud/JupyterPilot.git
cd JupyterPilot
pip install -e ".[dev]"
pytest -v tests/   # all 107 should pass

๐Ÿ“„ License

MIT License โ€” see LICENSE for details.


Built with ๐Ÿ”ง by @wajoud ยท Star โญ if this helps your team!

๐Ÿ› ๏ธ Installation Guide ย ยทย  ๐Ÿ—๏ธ Deploy to AWS EC2 ย ยทย  ๐Ÿค– AI Magic Commands ย ยทย  ๐Ÿ“Š Live Monitoring

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jupyterhub_pilot-0.2.0.tar.gz (56.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jupyterhub_pilot-0.2.0-py3-none-any.whl (39.4 kB view details)

Uploaded Python 3

File details

Details for the file jupyterhub_pilot-0.2.0.tar.gz.

File metadata

  • Download URL: jupyterhub_pilot-0.2.0.tar.gz
  • Upload date:
  • Size: 56.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.13

File hashes

Hashes for jupyterhub_pilot-0.2.0.tar.gz
Algorithm Hash digest
SHA256 279a5558fa3eaaad863c35208375c4ef522e6a5d83d4179c7540b2f908675b80
MD5 e5af288ae0a42594439362ce6722bd9e
BLAKE2b-256 e5290df37f4eeb0b4d0c619e32dec0d0c5b1cbabee5b37766f3125a66a1fabe0

See more details on using hashes here.

File details

Details for the file jupyterhub_pilot-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for jupyterhub_pilot-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 286989b41723492a14c393d0c2a16dd7dfc954ec25a4f29818e6f386c5463083
MD5 5a3730fcc97f05d161884ae9251e6c4a
BLAKE2b-256 84fa91f13328fe4a0d27cc52821dc0772da827bf766ea5111ea40dd1c23cc2d5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page