Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
238af1b
fix(cli-auth): atomic writes + observable failures on PAT rotation
dgokeeffe May 6, 2026
13e3a02
ci(release): generate signed SBOM on each release
dgokeeffe May 16, 2026
3671a63
docs(legal): add Project Support / AS-IS disclaimer to README
dgokeeffe May 17, 2026
73fc328
fix(security): close unauth info-disclosure surface (setup-status, pa…
dgokeeffe May 17, 2026
1f084a8
docs: move SBOM verification to docs/SECURITY.md, drop stale plans
mpkrass7 May 19, 2026
3fdad07
chore(deps-dev): update pytest-playwright requirement
dependabot[bot] Jun 7, 2026
64ca345
chore(deps): bump click from 8.3.3 to 8.4.1
dependabot[bot] Jun 7, 2026
574ad2f
chore(deps): bump idna from 3.16 to 3.17
dependabot[bot] Jun 7, 2026
b03d354
chore(deps): bump pydantic-core from 2.46.4 to 2.47.0
dependabot[bot] Jun 7, 2026
b3fca4c
docs: document Omnigent host integration
dgokeeffe Jul 10, 2026
557592b
docs: correct Omnigent identity and grant guidance
dgokeeffe Jul 18, 2026
bced517
docs(auth): describe loopback SP token broker
dgokeeffe Jul 19, 2026
681d91f
chore(deps): bump actions/setup-python from 6.2.0 to 7.0.0
dependabot[bot] Jul 26, 2026
adc436b
chore(deps): bump softprops/action-gh-release from 3.0.0 to 3.0.2
dependabot[bot] Jul 26, 2026
b3d2fc4
chore(deps): bump actions/checkout from 7.0.0 to 7.0.1
dependabot[bot] Jul 26, 2026
0eb89ce
chore(deps): bump astral-sh/setup-uv from 8.1.0 to 9.0.0
dependabot[bot] Aug 2, 2026
70a3839
fix(tests): scope e2e prerequisite skip to the e2e directory
dgokeeffe Aug 5, 2026
c4b0b31
Merge PR #102
dgokeeffe Aug 5, 2026
a6cca67
Merge PR #103
dgokeeffe Aug 5, 2026
6ad8723
Merge PR #104
dgokeeffe Aug 5, 2026
2d249fb
Merge PR #105
dgokeeffe Aug 5, 2026
e1d9459
Merge PR #80
dgokeeffe Aug 5, 2026
de4f2fa
Merge PR #81
dgokeeffe Aug 5, 2026
f181567
Merge PR #83
dgokeeffe Aug 5, 2026
cacb024
Merge PR #84
dgokeeffe Aug 5, 2026
831cbcf
Merge branch 'staging-main' into pr41-merge
dgokeeffe Aug 5, 2026
2c0f843
docs: keep litellm design doc referenced by app.py/setup_opencode/set…
dgokeeffe Aug 5, 2026
71a0abd
Merge PR #41: signed SBOM on release
dgokeeffe Aug 5, 2026
4f4a9a5
Merge branch 'staging-main' into pr43-merge
dgokeeffe Aug 5, 2026
d1026e4
Merge PR #43: Project Support / AS-IS disclaimer
dgokeeffe Aug 5, 2026
1f84863
Merge branch 'staging-main' into pr93-merge
dgokeeffe Aug 5, 2026
ad5a234
Merge PR #93: document Omnigent host integration + loopback SP broker
dgokeeffe Aug 5, 2026
6b468e6
Merge staging-main into fix/cli-auth-rotation-race
dgokeeffe Aug 5, 2026
4af9aae
Merge PR #23: atomic writes + observable failures on PAT rotation
dgokeeffe Aug 5, 2026
0282fe9
chore(config): declare every ENABLE_<CLI> toggle in each app.yaml ove…
dgokeeffe Aug 5, 2026
fdc9c4d
Merge chore/overlay-toggle-parity (salvaged from #30)
dgokeeffe Aug 5, 2026
c0e72b1
Merge staging-main into fix/info-disclosure-trim
dgokeeffe Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ jobs:
runs-on: databrickslabs-protected-runner-group
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

- name: Install audit tools
run: pip install pip-audit==2.9.0
Expand Down
49 changes: 47 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ jobs:
runs-on: databrickslabs-protected-runner-group
permissions:
contents: write
# id-token: write is required for cosign keyless signing via GitHub OIDC.
# Without it, cosign falls back to interactive auth and the workflow hangs.
id-token: write

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

Expand Down Expand Up @@ -111,10 +114,52 @@ jobs:
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"

# ----- Supply-chain provenance: SBOM + cosign keyless signature ---------
# Generates a CycloneDX SBOM from the repo (Python + npm package metadata),
# then signs it with cosign using a short-lived OIDC token from GitHub.
# Verifiers can confirm the SBOM came from this workflow at this tag via:
# cosign verify-blob --bundle coda-sbom.cdx.json.cosign.bundle \
# --certificate-identity-regexp 'https://github.com/databrickslabs/coding-agents-databricks-apps/.+' \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
# coda-sbom.cdx.json
- name: Generate CycloneDX SBOM
uses: anchore/sbom-action@9f7302141466aa6482940f15371237e9d9f4c34a # v0.20.5
with:
path: .
format: cyclonedx-json
output-file: coda-sbom.cdx.json
# Don't auto-upload; we attach via softprops below for one consistent release.
upload-artifact: false
upload-release-assets: false

- name: Install cosign
uses: sigstore/cosign-installer@d7d6e07ee54d2049ce5cdfc7eed4d6a6ccd80f5b # v3.5.0
with:
cosign-release: v2.4.1

- name: Sign SBOM with cosign (keyless OIDC)
run: |
# --yes auto-confirms the Sigstore transparency log entry (Rekor).
# The resulting bundle contains the signature + certificate + Rekor
# inclusion proof in one self-contained file — easier for downstream
# verifiers than separate .sig/.cert files.
cosign sign-blob --yes \
--bundle coda-sbom.cdx.json.cosign.bundle \
coda-sbom.cdx.json
# Sanity check: verify what we just signed before publishing.
cosign verify-blob \
--bundle coda-sbom.cdx.json.cosign.bundle \
--certificate-identity-regexp 'https://github.com/${{ github.repository }}/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
coda-sbom.cdx.json

- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
tag_name: "${{ steps.version.outputs.TAG }}"
name: "${{ steps.version.outputs.TAG }}"
body: ${{ steps.notes.outputs.NOTES }}
prerelease: ${{ inputs.prerelease }}
files: |
coda-sbom.cdx.json
coda-sbom.cdx.json.cosign.bundle
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ jobs:
runs-on: databrickslabs-protected-runner-group
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

- name: Run tests
run: uv run pytest tests/ -v
6 changes: 3 additions & 3 deletions .github/workflows/update-lockfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

- name: Regenerate requirements.lock
run: uv pip compile requirements.txt -o requirements.lock --generate-hashes
Expand Down
101 changes: 97 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@

---

## 💬 Project Support

Please note that this project is provided for your exploration only and is not
formally supported by Databricks with Service Level Agreements (SLAs). It is
provided AS-IS, and we do not make any guarantees. Please do not submit a
support ticket relating to any issues arising from the use of this project.

Any issues discovered through the use of this project should be filed as GitHub
[Issues on this repository](https://github.com/databrickslabs/coding-agents-databricks-apps/issues).

See [LICENSE.md](LICENSE.md) for full terms, including the warranty disclaimer
and limitation of liability. See [NOTICE.md](NOTICE.md) for third-party software
attribution.

---

<div align="center">
<video src="https://github.com/user-attachments/assets/40405b46-532a-4f14-82e3-414cb3744684" controls width="900"></video>
</div>
Expand Down Expand Up @@ -155,6 +171,77 @@ Tracing setup is skipped gracefully when `APP_OWNER` is not set (e.g., local dev

---

## Omnigent Host Integration

CoDA can register itself as a persistent **[Omnigent](https://github.com/omnigent-ai/omnigent) agent host** — an always-on target the Omnigent server can drive coding-agent sessions into. Those sessions run *inside this container* and use the same filesystem as browser terminals. They authenticate to Databricks as the CoDA app service principal, not as the interactive browser user, so their Unity Catalog authority may differ. A deployed CoDA app becomes both an interactive terminal **and** a headless host that survives restarts and redeploys.

**Off by default.** With `OMNIGENTS_SERVER_URL` unset, none of this runs and CoDA behaves exactly as before. This is opt-in, environment-specific wiring — the committed `app.yaml` keeps it commented out.

### Turning it on

Set three variables in your deployed `app.yaml` (see `app.yaml.lakemeter` for a ready-to-copy overlay template):

```yaml
# app.yaml
env:
# The Omnigent server this app registers against on boot.
- name: OMNIGENTS_SERVER_URL
value: "https://<your-omnigent-app>.<region>.databricksapps.com"
# UC Volume holding the omnigent host wheels (app SP needs READ_VOLUME).
- name: OMNIGENTS_WHEEL_SPEC
value: "/Volumes/<catalog>/<schema>/artifacts/wheels"
# Optional: force-reinstall the host CLI on boot while rolling out a new wheel.
- name: OMNIGENTS_FORCE_REINSTALL
value: "1"
```

Before deploying, grant the CoDA app service principal `CAN_USE` on the
Omnigent server app plus `USE_CATALOG`, `USE_SCHEMA`, `READ_VOLUME`, and
`WRITE_VOLUME` on the wheel-volume path. The repository's grant target applies
the complete prerequisite set:

```bash
make grant-omnigent-host PROFILE=<profile> APP_NAME=<coda-app>
```

On boot, `initialize_app()` calls `start_host()`, which — only when `OMNIGENTS_SERVER_URL` is set — installs the `omnigents host` CLI from the wheel volume and launches it as a supervised background process that dials the server over an outbound WSS tunnel.

### Two credentials, two jobs

The non-obvious part of this design is that the host uses **two separate credentials** (see `omnigents_host.py`):

```
┌─────────────────────── CoDA container ───────────────────────┐
│ │
│ omnigents host ──WSS tunnel──► Omnigent server │
│ │ (auth: app-SP OAuth token) │
│ │ │
│ └── spawns runner ──► AI Gateway │
│ (auth: CoDA's ANTHROPIC_* creds) │
└──────────────────────────────────────────────────────────────┘
```

* **Host tunnel and runners** authenticate to the server through short-lived app-SP OAuth tokens. CoDA captures the SP credentials before stripping them from the environment, keeps the client secret only in Flask process memory, and exposes fresh tokens through a loopback-only broker. The on-disk `[omnigents-host]` profile contains only the workspace host; spawned Omnigent runners receive a refresh command, not a static bearer or client secret.
* **Harness LLM** — the runner the host spawns authenticates to AI Gateway via CoDA's already-injected `ANTHROPIC_*` env. No new LLM credential is minted.

### Runtime controls

Beyond boot registration, the host can be driven at runtime:

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/omnigents-status` | GET | Host-integration state (FR-9 observability) |
| `/api/omnigent-host/status` | GET | Current runtime host state |
| `/api/omnigent-host/connect` | POST | Start a host tunnel for a supplied `server_url` |
| `/api/omnigent-host/disconnect` | POST | Stop the active host tunnel |
| `/api/omnigent-host/share` | POST | Share the SP-owned host with a connecting user |

### Related

`ENABLE_SP_APIKEYHELPER=true` enables the same loopback-broker boundary for agent gateway calls: helpers fetch short-lived app-SP OAuth tokens without persisting the SP client secret or a static token in terminal-visible configuration.

---

## Quick Start

### Deploy to Databricks Apps
Expand Down Expand Up @@ -262,12 +349,12 @@ Open [http://localhost:8000](http://localhost:8000) — type `claude`, `codex`,
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Terminal UI with inline setup progress |
| `/health` | GET | Health check with session count and setup status |
| `/api/setup-status` | GET | Setup progress for the UI |
| `/api/app-state` | GET | Persisted app state (owner, last rotation) |
| `/health` | GET | Liveness probe. Exempt from the SSO gate so the platform can reach it. Unauthenticated callers get only `{"status": "healthy"\|"degraded"}`; the owner additionally gets version, session count, setup status and PAT-rotator state |
| `/api/setup-status` | GET | Setup progress for the UI (owner-gated) |
| `/api/app-state` | GET | Persisted app state (owner, last rotation) (owner-gated) |
| `/api/version` | GET | App version |
| `/api/sessions` | GET | List active (non-exited) sessions with metadata |
| `/api/pat-status` | GET | Whether a valid, usable PAT is currently configured |
| `/api/pat-status` | GET | Whether a valid, usable PAT is currently configured (owner-gated) |
| `/api/configure-pat` | POST | Interactive first-session PAT setup (owner-gated via SSO) |
| `/api/inject-pat` | POST | Programmatic PAT injection for scripted provisioning (shared-secret gated; disabled unless `CODA_BOOTSTRAP_SECRET` is set). Also requires a workspace **OAuth bearer** for the Apps edge — a PAT bearer 401s at the platform edge before reaching the app |
| `/api/session` | POST | Create new terminal session |
Expand Down Expand Up @@ -321,6 +408,10 @@ Open [http://localhost:8000](http://localhost:8000) — type `claude`, `codex`,
| `MLFLOW_TRACING_ENABLED` | No | Set to `"true"` to enable MLflow tracing for Claude, Codex, and Gemini in one switch (default `"false"`) |
| `CLAUDE_CODE_OTEL_ENABLED` | No | Set to `"true"` to enable Claude Code OTEL export to Unity Catalog (default `"false"`) |
| `CLAUDE_CODE_OTEL_CATALOG_SCHEMA` | No | Target `<catalog>.<schema>` for `claude_otel_spans`, `claude_otel_logs`, and `claude_otel_metrics` |
| `OMNIGENTS_SERVER_URL` | No | Omnigent server to register against on boot. Unset = host integration off (default). See [Omnigent Host Integration](#omnigent-host-integration) |
| `OMNIGENTS_WHEEL_SPEC` | No | UC Volume path holding the `omnigents host` wheels (app SP needs `READ_VOLUME`). Required when `OMNIGENTS_SERVER_URL` is set |
| `OMNIGENTS_FORCE_REINSTALL` | No | Set `"1"` to reinstall the host CLI on boot (for rolling out a new wheel); otherwise `uv tool install` no-ops on an existing binary |
| `ENABLE_SP_APIKEYHELPER` | No | Set `"true"` to broker short-lived app-SP OAuth tokens over loopback without persisting the client secret in terminal-visible configuration |
| `DEEPWIKI_MCP_URL` | No | Override or disable the DeepWiki MCP server (set to `""` to remove) |
| `EXA_MCP_URL` | No | Override or disable the Exa MCP server (set to `""` to remove) |
| `TEAM_MEMORY_MCP_URL` | No | Optional shared-org-memory MCP server URL |
Expand All @@ -330,6 +421,8 @@ Open [http://localhost:8000](http://localhost:8000) — type `claude`, `codex`,

Single-user app — the owner is resolved via the app's service principal and Apps API (`app.creator`), with no PAT required at deploy time. Authorization checks `X-Forwarded-Email` against `app.creator`. On first terminal session, the user pastes a short-lived PAT interactively. Tokens auto-rotate every 10 minutes (15-minute lifetime), with old tokens proactively revoked. On restart, the user re-pastes (no persistence by design).

Each GitHub Release ships a signed CycloneDX SBOM — see [docs/SECURITY.md](./docs/SECURITY.md) for verification steps.

### Gunicorn

Production uses `workers=1` (PTY state is process-local), `threads=16` (concurrent polling + WebSocket), `gthread` worker class, `timeout=60` (long-lived WebSocket connections).
Expand Down
41 changes: 38 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,8 +1218,26 @@ def cleanup_stale_sessions():
@app.before_request
def authorize_request():
"""Check authorization before processing any request."""
# Skip auth for health check, setup status, and Socket.IO (has own auth via connect event)
if request.path in ("/health", "/api/setup-status", "/api/pat-status", "/api/configure-pat", "/api/inject-pat", "/api/app-state") or request.path.startswith("/socket.io"):
# Auth-exempt:
# /health — liveness probe. Stays reachable for the platform,
# but trims its body for unauthenticated callers;
# see health() for what each audience sees.
# /api/configure-pat — owner-gates itself in-handler (cannot use the
# before_request gate; needed during bootstrap before
# app_owner is resolved). See configure_pat() guard.
# /api/inject-pat — gated on the CODA_BOOTSTRAP_SECRET shared secret,
# and 404s when that env var is unset. Provisioning
# scripts have no SSO session, so it can't use the
# SSO gate. See inject_pat().
# /socket.io/* — has own auth gate via the 'connect' WS event
#
# Previously exempt but now owner-gated (closed unauth info-disclosure
# surface): /api/setup-status, /api/pat-status, /api/app-state. All three
# are only polled by the frontend, which loads from "/" (auth'd) so already
# has SSO cookies — no functional regression.
if request.path in (
"/health", "/api/configure-pat", "/api/inject-pat",
) or request.path.startswith("/socket.io"):
return None

authorized, user = check_authorization()
Expand Down Expand Up @@ -1319,6 +1337,17 @@ def attach_session():

@app.route("/health")
def health():
# Two audiences, two response shapes:
#
# unauthenticated — {"status": "healthy"|"degraded"} and nothing else.
# Version, session counts, setup state and rotator internals all enable
# version-targeted exploit selection or leak the app's auth posture to
# anyone who can reach the URL.
# the owner — the full diagnostic payload below.
#
# `status` itself stays visible to everyone: a liveness probe that can't
# report unhealthiness is useless, and "degraded" is the signal that makes
# a zombie app (worker answering, PAT rotation dead) observable at all.
with sessions_lock:
session_count = len(sessions)
with setup_lock:
Expand Down Expand Up @@ -1352,8 +1381,14 @@ def health():
pat_rotator.token is not None and auth.get("rotator_alive") is False
)

status = "degraded" if degraded else "healthy"

authorized, _ = check_authorization()
if not authorized:
return jsonify({"status": status})

return jsonify({
"status": "degraded" if degraded else "healthy",
"status": status,
"version": APP_VERSION,
"setup_status": current_setup_status,
"active_sessions": session_count,
Expand Down
13 changes: 11 additions & 2 deletions app.yaml.lakemeter
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,21 @@ env:
value: databricks-claude-opus-4-8
- name: HERMES_FALLBACK_MODEL
value: databricks-claude-opus-4-8
# Set ENABLE_HERMES=false to skip Hermes Agent install. Other CLIs are unaffected.
# Per-CLI install toggles. Each ENABLE_* defaults to true when absent, and an
# Apps overlay replaces app.yaml wholesale rather than merging — so list all
# of them explicitly or an omitted toggle silently means "install it".
- name: ENABLE_HERMES
value: "true"
# Set ENABLE_PI=false to skip installing the Pi coding agent. Other CLIs are unaffected.
- name: ENABLE_PI
value: "true"
- name: ENABLE_OPENCODE
value: "true"
# Codex needs a Responses-API (*-codex) endpoint and Gemini needs a served
# Gemini model; neither is available here, so skip their installs.
- name: ENABLE_CODEX
value: "false"
- name: ENABLE_GEMINI
value: "false"
- name: CLAUDE_CODE_DISABLE_AUTO_MEMORY
value: 0
- name: MAX_CONCURRENT_SESSIONS
Expand Down
Loading