Skip to content

experimental/ssh: resolve bare python/pip to the environment interpreter - #5888

Merged
anton-107 merged 4 commits into
mainfrom
anekipelov/deco-27499-env-python
Jul 10, 2026
Merged

experimental/ssh: resolve bare python/pip to the environment interpreter#5888
anton-107 merged 4 commits into
mainfrom
anekipelov/deco-27499-env-python

Conversation

@anton-107

@anton-107anton-107 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

In an interactive databricks ssh connect session, bare python/pip resolved to the wrong interpreter (system or cluster-libraries python) instead of the environment interpreter at $DATABRICKS_VIRTUAL_ENV, so packages installed in the environment (including with --base-environment) could not be imported without extra setup. import <env-package> failed with ModuleNotFoundError. (DECO-27499)

Root cause (two layers)

sshd forwards a snapshot of the environment-first PATH into each session via a SetEnv line, but two things clobbered it in an interactive shell:

  1. Login shell — the client ran exec bash -l; -l re-sources /etc/profile, which rebuilds PATH from scratch, dropping the environment bin dir → bare python = system python.
  2. Interactive rcany interactive bash (-i or -l) sources /etc/bash.bashrc, which on serverless runs activate_root_python_environment.sh and prepends .../cluster_libraries/python/binahead of the environment interpreter → bare python = cluster-libraries python.

The non-interactive -- <cmd> path was already correct (sshd runs it via $SHELL -c, sourcing neither file).

Fix (two coordinated changes)

  • Client (client.go): launch an interactive non-login shell (bash -i) so /etc/profile is no longer re-sourced.
  • Server (server.go): seed a guarded, idempotent ~/.bashrc snippet that re-prepends the environment interpreter's bin directory. ~/.bashrc is sourced after/etc/bash.bashrc, so it wins; /etc/bash.bashrc and /etc/profile.d are root-owned and not writable by the non-root serverless user, so ~/.bashrc is the only usable hook. The seed is best-effort — a write failure logs a warning and does not abort the tunnel (mirrors the existing /run/sshd handling).

Why server-side and not client-side

Each --name gets a fresh ephemeral $HOME, so a client-side edit wouldn't persist. A pure client-side re-prepend via bash --rcfile <(...) was rejected because the remote command is parsed by the login shell (/bin/sh/dash on some images), where process substitution is a syntax error and would break connect.

Scope

Applied on all compute, guarded by $DATABRICKS_VIRTUAL_ENV being set. The bootstrap sets that variable to sys.executable (an absolute path) on every compute type, so dirname always yields the correct interpreter's bin directory.

Testing

  • go test ./experimental/ssh/... green, including new TestSeedEnvActivation (create / append / trailing-newline / idempotent-preserves-content / guard-present).
  • bash -i assertions updated in client_internal_test.go.

Manually verified against real compute

Confirmed end-to-end across all three compute types — in each interactive session, which python resolved to the environment interpreter (dirname($DATABRICKS_VIRTUAL_ENV)) rather than the system or cluster-libraries python, and importing an environment-only package succeeded:

  • Serverless CPU — with a custom --base-environment (cowsay); import cowsay worked.
  • Serverless GPU (GPU_1xA10) — with the same custom --base-environment; import cowsay worked.
  • Dedicated cluster (single-node) — bare which python resolved to the environment interpreter.

This pull request and its description were written by Isaac.

In an interactive `databricks ssh connect` session, bare `python`/`pip`
resolved to the system or cluster-libraries interpreter rather than the
environment interpreter at `$DATABRICKS_VIRTUAL_ENV`, so packages installed
in the environment could not be imported without extra setup.
Two things clobbered the environment-first PATH that sshd forwards via
SetEnv: a login shell (`bash -l`) re-sourced /etc/profile, rebuilding PATH
from scratch; and any interactive bash sources /etc/bash.bashrc, which on
serverless runs activate_root_python_environment.sh and prepends the
cluster-libraries python ahead of the environment interpreter.
Fix both layers:
- Client: launch an interactive non-login shell (`bash -i`) so /etc/profile
is no longer re-sourced.
- Server: seed a guarded, idempotent `~/.bashrc` snippet that re-prepends
the environment interpreter's bin directory. ~/.bashrc is sourced after
/etc/bash.bashrc, so it wins; /etc/bash.bashrc and /etc/profile.d are
root-owned and not writable by the non-root serverless user. The seed is
best-effort: a write failure logs a warning and does not abort the tunnel.
Co-authored-by: Isaac
@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: a00426c

Run: 29089622075

Env💚​RECOVERED🙈​SKIP✅​pass🙈​skipTime
💚​aws linux4423010775:27
💚​aws windows4423210756:59
💚​aws-ucws linux443149955:22
💚​aws-ucws windows443169937:04
💚​azure linux4423010764:46
💚​azure windows4423210746:54
💚​azure-ucws linux443169925:39
💚​azure-ucws windows443189907:01
💚​gcp linux4422910785:11
💚​gcp windows4423110768:17
8 interesting tests: 4 RECOVERED, 4 SKIP
Test Nameaws linuxaws windowsaws-ucws linuxaws-ucws windowsazure linuxazure windowsazure-ucws linuxazure-ucws windowsgcp linuxgcp windows
💚​TestAccept💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R
🙈​TestAccept/bundle/invariant/no_drift🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/ssh/connection🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
💚​TestFetchRepositoryInfoAPI_FromRepo💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R
💚​TestFetchRepositoryInfoAPI_FromRepo/root💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R
💚​TestFetchRepositoryInfoAPI_FromRepo/subdir💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R
Top 10 slowest tests (at least 2 minutes):
durationenvtestname
7:04gcp windowsTestAccept
6:08aws windowsTestAccept
6:02aws-ucws windowsTestAccept
6:00azure-ucws windowsTestAccept
5:54azure windowsTestAccept
2:59aws linuxTestAccept
2:55azure linuxTestAccept
2:52gcp linuxTestAccept
2:47azure-ucws linuxTestAccept
2:45aws-ucws linuxTestAccept

…ompute
Add experimental/ssh/CLAUDE.md capturing the verification discipline for
`ssh connect` changes: confirm the built binary matches HEAD, build release
archives with `./task snapshot-release` (not `./task build`) for --releases-dir,
and delete the versioned workspace directory before re-verifying a rebuilt dev
binary (same-version dev uploads are otherwise skipped). Also notes that the
server and login session share a user/$HOME on serverless, so a per-compute
symptom is usually a stale upload rather than a real difference.
Co-authored-by: Isaac
The test set $HOME, but env.UserHomeDir reads $USERPROFILE on Windows, so
seedEnvActivation targeted the real profile and the temp-dir assertions
failed. Set env.HomeEnvVar() instead so the test uses the same home the
production code resolves, on every OS.
Co-authored-by: Isaac

@rugpanovrugpanov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving. Reviewed with Isaac Review + codex — no actionable findings from either. The bare python/pip → environment-interpreter resolution is focused and covered by the new server/client tests for the bashrc-seeding behavior.

@anton-107
anton-107 added this pull request to the merge queueJul 10, 2026
Merged via the queue into main with commit d6636f4Jul 10, 2026
27 checks passed
@anton-107
anton-107 deleted the anekipelov/deco-27499-env-python branch July 10, 2026 14:37
@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: d6636f4

Run: 29100590588

Env❌​FAIL🟨​KNOWN🔄​flaky💚​RECOVERED🙈​SKIP✅​pass🙈​skipTime
💚​aws linux44529100741:34
🔄​aws windows334484102053:37
❌​aws-ucws linux91242993817104:00
❌​aws-ucws windows9152936835125:46
💚​azure linux44523100844:02
💚​azure windows44480102154:28
🔄​azure-ucws linux45289585295:51
❌​azure-ucws windows21422837870105:07
💚​gcp linux44514101346:54
🔄​gcp windows324470102658:53
25 interesting tests: 11 FAIL, 10 flaky, 2 SKIP, 1 KNOWN, 1 RECOVERED
Test Nameaws linuxaws windowsaws-ucws linuxaws-ucws windowsazure linuxazure windowsazure-ucws linuxazure-ucws windowsgcp linuxgcp windows
🟨​TestAccept💚​R🔄​f🟨​K🟨​K💚​R💚​R💚​R🟨​K💚​R💚​R
🔄​TestAccept/bundle/invariant/no_drift🙈​S🙈​S💚​R💚​R🙈​S🙈​S💚​R🔄​f🙈​S🙈​S
🔄​TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=database_instance.yml.tmpl/READPLAN=1✅​p✅​p✅​p🔄​f
🔄​TestAccept/bundle/resources/clusters/resize-terminated-fallback✅​p🔄​f✅​p✅​p✅​p✅​p✅​p✅​p✅​p✅​p
🔄​TestAccept/bundle/resources/clusters/resize-terminated-fallback/DATABRICKS_BUNDLE_ENGINE=direct✅​p🔄​f✅​p✅​p✅​p✅​p✅​p✅​p✅​p✅​p
❌​TestAccept/bundle/resources/dashboards/change-name✅​p✅​p✅​p✅​p✅​p✅​p🔄​f❌​F✅​p✅​p
❌​TestAccept/bundle/resources/dashboards/change-name/DATABRICKS_BUNDLE_ENGINE=direct✅​p✅​p✅​p✅​p✅​p✅​p✅​p❌​F✅​p✅​p
🔄​TestAccept/bundle/resources/dashboards/change-name/DATABRICKS_BUNDLE_ENGINE=terraform✅​p✅​p✅​p✅​p✅​p✅​p🔄​f✅​p✅​p✅​p
❌​TestAccept/bundle/resources/postgres_endpoints/basic🙈​s🙈​s❌​F❌​F🙈​s🙈​s🙈​s🙈​s🙈​s🙈​s
❌​TestAccept/bundle/resources/postgres_endpoints/basic/DATABRICKS_BUNDLE_ENGINE=direct❌​F❌​F
❌​TestAccept/bundle/resources/postgres_endpoints/basic/DATABRICKS_BUNDLE_ENGINE=terraform❌​F❌​F
❌​TestAccept/bundle/resources/postgres_endpoints/replace_existing🙈​s🙈​s❌​F❌​F🙈​s🙈​s🙈​s🙈​s🙈​s🙈​s
❌​TestAccept/bundle/resources/postgres_endpoints/replace_existing/DATABRICKS_BUNDLE_ENGINE=direct❌​F❌​F
❌​TestAccept/bundle/resources/postgres_endpoints/replace_existing/DATABRICKS_BUNDLE_ENGINE=terraform❌​F❌​F
❌​TestAccept/bundle/resources/postgres_endpoints/update_autoscaling🙈​s🙈​s❌​F❌​F🙈​s🙈​s🙈​s🙈​s🙈​s🙈​s
❌​TestAccept/bundle/resources/postgres_endpoints/update_autoscaling/DATABRICKS_BUNDLE_ENGINE=direct❌​F❌​F
❌​TestAccept/bundle/resources/postgres_endpoints/update_autoscaling/DATABRICKS_BUNDLE_ENGINE=terraform❌​F❌​F
🔄​TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name🙈​S🙈​S🔄​f💚​R🙈​S🙈​S🔄​f💚​R🙈​S🙈​S
🔄​TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name/DATABRICKS_BUNDLE_ENGINE=direct🔄​f✅​p🔄​f✅​p
🙈​TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🙈​TestAccept/ssh/connection🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S🙈​S
🔄​TestSyncIncrementalSyncPythonNotebookToFile✅​p✅​p✅​p✅​p✅​p✅​p✅​p✅​p✅​p🔄​f
💚​TestFetchRepositoryInfoAPI_FromRepo💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R💚​R
🔄​TestFetchRepositoryInfoAPI_FromRepo/root💚​R💚​R💚​R💚​R💚​R💚​R💚​R🔄​f💚​R🔄​f
🔄​TestFetchRepositoryInfoAPI_FromRepo/subdir💚​R💚​R💚​R💚​R💚​R💚​R💚​R🔄​f💚​R🔄​f
Top 50 slowest tests (at least 2 minutes):
durationenvtestname
15:12gcp windowsTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
14:05gcp linuxTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
13:01azure-ucws linuxTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
12:38aws-ucws windowsTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
11:28azure windowsTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
8:45azure-ucws linuxTestAccept/bundle/resources/apps/lifecycle-started-omitted/DATABRICKS_BUNDLE_ENGINE=direct
8:41aws linuxTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
8:39aws windowsTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
8:20gcp windowsTestAccept/bundle/resources/clusters/deploy/local_ssd_count/DATABRICKS_BUNDLE_ENGINE=direct
8:07aws-ucws linuxTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:46gcp linuxTestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=direct
7:41azure-ucws linuxTestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
7:34azure-ucws windowsTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:34aws linuxTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:32azure windowsTestAccept/bundle/resources/apps/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
7:27gcp windowsTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:26gcp linuxTestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=terraform
7:25azure-ucws linuxTestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=terraform
7:24aws-ucws windowsTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:09gcp linuxTestAccept/bundle/resources/clusters/deploy/local_ssd_count/DATABRICKS_BUNDLE_ENGINE=direct
7:02gcp linuxTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:02azure linuxTestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
6:57aws-ucws linuxTestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=terraform
6:53azure windowsTestAccept/bundle/resources/apps/lifecycle-started-omitted/DATABRICKS_BUNDLE_ENGINE=direct
6:42azure linuxTestAccept/bundle/resources/clusters/deploy/data_security_mode/DATABRICKS_BUNDLE_ENGINE=direct
6:41azure-ucws linuxTestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
6:39aws linuxTestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=terraform
6:29azure-ucws windowsTestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=direct
6:12azure-ucws windowsTestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=terraform
6:11gcp linuxTestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
6:11aws windowsTestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
6:11azure-ucws windowsTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
6:09gcp windowsTestAccept
6:09azure windowsTestAccept
6:05azure windowsTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
5:57aws windowsTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
5:52aws-ucws linuxTestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=direct
5:49gcp linuxTestAccept/bundle/resources/apps/lifecycle-started-omitted/DATABRICKS_BUNDLE_ENGINE=direct
5:48gcp windowsTestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
5:47aws linuxTestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=terraform
5:45azure-ucws linuxTestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=direct
5:41aws linuxTestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
5:36aws-ucws linuxTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
5:33aws windowsTestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=terraform
5:28gcp windowsTestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
5:26aws-ucws windowsTestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=terraform
5:12azure-ucws windowsTestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
5:10gcp windowsTestAccept/bundle/resources/apps/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
5:07aws windowsTestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=direct
5:06azure-ucws linuxTestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct

deco-sdk-taggingBot added a commit that referenced this pull request Jul 15, 2026
## Release v1.8.0
### Notable Changes
* Auto-migrate a bundle from terraform to the direct engine when `bundle.engine` is `"direct"` (or `DATABRICKS_BUNDLE_ENGINE=direct`) and the post-deploy dry-run migration is clean; a warning is emitted if the dry-run surfaces errors or warnings so the automatic migration is skipped.
### CLI
* experimental `ssh connect`: bare `python`/`pip` in an interactive session now resolve to the environment interpreter (`$DATABRICKS_VIRTUAL_ENV`) instead of the system or cluster-libraries interpreter, so packages installed in the environment are importable without extra setup. The interactive shell is now non-login (`bash -i`) and the server seeds a `~/.bashrc` snippet that re-prepends the environment's bin directory to `PATH` ([#5888](#5888)).
* When Claude Code runs the CLI without the Databricks AI tooling installed, the CLI now prints a one-line recommendation on stderr to run `databricks aitools install`. The recommendation is shown at most once per hour per Claude session, and never for human callers or `aitools` commands.
* Fixed `databricks auth describe` misattributing a profile selected via `DATABRICKS_CONFIG_PROFILE` as `(from bundle)` when run inside a bundle root ([#5904](#5904)).
### Bundles
* `bundle generate` now warns when the generated configuration file is not matched by any pattern in the `include` section of `databricks.yml` ([#5868](#5868)).
* direct: Match UC Auto Upgrade managed property defaults with a wildcard pattern instead of enumerating each key ([#5877](#5877)).
* Recognize `ssh://` template URLs in `databricks bundle init` ([#5891](#5891)).
* `databricks bundle init` now reports an actionable error when given a template URL with an unsupported protocol (`http://`, `git://`, `ftp://`, `ftps://`) instead of failing with a confusing "not a bundle template" message ([#5902](#5902)).
* Added an `env:` section to `scripts.<name>` for declaring environment variables that may reference `${bundle.*}`, `${workspace.*}`, and `${var.*}` ([#4179](#4179), [#5299](#5299)).
Sign up for freeto 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.

3 participants

@anton-107@eng-dev-ecosystem-bot@rugpanov