Skip to content
Merged
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Claude Code on Databricks

Welcome! This environment comes pre-configured with 39 skills and 2 MCP servers.
Welcome! This environment comes pre-configured with 5 AI coding agents, 39 skills, and 2 MCP servers. Hermes Agent is available alongside Claude Code, Codex, Gemini CLI, and OpenCode — launch it with `hermes chat`.

## Skills (30 total)

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

[![Use this template](https://img.shields.io/badge/Use%20this%20template-2ea44f?logo=github)](https://github.com/datasciencemonkey/coding-agents-databricks-apps/generate)
[![Deploy to Databricks](https://img.shields.io/badge/Deploy-Databricks%20Apps-FF3621?logo=databricks&logoColor=white)](docs/deployment.md)
[![Agents](https://img.shields.io/badge/Agents-4%20included-green)](#whats-inside)
[![Agents](https://img.shields.io/badge/Agents-5%20included-green)](#whats-inside)
[![Skills](https://img.shields.io/badge/Skills-39%20built--in-blue)](#-all-39-skills)

> Run Claude Code, Codex, Gemini CLI, and OpenCode in your browser — zero setup, wired to your Databricks workspace.
> Run Claude Code, Codex, Gemini CLI, Hermes Agent, and OpenCode in your browser — zero setup, wired to your Databricks workspace.

---

Expand All @@ -25,6 +25,8 @@

🔵 **Gemini CLI** — Google's coding agent with shared skills

🟡 **Hermes Agent** — NousResearch's multi-provider AI CLI with tool-calling and skills

🟢 **OpenCode** — Open-source agent with multi-provider support

Every agent installs at boot and connects to your **Databricks AI Gateway** — on first terminal session, paste a short-lived PAT and all CLIs are configured automatically. Token auto-rotates every 10 minutes.
Expand Down Expand Up @@ -55,7 +57,7 @@ This isn't just a terminal in the cloud. Running coding agents on Databricks giv
| ✂️ **Split Panes** | Run two sessions side by side with a draggable divider |
| 🌐 **WebSocket I/O** | Real-time terminal output over WebSocket — zero-latency, eliminates polling delay |
| 🔁 **HTTP Polling Fallback** | Automatic fallback via Web Worker when WebSocket is unavailable |
| 🚀 **Parallel Setup** | 6 agent setups run in parallel (~5x faster startup) |
| 🚀 **Parallel Setup** | 7 agent setups run in parallel (~5x faster startup) |
| 🔍 **Search** | Find anything in your terminal history (Ctrl+Shift+F) |
| 🎤 **Voice Input** | Dictate commands with your mic (Option+V) |
| 📋 **Image Paste** | Paste or drag-and-drop images into the terminal — saved to `~/uploads/`, path inserted automatically |
Expand Down Expand Up @@ -284,7 +286,7 @@ This template repo opens that vision up for every Databricks user — no IDE set
| `HOME` | Yes | Set to `/app/python/source_code` in app.yaml |
| `ANTHROPIC_MODEL` | No | Claude model name (default: `databricks-claude-opus-4-6`) |
| `CODEX_MODEL` | No | Codex model name (default: `databricks-gpt-5-3-codex`) |
| `GEMINI_MODEL` | No | Gemini model name (default: `databricks-gemini-3-1-pro`) |
| `GEMINI_MODEL` | No | Gemini model name (default: `databricks-gemini-2-5-pro`) |
| `DATABRICKS_GATEWAY_HOST` | No | AI Gateway URL override. Auto-discovered from `DATABRICKS_WORKSPACE_ID` if unset |

### Security Model
Expand Down
19 changes: 18 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def handle_sigterm(signum, frame):
{"id": "codex", "label": "Configuring Codex CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None},
{"id": "opencode", "label": "Configuring OpenCode CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None},
{"id": "gemini", "label": "Configuring Gemini CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None},
{"id": "hermes", "label": "Configuring Hermes Agent", "status": "pending", "started_at": None, "completed_at": None, "error": None},
{"id": "databricks", "label": "Setting up Databricks CLI", "status": "pending", "started_at": None, "completed_at": None, "error": None},
{"id": "mlflow", "label": "Enabling MLflow tracing", "status": "pending", "started_at": None, "completed_at": None, "error": None},
]
Expand Down Expand Up @@ -317,7 +318,7 @@ def _configure_all_cli_auth(token):
# 3. Re-run Codex, OpenCode, Gemini setup scripts with token in env
# They are idempotent: detect CLI already installed, just write config files
env = {**os.environ, "DATABRICKS_TOKEN": token}
for script in ["setup_codex.py", "setup_opencode.py", "setup_gemini.py"]:
for script in ["setup_codex.py", "setup_opencode.py", "setup_gemini.py", "setup_hermes.py"]:
try:
result = subprocess.run(
["uv", "run", "python", script],
Expand Down Expand Up @@ -368,6 +369,7 @@ def run_setup():
("codex", ["uv", "run", "python", "setup_codex.py"]),
("opencode", ["uv", "run", "python", "setup_opencode.py"]),
("gemini", ["uv", "run", "python", "setup_gemini.py"]),
("hermes", ["uv", "run", "python", "setup_hermes.py"]),
("databricks", ["uv", "run", "python", "setup_databricks.py"]),
("mlflow", ["uv", "run", "python", "setup_mlflow.py"]),
]
Expand All @@ -379,6 +381,18 @@ def run_setup():
]
wait(futures)

# Sync latest token into all CLI configs — covers the race where PAT
# rotation happened while a setup script was still installing (the
# rotation's update_cli_tokens() call silently skips missing config files).
current_token = os.environ.get("DATABRICKS_TOKEN", "")
if current_token:
try:
from cli_auth import update_cli_tokens
update_cli_tokens(current_token)
logger.info("Post-setup token sync: all CLI configs updated with current token")
except Exception as e:
logger.warning(f"Post-setup token sync failed: {e}")

with setup_lock:
any_error = any(s["status"] == "error" for s in setup_state["steps"])
setup_state["status"] = "error" if any_error else "complete"
Expand Down Expand Up @@ -995,6 +1009,9 @@ def create_session():
# DATABRICKS_HOST is set in env (even without credentials).
shell_env.pop("DATABRICKS_TOKEN", None)
shell_env.pop("DATABRICKS_HOST", None)
# Also strip CLI-specific API keys so they read from config files
# (always current after rotation) instead of stale env snapshots.
shell_env.pop("GEMINI_API_KEY", None)
# Ensure HOME is set correctly
if not shell_env.get("HOME") or shell_env["HOME"] == "/":
shell_env["HOME"] = "/app/python/source_code"
Expand Down
2 changes: 1 addition & 1 deletion app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ env:
- name: ANTHROPIC_MODEL
value: databricks-claude-opus-4-7
- name: GEMINI_MODEL
value: databricks-gemini-3-1-pro
value: databricks-gemini-2-5-pro
- name: CODEX_MODEL
value: databricks-gpt-5-3-codex
- name: CLAUDE_CODE_DISABLE_AUTO_MEMORY
Expand Down
10 changes: 9 additions & 1 deletion app.yaml.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ env:
- name: ANTHROPIC_MODEL
value: databricks-claude-opus-4-6
- name: GEMINI_MODEL
value: databricks-gemini-3-1-pro
value: databricks-gemini-2-5-pro
- name: HERMES_MODEL
value: databricks-claude-opus-4-7
- name: HERMES_FALLBACK_MODEL
value: databricks-claude-opus-4-6
# Set ENABLE_HERMES=false to skip Hermes Agent install.
# Other CLIs are unaffected.
- name: ENABLE_HERMES
value: "true"
#OPTIONAL: Use the new Databricks AI Gateway if you have access (recommended), otherwise it will default to the older endpoint
- name: DATABRICKS_GATEWAY_HOST
value: https://<your-gateway-id>.ai-gateway.<env>.cloud.databricks.com
Expand Down
20 changes: 20 additions & 0 deletions cli_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def update_cli_tokens(token):
_update_codex(token)
_update_opencode(token)
_update_gemini(token)
_update_hermes(token)


def _update_claude(token):
Expand Down Expand Up @@ -68,6 +69,25 @@ def _update_gemini(token):
_replace_dotenv_key(path, "GEMINI_API_KEY", token)


def _update_hermes(token):
"""Update api_key lines in ~/.hermes/config.yaml."""
path = os.path.join(_HOME, ".hermes", "config.yaml")
try:
with open(path) as f:
content = f.read()
new_content = re.sub(
r'^( api_key: ).*$',
rf'\g<1>{token}',
content,
flags=re.MULTILINE
)
if new_content != content:
with open(path, "w") as f:
f.write(new_content)
except OSError:
pass


def _replace_dotenv_key(path, key, value):
"""Replace a KEY=value line in a dotenv file."""
try:
Expand Down
5 changes: 3 additions & 2 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ databricks apps deploy <your-app-name> \
| `HOME` | Yes | Set to `/app/python/source_code` in app.yaml |
| `ANTHROPIC_MODEL` | No | Claude model name (default: `databricks-claude-opus-4-6`) |
| `CODEX_MODEL` | No | Codex model name (default: `databricks-gpt-5-3-codex`) |
| `GEMINI_MODEL` | No | Gemini model name (default: `databricks-gemini-3-1-pro`) |
| `GEMINI_MODEL` | No | Gemini model name (default: `databricks-gemini-2-5-pro`) |
| `HERMES_MODEL` | No | Hermes model name (default: `databricks-claude-opus-4-7`) |
| `DATABRICKS_GATEWAY_HOST` | No | AI Gateway URL override. Auto-discovered from `DATABRICKS_WORKSPACE_ID` if unset. Falls back to direct model serving if neither is available |

## Security Model
Expand All @@ -78,7 +79,7 @@ This is a **single-user, zero-config auth** app. No secrets or tokens are requir

1. **Owner resolution**: The app owner is determined from `app.creator` via the service principal + Apps API — no PAT needed
2. **Authorization**: Each request's `X-Forwarded-Email` header is compared against `app.creator`. Non-matching users see 403
3. **Interactive PAT setup**: On first terminal session, the user pastes a short-lived PAT interactively. All CLIs (Claude, Codex, OpenCode, Gemini, Databricks) are configured automatically
3. **Interactive PAT setup**: On first terminal session, the user pastes a short-lived PAT interactively. All CLIs (Claude, Codex, OpenCode, Gemini, Hermes, Databricks) are configured automatically
4. **Auto-rotation**: PAT rotates every 10 minutes with a 15-minute lifetime. Old tokens are proactively revoked. Maximum leaked-token exposure: 15 minutes
5. **Session-aware**: Rotation is skipped when no active terminal sessions exist
6. **On restart**: The user re-pastes a token (no persistence by design)
Expand Down
25 changes: 23 additions & 2 deletions setup_gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

host = os.environ.get("DATABRICKS_HOST", "")
token = os.environ.get("DATABRICKS_TOKEN", "")
gemini_model = os.environ.get("GEMINI_MODEL", "databricks-gemini-3-1-pro")
gemini_model = os.environ.get("GEMINI_MODEL", "databricks-gemini-2-5-pro")

# 1. Install Gemini CLI into ~/.local/bin (always, even without token)
local_bin = home / ".local" / "bin"
Expand Down Expand Up @@ -78,13 +78,34 @@
gemini_dir = home / ".gemini"
gemini_dir.mkdir(exist_ok=True)

# Pre-trust ~/projects/ so Gemini CLI loads .env and project settings.
# Without this, Gemini's security engine silently skips .env loading in
# untrusted workspaces, causing auth failures (see gemini-cli#20005).
projects_dir = str(home / "projects")
trusted_folders_path = gemini_dir / "trustedFolders.json"
try:
if trusted_folders_path.exists():
trusted = json.loads(trusted_folders_path.read_text())
else:
trusted = {}
if trusted.get(projects_dir) != "TRUST_FOLDER":
trusted[projects_dir] = "TRUST_FOLDER"
# Also trust home dir so ~/.gemini/.env is always loadable
home_str = str(home)
if trusted.get(home_str) != "TRUST_FOLDER":
trusted[home_str] = "TRUST_FOLDER"
trusted_folders_path.write_text(json.dumps(trusted, indent=2))
print(f"Gemini trusted folders configured: {trusted_folders_path}")
except Exception as e:
print(f"Warning: could not write trustedFolders.json: {e}")

# Write .env file with Databricks endpoint configuration
# Gemini CLI auto-loads env from ~/.gemini/.env
# The Google-native endpoint on Databricks mirrors /serving-endpoints/anthropic
env_content = f"""# Databricks Model Serving - Google Gemini native endpoint
GEMINI_MODEL={gemini_model}
GOOGLE_GEMINI_BASE_URL={gemini_base_url}
GEMINI_API_KEY_AUTH_MECHANISM="bearer"
GEMINI_API_KEY_AUTH_MECHANISM=bearer
GEMINI_API_KEY={auth_token}
"""

Expand Down
Loading