google-colab-cli 0.7.5__tar.gz → 0.7.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/AGENTS.md +2 -2
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/CHANGELOG.md +27 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/PKG-INFO +2 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/01_session_management.md +14 -2
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/02_execution_and_interactive.md +3 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/03_file_management.md +2 -2
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/04_automation_and_utility.md +26 -6
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/05_run_command.md +2 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/06_ssh_access.md +87 -16
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/README.md +11 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/repro_bundled_oauth/test.sh +8 -18
- google_colab_cli-0.7.6/integration/repro_legacy_token_migration/test.sh +136 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/repro_piped_console/test.sh +5 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/repro_run_command/test.sh +5 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/repro_ssh/test.sh +19 -1
- google_colab_cli-0.7.6/integration/repro_windows_ssh/test.py +120 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/pyproject.toml +1 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/skills/colab-operator/SKILL.md +1 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/auth.py +150 -11
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/cli.py +5 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/automation.py +16 -4
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/execution.py +12 -3
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/run.py +17 -4
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/session.py +37 -11
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/ssh.py +169 -40
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/common.py +54 -4
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/contents.py +3 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/history.py +5 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/runtime.py +114 -41
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/state.py +19 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/utils.py +15 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/conftest.py +17 -0
- google_colab_cli-0.7.6/tests/mock_kernel.py +105 -0
- google_colab_cli-0.7.6/tests/test_auth.py +659 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_auth_adc.py +27 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_automation.py +25 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_cli.py +26 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_contents.py +37 -0
- google_colab_cli-0.7.6/tests/test_local_credentials.py +255 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_run.py +24 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_runtime.py +63 -35
- google_colab_cli-0.7.6/tests/test_runtime_options.py +79 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_ssh.py +193 -23
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_ssh_autocreate.py +12 -3
- google_colab_cli-0.7.6/tests/test_ssh_cmd.py +203 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_ssh_lifecycle.py +2 -1
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_ssh_wire_contract.py +147 -4
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_ssh_workdir.py +19 -0
- google_colab_cli-0.7.6/tests/test_streaming.py +164 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_utils.py +23 -1
- google_colab_cli-0.7.6/tests/test_windows_ssh.py +165 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/uv.lock +151 -26
- google_colab_cli-0.7.5/tests/test_auth.py +0 -158
- google_colab_cli-0.7.5/tests/test_streaming.py +0 -98
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/.githooks/pre-commit +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/.gitignore +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/.pre-commit-config.yaml +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/.python-version +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/CONTRIBUTING.md +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/LICENSE +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/README.md +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/cloudbuild.yaml +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/3042ab12-2026-05-07.png +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/demo.webm +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/docs/demos.md +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/examples/finetune_run.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/repro_plot_redirection/test.sh +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/integration/repro_variable_persistence/test.sh +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/auto_update.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/client.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/__init__.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/files.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/usage.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/commands/utility.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/console.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/consumption.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/converter.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/oauth_config.json +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/src/colab_cli/repl.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_cli_log.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_client.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_common.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_console.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_consumption.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_consumption_client.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_exec.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_history.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_ipynb_exec.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_log_export.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_pay.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_readme.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_repl.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_resolution_logic.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_state.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_update.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_url.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_version.py +0 -0
- {google_colab_cli-0.7.5 → google_colab_cli-0.7.6}/tests/test_whoami.py +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
- **Common**: `common.py` centralizes shared `State` (lazy-loading) and session resolution.
|
|
6
6
|
- **Client**: `ColabClient` handles API interactions (assignment, unassignment).
|
|
7
7
|
- **Auth**: `auth.py` exposes a single `get_credentials(config_path, provider)` facade that dispatches on the `AuthProvider` enum. Two providers are supported, selected via the global `--auth=oauth2|adc` flag (default `oauth2`):
|
|
8
|
-
- `oauth2`: public `google-auth-oauthlib` `InstalledAppFlow`, token cached
|
|
8
|
+
- `oauth2`: public `google-auth-oauthlib` `InstalledAppFlow`, token cached in the OS keyring via the Python `keyring` library (`service_name="google-colab-cli"`, `username="oauth_token"`), falling back to `_run_remote_flow` if the keyring is not accessible or locked (keyring failures are echoed to stderr; logs alone go to a file; on Linux, `_check_linux_keyring_locked` checks `secretstorage` beforehand and warns immediately when the `SecretService` collection is locked). A pre-keyring `~/.config/colab-cli/token.json` (`LEGACY_TOKEN_PATH`) is migrated into the keyring and deleted only after a successful keyring write, or immediately when the keyring already holds a token. Integration scripts assume no legacy file exists and detect OAuth2 availability via a keyring-only `has_oauth2_token` helper; the migration path is covered by `integration/repro_legacy_token_migration/test.sh` (isolated HOME + file-backed test keyring). Tests that must ignore the host keyring set `PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring`. Reads the client OAuth config from `-c/--client-oauth-config` (default `~/.colab-cli-oauth-config.json`), falling back to the **bundled** `src/colab_cli/oauth_config.json` resource (re-added in PR #41 / `9f44fe2`, 2026-05-29 — the earlier "removed in `20eb88e`" note was stale/incorrect; the file exists and `auth.py:_get_google_auth_credentials` loads it via `importlib.resources`). As of 2026-06-11 the flow is a **remote copy-paste flow**, not a localhost server: `_run_remote_flow` sets `redirect_uri=https://sdk.cloud.google.com/applicationdefaultauthcode.html` + `token_usage=remote`, prints the URL, and reads the pasted code via `input()`. NEVER revert to OOB (`urn:ietf:wg:oauth:2.0:oob`) — Google blocked it in 2022 ("OOB flow has been blocked"); the `sdk.cloud.google.com` redirect is registered only to the bundled cloud-SDK client (`764086051850-...`), so any other client id gets `redirect_uri_mismatch`. Server-side acceptance/rejection of these variants is verifiable GET-only by building the authorization URL and inspecting whether Google reaches sign-in vs. an OAuth error page (no resources allocated).
|
|
9
9
|
- `adc`: Google Application Default Credentials via `google.auth.default()`. The CLI passes `scopes=PUBLIC_SCOPES` (which includes `colaboratory`) and re-applies via `creds.with_scopes()` for credential types that support it. **User credentials minted by `gcloud auth application-default login` ignore the `scopes=` kwarg AND raise `NotImplementedError` on `with_scopes`**: ADC users must explicitly re-authenticate with `gcloud auth application-default login --scopes=openid,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/colaboratory`. `userinfo.email` is required by the session backend at `colab.research.google.com` (assign/unassign/sessions return 401 without it); `colaboratory` is retained for forward compatibility and other Colab features; `openid` and `cloud-platform` are mandated by `gcloud` itself, which rejects scope lists that omit `cloud-platform` with `Invalid value for [--scopes]`. Service-account / GCE / GKE / impersonated creds get the right scopes transparently via `with_scopes`.
|
|
10
10
|
- **Backend Hosts**: Two distinct backends with different requirements:
|
|
11
11
|
- `colab.research.google.com` (session backend / `tun/m/...`): accepts the `userinfo.email` scope. Handles assign, unassign, CCU info, and the contents API.
|
|
@@ -71,7 +71,7 @@ The package version is derived from the git tag via `hatch-vcs` (see `pyproject.
|
|
|
71
71
|
12. **Amend Safety**: Before `git commit --amend`, explicitly verify all three preconditions: (a) the user requested amend OR a pre-commit hook auto-modified files for an otherwise-successful commit, (b) HEAD was created by you in this conversation (`git log -1 --format='%an %ae'`), (c) the commit has not been pushed to a remote. If any precondition fails, create a new commit instead. NEVER amend a failed/rejected commit — fix the issue and create a new one.
|
|
72
72
|
13. **Run Integration Tests Yourself**: Re-read AGENTS.md "Agent Execution Limitations" before claiming you can't run a test. The CANNOT-run list is exclusively interactive commands (`colab auth`, `colab drivemount`, `colab repl`, `colab console`). Tests built on `colab new` / `colab stop` / `colab log` are non-interactive and you MUST run them yourself before declaring a fix complete. Encoded after running through a full implement-and-document cycle for a fix that the integration test would have falsified in 30 seconds — the cure was `uv run bash integration/repro_run_command/test.sh`.
|
|
73
73
|
14. **Heed Research Caveats**: When a research subagent surfaces a caveat ("the proto allows it but the policy may reject"), treat it as a TODO to verify, not as a footnote. The validation pattern: shell out to the actual service with the proposed inputs and confirm the response matches expectations BEFORE writing the code that depends on it. For policy-gated paths, run a one-shot probe before committing to a design.
|
|
74
|
-
15. **Two distinct auths — never confuse them**: This codebase has two unrelated authentication concerns: (a) **CLI-to-Colab-control-plane**: how `Client` authenticates HTTP requests to `colab.research.google.com` and `colab.pa.googleapis.com`. Driven by the global `--auth={oauth2,adc}` flag and `auth.py:get_credentials`. The OAuth2 path is bootstrapped automatically by `google_auth_oauthlib`'s `InstalledAppFlow` on first invocation when
|
|
74
|
+
15. **Two distinct auths — never confuse them**: This codebase has two unrelated authentication concerns: (a) **CLI-to-Colab-control-plane**: how `Client` authenticates HTTP requests to `colab.research.google.com` and `colab.pa.googleapis.com`. Driven by the global `--auth={oauth2,adc}` flag and `auth.py:get_credentials`. The OAuth2 path is bootstrapped automatically by `google_auth_oauthlib`'s `InstalledAppFlow` on first invocation when no token is stored in `keyring` (or `keyring` is inaccessible); **no separate command is needed** — any `--auth=oauth2` invocation (e.g. `colab --auth=oauth2 sessions`) triggers the browser consent flow. (b) **VM-side credentials**: how the `colab auth` subcommand injects user GCP credentials *into* the running Colab kernel so notebook code (e.g. `gcloud`, BigQuery client) can make authenticated calls from inside the VM. This uses the `USE_AUTH_EPHEM='0'` gcloud-fallback path executed on the kernel via `ColabRuntime`. **NEVER tell a user to "run `colab auth`" as a prerequisite for fixing CLI-side auth issues** — they're orthogonal layers and the suggestion misleads.
|
|
75
75
|
16. **Pydantic validation requires a schema**: `_issue_request` accepts an optional `schema=` and historically called `TypeAdapter(schema).validate_python(...)` unconditionally. When `schema=None` (a caller that doesn't care about the response body — e.g. unassign or fire-and-forget endpoints that return `[]`), this raises `pydantic.ValidationError: Input should be None`. Always guard with `if schema is None: return` after the empty-body short-circuit.
|
|
76
76
|
17. **Suggest the branch-diff review command after committing**: The user reviews changes with `git diff main..<branch-name>` (full cumulative diff against `main`, not just the latest commit). After landing one or more commits on a feature branch, ALWAYS suggest the exact command — e.g. "Review with `git diff main..sort-help-commands`" — instead of `git show <sha>` (which only shows a single commit and misses context when a branch has multiple commits). Encoded 2026-05-05 after suggesting `git show 9d9c7da` for a branch the user wanted to review holistically.
|
|
77
77
|
18. **Verify research-tool claims with primary sources**: Research tools and AI assistants can be confidently wrong, especially about edge cases or features outside their training corpus. When such a tool says "X is not used / not parsed / doesn't exist", treat it as a hypothesis to verify, not a fact. Always cross-check against the primary source (the actual code or config) — and when a tool names files, check whether the indirection chain it describes actually exists. The cost of believing the tool when it's wrong is shipping a non-functional feature; the cost of double-checking is small. Encoded 2026-05-05 after a `colab url` first-cut shipped the wrong URL format because of unverified output.
|
|
@@ -8,6 +8,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
The package version is derived from the git tag via `hatch-vcs`; each release
|
|
9
9
|
below corresponds to a tag of the same name.
|
|
10
10
|
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **ssh:** Every `colab ssh` WebSocket now sends `X-Colab-Client-Agent: colab-cli` and the query values `colab-client-agent=colab-cli` and `colab-client-agent=ssh-mode-<mode>`, where the mode is `interactive` for plain `colab ssh`, `cmd` for `colab ssh -- CMD`, and `proxy` otherwise.
|
|
16
|
+
- **ssh:** When the server rejects a connection with HTTP 429 and reports its
|
|
17
|
+
SSH session limit, `colab ssh` now says the limit is reached (N of N sessions
|
|
18
|
+
in use on the runtime) and that the limit depends on the Colab plan. A 429
|
|
19
|
+
without a usable limit keeps the previous message.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **ssh:** `colab ssh [-s NAME] -- CMD` runs one command on an existing runtime and exits with its status. Output is byte-exact when piped (a PTY only when stdin and stdout are terminals), and it never creates a runtime. Its handshake is tagged `ssh-mode-cmd`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **ssh:** `colab --config FILE ssh` and `colab --auth adc ssh` now keep those options in the ProxyCommand that `colab ssh` starts, so it uses the same sessions file and auth provider instead of the defaults.
|
|
28
|
+
- **security:** Create the debug log (`~/.config/colab-cli/colab.log`),
|
|
29
|
+
`sessions.json`, `settings.json` and session history files readable only by
|
|
30
|
+
their owner, and tighten copies that older versions left readable by others
|
|
31
|
+
the next time the CLI uses them.
|
|
32
|
+
- **security:** Stop writing access tokens, runtime proxy tokens, OAuth codes,
|
|
33
|
+
client secrets, auth headers and cookies to the debug log. Logs written by
|
|
34
|
+
older versions may contain credentials and can be deleted; if other users
|
|
35
|
+
could read an old log, revoke the CLI's access in your Google Account and
|
|
36
|
+
sign in again.
|
|
37
|
+
|
|
11
38
|
## [0.7.1] - 2026-09-14
|
|
12
39
|
|
|
13
40
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-colab-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
4
4
|
Summary: CLI for interacting with Colab.
|
|
5
5
|
Project-URL: Homepage, https://github.com/googlecolab/google-colab-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/googlecolab/google-colab-cli
|
|
@@ -22,6 +22,7 @@ Requires-Dist: google-auth-oauthlib>=1.3.0
|
|
|
22
22
|
Requires-Dist: google-auth>=2.49.1
|
|
23
23
|
Requires-Dist: html2text>=2024.2.26
|
|
24
24
|
Requires-Dist: jupyter-kernel-client==0.9.0
|
|
25
|
+
Requires-Dist: keyring>=25.7.0
|
|
25
26
|
Requires-Dist: nbformat>=5.10.4
|
|
26
27
|
Requires-Dist: packaging>=24.0
|
|
27
28
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
log:
|
|
3
|
+
2026-11-04: `--gpu` and `--tpu` now reject unknown values instead of silently selecting `A100` (GPU) or `v6e1` (TPU). `resolve_runtime_options()` uses the new `GPU_ACCELERATORS` / `TPU_ACCELERATORS` maps and raises a `typer.BadParameter` that names the flag and the supported list; `--help` is generated from the same maps so it cannot drift. Validation runs before any assignment request, in both `colab new` and `colab run`.
|
|
4
|
+
2026-11-02: Added `_check_linux_keyring_locked` in `auth.py` to check if the active Linux `SecretService` keyring collection is locked via `secretstorage` before reading or writing OAuth tokens, logging and printing an immediate warning with the collection label, timeout, and `--auth=adc` hint while still attempting the keyring operation.
|
|
5
|
+
2026-10-29: The debug log (`colab.log` and `--logtostderr`) masks access tokens, runtime proxy tokens, OAuth codes, client secrets, auth headers and cookies, including in percent-encoded URLs; `jupyter_kernel_client` logs at INFO so kernel stdin replies are not recorded.
|
|
6
|
+
2026-10-29: `sessions.json`, `settings.json`, `colab.log` and session history files are created with mode 0600, and existing copies are tightened to 0600 the next time the CLI uses them (history files whenever the history logger starts).
|
|
7
|
+
2026-10-21: Stored OAuth2 credentials in the OS native keyring via the `keyring` library instead of `~/.config/colab-cli/token.json`, falling back to `_run_remote_flow` if keyring is inaccessible. Keyring failures are reported on stderr, and an existing `token.json` is migrated into the keyring and then deleted (or deleted outright if the keyring already has a token).
|
|
3
8
|
2026-10-14: Added configurable HTTP request timeouts and bounded allocation retries. Uncertain allocation POSTs are reconciled by GET using the same notebook hash before resubmission.
|
|
4
9
|
2026-09-25: Runtime proxy tokens are now refreshed before expiry. `SessionState` persists `token_expires_at`, and `State.get_session` re-fetches the token via `GET /tun/m/assignments` when it's within 5 minutes of expiring.
|
|
5
10
|
2026-09-25: Removed the keep-alive background daemon and related client/CLI ping functions. VM liveness is automatically maintained by the Colab backend based on kernel activity.
|
|
@@ -30,6 +35,7 @@ Defines the specific hardware model.
|
|
|
30
35
|
- **GPU Accelerators**:
|
|
31
36
|
- `T4`: NVIDIA T4 (standard free-tier GPU).
|
|
32
37
|
- `L4`: NVIDIA L4 (cost-effective modern GPU).
|
|
38
|
+
- `G4`: NVIDIA G4 (entry-level datacenter GPU).
|
|
33
39
|
- `A100`: NVIDIA A100 (high-performance GPU).
|
|
34
40
|
- `H100`: NVIDIA H100 (latest-gen performance GPU).
|
|
35
41
|
- **TPU Accelerators**:
|
|
@@ -52,6 +58,12 @@ The CLI maps user flags to these backend parameters:
|
|
|
52
58
|
- `colab new my-session --high-mem` -> adds `shape=hm` (when supported)
|
|
53
59
|
- `colab new my-session --gpu A100 --high-mem` -> `variant=GPU`, `accelerator=A100`, `shape=hm`
|
|
54
60
|
|
|
61
|
+
`--gpu` accepts `T4`, `L4`, `G4`, `H100`, and `A100`; `--tpu` accepts `v5e1`
|
|
62
|
+
and `v6e1` (case-insensitive). Anything else is rejected with the supported
|
|
63
|
+
list before a VM is requested. Unknown values used to fall back silently to
|
|
64
|
+
`A100`/`v6e1`, which turned a typo in an automation script into a real,
|
|
65
|
+
billable allocation.
|
|
66
|
+
|
|
55
67
|
## Approach
|
|
56
68
|
|
|
57
69
|
### HTTP Timeout and Allocation Recovery
|
|
@@ -86,7 +98,7 @@ end to end; mocked timeouts alone do not establish backend compatibility.
|
|
|
86
98
|
- `nsa`: 1 (Standard flag observed in browser traces, typically for "next-gen session architecture").
|
|
87
99
|
- `variant`: Selected from the list above.
|
|
88
100
|
- `accelerator`: Selected from the list above.
|
|
89
|
-
- **State Persistence**: The response contains a `token` and potentially a backend URL or identifier. We will store this in a local JSON file (default `~/.config/colab-cli/sessions.json`).
|
|
101
|
+
- **State Persistence**: The response contains a `token` and potentially a backend URL or identifier. We will store this in a local JSON file (default `~/.config/colab-cli/sessions.json`), readable only by its owner (mode 0600).
|
|
90
102
|
- Format: `{ "session_name": { "token": "...", "backend_url": "...", "hardware": "..." } }`
|
|
91
103
|
|
|
92
104
|
### 2. Session Status (`colab status`)
|
|
@@ -122,7 +134,7 @@ The runtime proxy token (`runtimeProxyInfo.token`) authenticates every request t
|
|
|
122
134
|
- **Global Flags**:
|
|
123
135
|
- `-c`, `--client-oauth-config`: Path to the client secrets JSON file (default: `~/.colab-cli-oauth-config.json`).
|
|
124
136
|
- `--config`: Path to the session state JSON file (default: `~/.config/colab-cli/sessions.json`).
|
|
125
|
-
- **Token Storage**: Credentials are persisted to `~/.config/colab-cli/token.json` after the
|
|
137
|
+
- **Token Storage**: Credentials are persisted to the OS native keyring via the `keyring` library (`service_name="google-colab-cli"`, `username="oauth_token"`) after the initial flow. If the keyring is inaccessible or locked, `_run_remote_flow` is invoked for the current run and a warning is printed to stderr. A legacy `~/.config/colab-cli/token.json` is migrated into the keyring on first use and deleted after a successful keyring write, or immediately if the keyring already holds a token.
|
|
126
138
|
- Use `requests` for robust HTTP interactions and `pydantic` for schema validation.
|
|
127
139
|
- Handle authentication headers (likely `Authorization: Bearer <token>` or cookies).
|
|
128
140
|
|
|
@@ -5,6 +5,8 @@ log:
|
|
|
5
5
|
2026-05-07: Fixed `print_kitty` (used by `colab exec --output-image` and any image-producing exec) to no-op when `sys.stdout.isatty()` is false. The Kitty Graphics Protocol escape sequence is meaningless when stdout is a file or pipe and was visually corrupting captured output (a multi-KB base64 PNG blob would land in log files, grep targets, or showboat captures). Image bytes are still saved to disk via `handle_image`'s file-write path; only the inline-render attempt is suppressed.
|
|
6
6
|
|
|
7
7
|
2026-06-04: Bumped the default `--timeout` for `colab exec` from 10s to 30s (and the matching `colab run` default) so brief silent tasks are less likely to hit a premature `TimeoutError`. Explicit `--timeout` overrides are unaffected.
|
|
8
|
+
|
|
9
|
+
2026-10-24: `--timeout` for `colab exec` (and `colab run`) is now an idle limit: execution fails only after that many seconds without output, so code that keeps printing is never cut off, and `--timeout 0` means no limit. On 0.8.0/0.9.0 the old deadline was never enforced, so code that is silent for longer than `--timeout` now fails where it used to complete. `colab repl` and `colab install`, which pass no timeout, wait until execution finishes, or until the kernel dies or restarts. None of them busy-loop while waiting. `colab auth` and `colab drivemount` are unchanged. See #183.
|
|
8
10
|
---
|
|
9
11
|
|
|
10
12
|
# Design: Execution and Interactive Interaction (`repl`, `exec`, `console`)
|
|
@@ -28,7 +30,7 @@ Execution involves sending Python code (or shell commands) to the Jupyter kernel
|
|
|
28
30
|
- If file path is local: Read content, send as code.
|
|
29
31
|
- If file path is remote: Execute `!python <path>`.
|
|
30
32
|
- **Multi-Modal Output**: Handle `display_data` messages (e.g., `image/png`, `text/html`). For the CLI, we'll save images to temporary files and print their paths, or if the terminal supports it (e.g., iTerm2), inline them.
|
|
31
|
-
- **Timeout Configuration**: Exposes a `--timeout` flag (default 30s)
|
|
33
|
+
- **Timeout Configuration**: Exposes a `--timeout` flag (default 30s): the longest silence allowed before execution fails. Raise it so long-running silent tasks (like model compilation or data downloading) are not prematurely killed; `0` means no limit.
|
|
32
34
|
|
|
33
35
|
### 3. Console (`colab console`)
|
|
34
36
|
- **Implementation**: Connects directly to the backend terminal endpoint (`/colab/tty`) via WebSockets using `websocket-client`.
|
|
@@ -31,7 +31,7 @@ File management on the Colab VM will be implemented using the Jupyter Contents A
|
|
|
31
31
|
### 3. Downloading Files (`colab download`)
|
|
32
32
|
- **API**: `GET /api/contents/<remote_path>?content=1` (as seen in HAR).
|
|
33
33
|
- **Response**: JSON with `content` field.
|
|
34
|
-
- **Handling**: Decodes content based on `format` (
|
|
34
|
+
- **Handling**: Decodes content based on `format` (`base64`, `json`, or plain `text`) and saves it locally.
|
|
35
35
|
|
|
36
36
|
### 4. Deleting Files (`colab rm`)
|
|
37
37
|
- **API**: `DELETE /api/contents/<remote_path>`.
|
|
@@ -59,4 +59,4 @@ TDD is mandatory for all file management features.
|
|
|
59
59
|
|
|
60
60
|
### 2. Error Cases
|
|
61
61
|
- **Test Case**: Verify 404 responses are correctly caught and presented as a "File not found" error to the user.
|
|
62
|
-
- **Test Case**: Verify correct handling of large file uploads exceeding API limits via kernel streaming.
|
|
62
|
+
- **Test Case**: Verify correct handling of large file uploads exceeding API limits via kernel streaming.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
log:
|
|
3
|
+
2026-11-07: Optimized `_get_adc_credentials` in `auth.py` to stub `google.auth._cloud_sdk.get_project_id` around `google.auth.default()`. When ADC user credentials lack a `quota_project_id`, `google-auth` otherwise spawns `gcloud config get-value project` on every CLI invocation to resolve a `project_id` that `_get_adc_credentials` immediately discards (`_`).
|
|
4
|
+
2026-11-04: `colab drivemount` now opens the Drive consent URL in the system browser (`utils.open_url_in_browser`, also used by `colab pay`/`colab url --open`) instead of only printing it. The URL is still printed first, and a missing browser opener (SSH, containers, CI) is treated as normal: the flow keeps waiting for the user exactly as before. The prompt moved into `commands/automation.py::show_drive_auth_url` so it can be unit-tested.
|
|
5
|
+
2026-11-02: Added `_check_linux_keyring_locked` in `auth.py` to inspect the active Linux `SecretService` collection via `secretstorage` before `get_password`/`set_password`, warning immediately when the keyring is locked so users know why the CLI is waiting on an unlock prompt (and suggesting `--auth=adc`).
|
|
6
|
+
2026-10-29: Session history files are created readable only by the owner (0600), and existing ones are tightened to 0600 whenever the history logger starts.
|
|
7
|
+
2026-10-21: Migrated OAuth2 token persistence in `auth.py` from `~/.config/colab-cli/token.json` to the OS native password storage via the `keyring` library (`KEYRING_SERVICE_NAME = "google-colab-cli"`, `KEYRING_USERNAME = "oauth_token"`). When the keyring is empty, locked, or inaccessible, `_run_remote_flow` is invoked; keyring read/write failures print a warning to stderr without failing the command. An existing `~/.config/colab-cli/token.json` (`LEGACY_TOKEN_PATH`) is used when the keyring has no token, copied into the keyring, and deleted only after the keyring write succeeds; it is also deleted when the keyring already holds a token.
|
|
3
8
|
2026-09-25: Cleaned up references to keep-alive background daemon and pre-flight pings. VM liveness is automatically maintained by the Colab backend based on active kernel activity.
|
|
4
9
|
2026-08-10: Added `colab usage` for account-level compute-unit rate/balance via `GET /tun/m/ccu-info` on the session backend (same bearer token as `colab new`).
|
|
5
10
|
2026-06-11: Replaced the `oauth2` provider's `run_local_server()` (localhost redirect) with a remote copy-paste flow (`_run_remote_flow` in `auth.py`). The CLI now prints an authorization URL built with `redirect_uri=https://sdk.cloud.google.com/applicationdefaultauthcode.html` and `token_usage=remote`, then reads the pasted authorization code via `input()` and exchanges it with `flow.fetch_token(code=...)`. This is the same flow `gcloud auth application-default login` uses and works identically in local and remote/headless/container environments, removing the heuristic of whether to auto-open a browser. Confirmed server-side acceptance with a live GET-only check against the bundled cloud-SDK client (`764086051850-...`); the OOB redirect and a non-bundled client id were both verified to be rejected (`OOB flow has been blocked` / `redirect_uri_mismatch`). Unit tests in `tests/test_auth.py` assert no localhost server is started, the redirect URI + `token_usage=remote` are set, and the pasted code is exchanged.
|
|
@@ -32,7 +37,18 @@ backend, selected via the global `--auth=<provider>` flag:
|
|
|
32
37
|
`token_usage=remote`) using the registered HTTPS landing page
|
|
33
38
|
`https://sdk.cloud.google.com/applicationdefaultauthcode.html`; the user
|
|
34
39
|
signs in, copies the code Google displays, and pastes it back at the
|
|
35
|
-
prompt. The refresh token is cached
|
|
40
|
+
prompt. The refresh token is cached in the OS native keyring via the
|
|
41
|
+
`keyring` library (`service_name="google-colab-cli"`, `username="oauth_token"`),
|
|
42
|
+
falling back to running `_run_remote_flow` if the keyring is not accessible
|
|
43
|
+
or locked. On Linux, `_check_linux_keyring_locked` inspects the active
|
|
44
|
+
`SecretService` collection via `secretstorage` before reading or writing
|
|
45
|
+
and prints an immediate warning if the collection is locked while waiting
|
|
46
|
+
on the unlock prompt. Keyring read/write failures are printed to stderr, since a
|
|
47
|
+
failed write means the user is prompted again on every command. A token
|
|
48
|
+
left at the pre-keyring path `~/.config/colab-cli/token.json` is migrated
|
|
49
|
+
into the keyring on first use and the file is deleted once the keyring
|
|
50
|
+
write succeeds (it is kept if the write fails). If the keyring already
|
|
51
|
+
holds a token, the legacy file is deleted.
|
|
36
52
|
This is the same mechanism `gcloud auth application-default login` uses,
|
|
37
53
|
and it behaves identically on local, remote, headless, and container
|
|
38
54
|
hosts (no auto-opened browser, no bound port). We deliberately do **not**
|
|
@@ -65,9 +81,9 @@ sufficient for this host.
|
|
|
65
81
|
How each provider supplies the scope:
|
|
66
82
|
|
|
67
83
|
- **`oauth2`**: `PUBLIC_SCOPES` already includes `colaboratory`, so the
|
|
68
|
-
InstalledAppFlow consent screen lists it. Existing cached tokens
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
InstalledAppFlow consent screen lists it. Existing cached tokens in the
|
|
85
|
+
keyring that were minted before a scope change must be cleared to trigger a
|
|
86
|
+
fresh consent flow.
|
|
71
87
|
- **`adc`**: `google.auth.default(scopes=PUBLIC_SCOPES)` is called, and for
|
|
72
88
|
credential subclasses that support `with_scopes` (service accounts,
|
|
73
89
|
GCE/GKE metadata, impersonated) we re-apply via `creds.with_scopes(...)`.
|
|
@@ -123,7 +139,10 @@ How each provider supplies the scope:
|
|
|
123
139
|
automatically interacts with the Colab backend
|
|
124
140
|
(`/tun/m/credentials-propagation/`), prompts the user with the Google OAuth
|
|
125
141
|
consent URL if needed, and dispatches the required `colab_reply` message to
|
|
126
|
-
the `stdin` channel to unlock the kernel thread.
|
|
142
|
+
the `stdin` channel to unlock the kernel thread. The consent URL is also
|
|
143
|
+
passed to `webbrowser.open` (see `utils.open_url_in_browser`); when no
|
|
144
|
+
browser opener is available (SSH, containers, CI) the URL stays on screen
|
|
145
|
+
and the flow continues to wait for the user, unchanged.
|
|
127
146
|
- **Timeout**: The kernel is silent (no iopub traffic) the entire time the
|
|
128
147
|
user is OAuthing in their browser. To avoid the upstream 10s
|
|
129
148
|
`jupyter_kernel_client` default raising `TimeoutError` mid-flow, this
|
|
@@ -135,7 +154,8 @@ How each provider supplies the scope:
|
|
|
135
154
|
- **Action**: Capture the session's command history and outputs.
|
|
136
155
|
- **Storage**: Maintain a local JSON-L file of all major operations,
|
|
137
156
|
executions, and stdin interactions in
|
|
138
|
-
`~/.config/colab-cli/history/<session_name>.jsonl
|
|
157
|
+
`~/.config/colab-cli/history/<session_name>.jsonl`, readable only by the
|
|
158
|
+
owner (mode 0600).
|
|
139
159
|
- **Viewing**: `colab log list` and `colab log show <session>`.
|
|
140
160
|
- **Conversion (Planned)**: Future expansion to convert history logs to
|
|
141
161
|
`.ipynb` or `.html`.
|
|
@@ -5,6 +5,7 @@ log:
|
|
|
5
5
|
2026-05-12: Initial design and implementation of `colab run <script.py> [args...]`. Combines `colab new` + `colab exec` + `colab stop` into a single fire-and-forget invocation so a Python file can use `#!/usr/bin/env -S colab run` as a shebang line and execute on a freshly-allocated Colab VM. Adds `--keep` (skip auto-stop), `--gpu` / `--tpu` (passthrough to session creation), `-s/--session` (name the ephemeral session), and propagates the script's exit status (non-zero on any uncaught exception in the kernel). The script's `sys.argv` is re-set inside the kernel to mirror native `python script.py arg1 arg2` semantics, and `__name__` is set to `"__main__"`.
|
|
6
6
|
2026-05-12: Native CPython exit-code semantics for `sys.exit()` / `raise SystemExit(...)` from the script body. The Colab kernel reports a `SystemExit` as `output_type=='error'`, which under the previous logic would have (a) printed the IPython traceback (`An exception has occurred, use %tb...`) and (b) flagged the run as a failure regardless of the integer exit code. Now: `sys.exit()` / `sys.exit(0)` exit 0 silently; `sys.exit(N)` exits N; `sys.exit('msg')` exits 1 (matching CPython). The IPython "To exit: use 'exit', 'quit', or Ctrl-D." UserWarning is filtered via the prelude. Encoded after running `examples/gpu_hello.py` end-to-end and seeing the noisy `SystemExit: 0` traceback at the end of an otherwise-successful GPU run.
|
|
7
7
|
2026-06-04: Bumped the default value of the `--timeout` flag from 10.0s to 30.0s so short-but-silent tasks aren't prematurely killed out of the box. Mirrors the same change for `colab exec`.
|
|
8
|
+
2026-10-24: `--timeout` is now an idle limit: the run fails only after that many seconds without output, so a script that keeps printing can run as long as it needs, and `--timeout 0` means no limit. The CLI waits for kernel messages itself, in blocking slices of up to one second, instead of using `jupyter-kernel-client`'s `execute_interactive`. That deadline covered the whole run: 1.0.2 enforces it, failing runs after 30s even while they print, and 0.8.0/0.9.0 never enforce it, polling at ~100% CPU once it passes. As a result, on 0.8.0/0.9.0 a script that is silent for longer than `--timeout` now fails (and, without `--keep`, its session is stopped) where it used to complete. See #183.
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
# Design: `colab run` — Shebang-Compatible One-Shot Execution
|
|
@@ -33,7 +34,7 @@ colab run [OPTIONS] SCRIPT [SCRIPT_ARGS]...
|
|
|
33
34
|
| `--tpu` | str | None | Same set as `colab new --tpu` (v5e1, v6e1). |
|
|
34
35
|
| `--high-mem` | bool | False | Same as `colab new --high-mem` — request high-RAM when supported. |
|
|
35
36
|
| `--keep` | bool | False | Do **not** stop the session after the script finishes. |
|
|
36
|
-
| `--timeout` | float | 30.0 |
|
|
37
|
+
| `--timeout` | float | 30.0 | Seconds to wait without output before failing, so a hung or silent task doesn't block forever. Raise it for long silent tasks; `0` means no limit. |
|
|
37
38
|
|
|
38
39
|
### Shebang usage
|
|
39
40
|
With `--keep` and `--gpu` baked into the shebang line, an entire one-file workload becomes:
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
log:
|
|
3
|
+
2026-11-09: `--identity/-i` now accepts either a `.pub` public key or a private key path: `_resolve_pubkey` reads the `.pub` file directly when present before falling back to `_pubkey_from_identity` (`ssh-keygen -y -f`), and `_ssh_base_args` strips `.pub` for `ssh -i` when the sibling private key exists on disk.
|
|
4
|
+
2026-10-29: Added `colab ssh [-s NAME] -- CMD`, which runs one command on an existing session and exits with its status (a PTY only when stdin and stdout are terminals; neither it nor its ProxyCommand child creates a runtime). Its handshake carries `ssh-mode-cmd`, and the update banner is skipped for `ssh` when stdout is not a terminal. Added `tests/test_ssh_cmd.py`.
|
|
5
|
+
2026-10-29: HTTP 429 now shows the server's SSH session limit ("N of N SSH sessions in use") when the body carries one.
|
|
6
|
+
2026-10-29: The ProxyCommand that `colab ssh` starts now keeps a non-default global `--auth` and `--config` (as an absolute path), so `colab --config ALT ssh -s S` bridges S from ALT instead of the default sessions file.
|
|
7
|
+
2026-10-29: Every `/colab/ssh` handshake now sends `X-Colab-Client-Agent: colab-cli` and the two `colab-client-agent` query values `colab-cli` and `ssh-mode-<mode>`. The mode is `interactive` for plain `colab ssh`, `cmd` for `colab ssh -- CMD` (both pass it to their ProxyCommand re-invocation through a hidden `--colab-internal-mode` option), and `proxy` otherwise.
|
|
8
|
+
2026-10-20: Fixed Windows SSH ProxyCommand quoting, local null-device selection, and cleanup signal registration. Added native Windows regression and live integration tests; verified the SSH suite on Windows, Linux, and macOS, plus live Windows remote execution and runtime cleanup.
|
|
3
9
|
2026-09-25: Cleaned up keep-alive daemon references in session creation and teardown documentation.
|
|
4
10
|
2026-08-09: Added `--high-mem` passthrough when `colab ssh` auto-creates a runtime (forwards to `colab new --high-mem`).
|
|
5
11
|
2026-07-17: Initial design and implementation of `colab ssh` — client side of SSH-over-WebSocket runtime access. Adds three modes (interactive shell, `-s SESSION`, and `--proxy-mode` OpenSSH ProxyCommand bridge), `--identity/-i` key selection, and per-HTTP-status handshake error messages. Server side is out of scope for this repo; the subcommand is a no-op against runtimes that do not expose the `/colab/ssh` endpoint (surfaces an actionable HTTP 404 message).
|
|
@@ -22,18 +28,18 @@ runtime-proxy token so no separate credential handling is needed.
|
|
|
22
28
|
## User Surface
|
|
23
29
|
|
|
24
30
|
```
|
|
25
|
-
colab ssh [OPTIONS]
|
|
31
|
+
colab ssh [OPTIONS] [-- CMD [ARGS]...]
|
|
26
32
|
```
|
|
27
33
|
|
|
28
34
|
| Flag | Type | Default | Purpose |
|
|
29
35
|
|---|---|---|---|
|
|
30
|
-
| `-s`, `--session` | str | auto | Session to connect to. If omitted, uses your only active session, auto-creates one when you have none, or errors when you have several. |
|
|
36
|
+
| `-s`, `--session` | str | auto | Session to connect to. If omitted, uses your only active session, auto-creates one when you have none (not with `-- CMD`), or errors when you have several. |
|
|
31
37
|
| `--proxy-mode` | bool | False | Act as an OpenSSH `ProxyCommand`-compatible WebSocket↔stdio bridge (reads stdin, writes stdout) for `~/.ssh/config`. Every other flag still applies. |
|
|
32
|
-
| `-i`, `--identity` | str | auto |
|
|
38
|
+
| `-i`, `--identity` | str | auto | SSH key (`.pub` public key or private key path) for the public key sent to Colab. Default: first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`. |
|
|
33
39
|
| `--gpu` | str | None | GPU accelerator for a runtime this command creates (T4, L4, G4, H100, A100). |
|
|
34
40
|
| `--tpu` | str | None | TPU accelerator for a runtime this command creates (v5e1, v6e1). |
|
|
35
41
|
| `--high-mem` | bool | False | Request high-RAM when this command auto-creates a runtime (ignored when connecting to an existing session). |
|
|
36
|
-
| `--rm` | bool | False | Stop the runtime when the session ends. Interactive: only a runtime `colab ssh` auto-created (a reused session is never removed). `--proxy-mode`: the bridged session, on disconnect. |
|
|
42
|
+
| `--rm` | bool | False | Stop the runtime when the session ends. Interactive: only a runtime `colab ssh` auto-created (a reused session is never removed). `--proxy-mode`: the bridged session, on disconnect. Ignored with `-- CMD`. |
|
|
37
43
|
|
|
38
44
|
### `~/.ssh/config` usage
|
|
39
45
|
`--proxy-mode` turns `colab ssh` into a transport any SSH-based tool can drive:
|
|
@@ -53,24 +59,50 @@ where a bare `colab` may not be on `PATH`. External SSH tools run their own
|
|
|
53
59
|
remote command, so to also land in `/content` add `RequestTTY yes` and
|
|
54
60
|
`RemoteCommand cd /content 2>/dev/null; exec bash -l`.
|
|
55
61
|
|
|
62
|
+
### Remote command
|
|
63
|
+
`colab ssh [-s NAME] -- CMD [ARGS]...` runs CMD in `/content` on an existing
|
|
64
|
+
session (never creating one); every word from CMD on goes to CMD, flags too.
|
|
65
|
+
|
|
66
|
+
- One argument is a shell command line (`-- 'ls / | wc -l'`); several are an
|
|
67
|
+
exact argv, so `*`, `~` and `$VAR` in separate arguments are not expanded.
|
|
68
|
+
Scripts can also pipe in: `echo cmd | colab ssh -s NAME -- bash`.
|
|
69
|
+
- `-t` only when stdin and stdout are terminals; otherwise no PTY, so piped
|
|
70
|
+
output is byte-exact. The exit status is CMD's; 255 means an ssh error or a
|
|
71
|
+
remote command killed by a signal (e.g. SIGPIPE from `| head`).
|
|
72
|
+
- A runtime accepts a limited number of concurrent SSH connections; past it
|
|
73
|
+
the server answers 429 and `colab ssh` prints its message.
|
|
74
|
+
- With a terminal on both ends, Ctrl-C reaches the remote command and ssh
|
|
75
|
+
exits 255. Piped, Ctrl-C stops only `colab ssh` (exit 130) and the remote
|
|
76
|
+
command may keep running: stop it with a pattern that cannot match its own
|
|
77
|
+
shell, e.g. `colab ssh -s NAME -- pkill -f '[s]leep 300'`, or `colab stop`.
|
|
78
|
+
- SSH sessions use a minimal environment: Python, pip and `nvidia-smi` work;
|
|
79
|
+
tools such as `gcloud` or `node` may need their directories added to `PATH`.
|
|
80
|
+
|
|
56
81
|
## Behavior
|
|
57
82
|
|
|
58
83
|
1. **Session resolution / auto-create**: With `-s NAME`, resolves that session
|
|
59
84
|
(via `state.resolve_session`, the same helper the other commands use). Bare
|
|
60
85
|
`colab ssh` uses your only active session; with **no** session it auto-creates
|
|
61
|
-
one (mirrors `colab new`: assign → persist `SessionState
|
|
86
|
+
one (mirrors `colab new`: assign → persist `SessionState`; never with
|
|
87
|
+
`-- CMD`); with **multiple**
|
|
62
88
|
it errors and asks you to pick one with `-s`.
|
|
63
89
|
2. **Connect**: Opens the WebSocket to `wss://<netloc>/colab/ssh?colab-runtime-proxy-token=<token>`
|
|
64
90
|
and sends the resolved public key verbatim in the `X-Colab-Ssh-Pubkey` header
|
|
65
91
|
(no transformation -- the bytes the user controls are exactly what the server
|
|
66
92
|
receives). Only `ssh-ed25519` / `ecdsa-sha2-nistp{256,384,521}` keys are
|
|
67
|
-
accepted.
|
|
93
|
+
accepted. It also sends `X-Colab-Client-Agent: colab-cli` and appends
|
|
94
|
+
`&colab-client-agent=colab-cli&colab-client-agent=ssh-mode-<mode>`: `interactive`
|
|
95
|
+
for plain `colab ssh`, `cmd` for `colab ssh -- CMD`, `proxy` for a
|
|
96
|
+
user-written `ProxyCommand` or an IDE.
|
|
68
97
|
3. **Interactive shell**: Spawns the system `ssh` binary with the CLI re-invoked
|
|
69
|
-
as its own `ProxyCommand` (`python -m colab_cli.cli ssh --proxy-mode`), so the
|
|
98
|
+
as its own `ProxyCommand` (`python -m colab_cli.cli ssh --proxy-mode --colab-internal-mode interactive`), so the
|
|
70
99
|
WebSocket bridge and the interactive shell share one code path. It forces a
|
|
71
100
|
PTY (`-t`) and runs `cd /content 2>/dev/null; exec $SHELL -l` so you land in
|
|
72
101
|
`/content` (Colab's working dir) rather than root's home; a missing `/content`
|
|
73
102
|
falls back to the login home.
|
|
103
|
+
On native Windows, the ProxyCommand uses `subprocess.list2cmdline` for
|
|
104
|
+
CreateProcessW argument parsing; POSIX systems continue to use `shlex.join`.
|
|
105
|
+
The local `UserKnownHostsFile` uses `os.devnull` (`nul` on Windows).
|
|
74
106
|
4. **`--proxy-mode` bridge**: Bridges the WebSocket ↔ stdin/stdout for use as an
|
|
75
107
|
OpenSSH `ProxyCommand`. Honors every flag: `-s NAME` creates the session if
|
|
76
108
|
missing (creation output routed to stderr so stdout stays the clean ssh byte
|
|
@@ -80,9 +112,12 @@ remote command, so to also land in `/content` add `RequestTTY yes` and
|
|
|
80
112
|
**SIGHUP** (verified), not just stdin EOF, and Python's default SIGHUP action
|
|
81
113
|
would terminate the process before the teardown `finally` ran — leaking the
|
|
82
114
|
runtime. `--rm` therefore installs
|
|
83
|
-
SIGHUP/SIGTERM/SIGINT
|
|
84
|
-
`finally`. `SIGKILL` cannot be intercepted, so a `kill -9`/hard crash can
|
|
115
|
+
handlers for the available SIGHUP/SIGTERM/SIGINT signals that run the stop,
|
|
116
|
+
idempotent with the `finally`. `SIGKILL` cannot be intercepted, so a `kill -9`/hard crash can
|
|
85
117
|
still leak; a normal disconnect is SIGHUP and is handled.
|
|
118
|
+
Windows has no SIGHUP, so registration skips it and retains SIGTERM/SIGINT.
|
|
119
|
+
Forced process termination on Windows can also bypass cleanup; this change
|
|
120
|
+
does not guarantee teardown after a killed process.
|
|
86
121
|
6. **Error handling**: The WebSocket upgrade maps each common HTTP status to an
|
|
87
122
|
actionable message:
|
|
88
123
|
|
|
@@ -92,17 +127,22 @@ remote command, so to also land in `/content` add `RequestTTY yes` and
|
|
|
92
127
|
| 401 | Token invalid/expired — try `colab new` |
|
|
93
128
|
| 403 | Forbidden — token lacks permission for this action (feature-off returns 404, not 403) |
|
|
94
129
|
| 404 | SSH not exposed on this runtime — SSH is baked in at creation, so run `colab new` |
|
|
95
|
-
| 429 |
|
|
130
|
+
| 429 | Session limit reached: "N of N SSH sessions in use", and the limit depends on the Colab plan; without a usable limit in the body, another `colab ssh` is connected: disconnect it first |
|
|
96
131
|
| 502 | Runtime `sshd` unreachable — runtime may be unhealthy |
|
|
97
132
|
| other / none | Raw status or a network-check hint |
|
|
98
133
|
|
|
134
|
+
7. **Remote command**: `-- CMD` resolves an existing session and runs CMD as
|
|
135
|
+
described under Remote command. Its ProxyCommand child (mode `cmd`) only
|
|
136
|
+
resolves the session too, so neither process creates a runtime.
|
|
99
137
|
|
|
100
138
|
## Testing Strategy (TDD)
|
|
101
139
|
|
|
102
140
|
### Unit tests (`tests/test_ssh.py`)
|
|
103
141
|
1. WebSocket URL construction (`wss` for https, `ws` for http; token query param).
|
|
104
|
-
2. Pubkey resolution — `--identity` (
|
|
105
|
-
|
|
142
|
+
2. Pubkey resolution — `--identity` (reads the matching `.pub` file first,
|
|
143
|
+
whether passed with or without `.pub`, before falling back to `ssh-keygen -y
|
|
144
|
+
-f`) and the `~/.ssh` default scan; missing-key and missing-identity exit
|
|
145
|
+
paths.
|
|
106
146
|
3. The full status→message map (400/401/403/404/429/502/other/none).
|
|
107
147
|
4. Shell quoting for the `ProxyCommand` string.
|
|
108
148
|
5. Session resolution (existing vs missing).
|
|
@@ -111,10 +151,13 @@ remote command, so to also land in `/content` add `RequestTTY yes` and
|
|
|
111
151
|
|
|
112
152
|
### Wire-contract tests (`tests/test_ssh_wire_contract.py`)
|
|
113
153
|
Stands up a loopback WebSocket server and drives the real connect path (no mock)
|
|
114
|
-
to assert the request path, the `colab-runtime-proxy-token` query param,
|
|
115
|
-
`X-Colab-Ssh-Pubkey` header
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
to assert the request path, the `colab-runtime-proxy-token` query param, the
|
|
155
|
+
`X-Colab-Ssh-Pubkey` header, the `X-Colab-Client-Agent` header and the two
|
|
156
|
+
`colab-client-agent` query values reach the wire verbatim. Includes mutation tests
|
|
157
|
+
that fail if `_SSH_PATH`/`_PUBKEY_HEADER`, the client-agent header or a mode value
|
|
158
|
+
drift, real HTTP 400/429 mapping (including a 429 body with a limit) via a genuine
|
|
159
|
+
`WebSocketBadStatusException`, and real CLI child processes for plain `colab ssh`
|
|
160
|
+
(`interactive`) and a user `ProxyCommand` (`proxy`).
|
|
118
161
|
|
|
119
162
|
### Auto-create & proxy-mode tests (`tests/test_ssh_autocreate.py`)
|
|
120
163
|
Bare `colab ssh` create vs reuse vs ambiguous; `--gpu/--tpu` passthrough; `--rm`
|
|
@@ -126,6 +169,11 @@ SIGHUP cleanup handler being installed only under `--rm`.
|
|
|
126
169
|
Interactive `ssh` forces a PTY (`-t`) and runs a `cd /content` remote command
|
|
127
170
|
(host before the command, `2>/dev/null` tolerance for a missing directory).
|
|
128
171
|
|
|
172
|
+
### Remote-command tests (`tests/test_ssh_cmd.py`)
|
|
173
|
+
`-- CMD` forwarding and quoting, the `-t`/`-T` rule, exit status, no
|
|
174
|
+
auto-create in the parent or its ProxyCommand child, `ssh-mode-cmd` at
|
|
175
|
+
`websocket.WebSocket.connect`, closed stdio, and the update-banner gate.
|
|
176
|
+
|
|
129
177
|
### Integration test (`integration/repro_ssh/`)
|
|
130
178
|
Two parts. An offline smoke that always runs (no VM): ``--help`` advertises the
|
|
131
179
|
documented flags, and an unknown session exits 2 with an actionable message. A
|
|
@@ -135,3 +183,26 @@ command over the WebSocket bridge -- exercising the same connect ->
|
|
|
135
183
|
pubkey-header auth -> handshake -> bridge -> remote-exec path as the interactive
|
|
136
184
|
shell, minus the TTY -- asserts the RSA-key rejection, and verifies ``colab
|
|
137
185
|
stop`` leaves no orphan VM.
|
|
186
|
+
|
|
187
|
+
### Windows regression tests (`tests/test_windows_ssh.py`)
|
|
188
|
+
Real Windows child processes run the generated ProxyCommand using a Python
|
|
189
|
+
interpreter installed under a path with spaces. Assertions check exact argv
|
|
190
|
+
preservation for spaces, quotes, shell punctuation, backslashes, Unicode, and
|
|
191
|
+
identity paths. A separate test launches the real Windows OpenSSH client with
|
|
192
|
+
a local stub ProxyCommand and verifies its argv, without contacting Colab.
|
|
193
|
+
Portable tests also check registration without SIGHUP and the local null
|
|
194
|
+
device. Existing POSIX shell quoting and teardown tests remain in the suite.
|
|
195
|
+
|
|
196
|
+
These and the existing SSH tests passed on Windows, Linux, and macOS with
|
|
197
|
+
Python 3.12. This covers SSH compatibility only; interactive console behavior,
|
|
198
|
+
local file editing, and self-update require separate platform validation.
|
|
199
|
+
|
|
200
|
+
`uv run python integration/repro_windows_ssh/test.py` provides a native Windows
|
|
201
|
+
live test using cached OAuth2 credentials. It creates a CPU runtime, executes a
|
|
202
|
+
remote command through the generated ProxyCommand with a key path containing
|
|
203
|
+
spaces, exercises `--rm`, and cleans up the recorded endpoint on failure. Exit
|
|
204
|
+
77 is a prerequisite skip, not a successful live validation.
|
|
205
|
+
|
|
206
|
+
The native Windows live test passed on 2026-10-20 with Python 3.12.14 and the
|
|
207
|
+
Windows OpenSSH client. This validates remote command execution and cleanup
|
|
208
|
+
for this SSH path, not overall native Windows feature parity.
|
|
@@ -16,7 +16,9 @@ End-to-end tests that run against a **live Colab backend** (unlike the mocked un
|
|
|
16
16
|
| `repro_variable_persistence/` | Variables persist across `colab exec` calls in the same session. |
|
|
17
17
|
| `repro_piped_console/` | Fast smoke test (~5s including session creation): `echo cmd \| colab console -s s` runs the command and exits within 30s. Regression test for the 2026-05-07 EOF-handler fix. |
|
|
18
18
|
| `repro_bundled_oauth/` | Fast smoke test (~5s): verifies that the fallback OAuth configuration is loaded and starts the OAuth flow with the default client ID when local config is missing. |
|
|
19
|
+
| `repro_legacy_token_migration/` | Fast smoke test (~5s, no VM): a pre-keyring `~/.config/colab-cli/token.json` is migrated into the keyring and deleted, deleted when the keyring already has a token, and kept (with a stderr warning) when the keyring is inaccessible. Uses a throwaway HOME and file-backed test keyring. |
|
|
19
20
|
| `repro_ssh/` | Fast smoke test (~5s): `--help` advertises the flags and an unknown session exits. Slow soak test (~95s): Live e2e allocates a CPU VM, runs a real remote command over `colab ssh --proxy-mode` |
|
|
21
|
+
| `repro_windows_ssh/` | Native Windows live CPU test: Windows OpenSSH launches the generated ProxyCommand, executes a remote marker command, and releases the recorded runtime. Requires cached OAuth2 credentials; exits 77 if prerequisites are missing. |
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
## Running
|
|
@@ -25,6 +27,15 @@ uv run bash integration/repro_run_command/test.sh
|
|
|
25
27
|
```
|
|
26
28
|
`uv run` ensures the local `colab` entry point is on `PATH`.
|
|
27
29
|
|
|
30
|
+
For the native Windows SSH check, run in PowerShell from the repository:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
uv run python integration/repro_windows_ssh/test.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Authenticate first with `uv run colab --auth=oauth2 sessions`. The Windows test
|
|
37
|
+
uses no Bash or WSL, and preserves unrelated active sessions.
|
|
38
|
+
|
|
28
39
|
## Adding a scenario
|
|
29
40
|
1. Create `repro_<short_description>/`.
|
|
30
41
|
2. Add a script (`.sh` or `.py`) that demonstrates or verifies the issue.
|
|
@@ -17,26 +17,20 @@
|
|
|
17
17
|
#
|
|
18
18
|
# This test verifies that the CLI correctly falls back to using the bundled
|
|
19
19
|
# oauth_config.json containing the default client ID when no local config is present.
|
|
20
|
-
# It simulates a fresh user flow by temporarily hiding existing
|
|
21
|
-
# starting the OAuth flow, and asserting that the printed
|
|
22
|
-
# contains the correct Client ID.
|
|
20
|
+
# It simulates a fresh user flow by temporarily hiding an existing config and
|
|
21
|
+
# the keyring token, starting the OAuth flow, and asserting that the printed
|
|
22
|
+
# authorization URL contains the correct Client ID.
|
|
23
23
|
#
|
|
24
24
|
|
|
25
25
|
set -e
|
|
26
26
|
|
|
27
|
-
TOKEN_PATH="$HOME/.config/colab-cli/token.json"
|
|
28
27
|
CONFIG_PATH="$HOME/.colab-cli-oauth-config.json"
|
|
29
28
|
BACKUP_SUFFIX=".backup.$(date +%s)"
|
|
30
29
|
|
|
31
|
-
TOKEN_BACKUP=""
|
|
32
30
|
CONFIG_BACKUP=""
|
|
33
31
|
|
|
34
32
|
cleanup() {
|
|
35
33
|
echo "[*] Cleaning up backups..."
|
|
36
|
-
if [ -n "$TOKEN_BACKUP" ] && [ -f "$TOKEN_BACKUP" ]; then
|
|
37
|
-
mv "$TOKEN_BACKUP" "$TOKEN_PATH"
|
|
38
|
-
echo "[*] Restored token.json"
|
|
39
|
-
fi
|
|
40
34
|
if [ -n "$CONFIG_BACKUP" ] && [ -f "$CONFIG_BACKUP" ]; then
|
|
41
35
|
mv "$CONFIG_BACKUP" "$CONFIG_PATH"
|
|
42
36
|
echo "[*] Restored .colab-cli-oauth-config.json"
|
|
@@ -44,13 +38,7 @@ cleanup() {
|
|
|
44
38
|
}
|
|
45
39
|
trap cleanup EXIT
|
|
46
40
|
|
|
47
|
-
# 1. Back up existing
|
|
48
|
-
if [ -f "$TOKEN_PATH" ]; then
|
|
49
|
-
TOKEN_BACKUP="${TOKEN_PATH}${BACKUP_SUFFIX}"
|
|
50
|
-
mv "$TOKEN_PATH" "$TOKEN_BACKUP"
|
|
51
|
-
echo "[*] Temporarily backed up token.json to $TOKEN_BACKUP"
|
|
52
|
-
fi
|
|
53
|
-
|
|
41
|
+
# 1. Back up the existing client config if it exists
|
|
54
42
|
if [ -f "$CONFIG_PATH" ]; then
|
|
55
43
|
CONFIG_BACKUP="${CONFIG_PATH}${BACKUP_SUFFIX}"
|
|
56
44
|
mv "$CONFIG_PATH" "$CONFIG_BACKUP"
|
|
@@ -63,7 +51,9 @@ echo "[*] Running 'colab --auth=oauth2 sessions' (expecting to trigger browser f
|
|
|
63
51
|
# We redirect output to a file so we can inspect it.
|
|
64
52
|
OUTPUT_LOG=$(mktemp)
|
|
65
53
|
set +e
|
|
66
|
-
|
|
54
|
+
# The null keyring backend hides any token cached in the host's OS keyring.
|
|
55
|
+
PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring \
|
|
56
|
+
PYTHONUNBUFFERED=1 timeout 5 uv run colab --auth=oauth2 sessions > "$OUTPUT_LOG" 2>&1
|
|
67
57
|
EXIT_CODE=$?
|
|
68
58
|
set -e
|
|
69
59
|
|
|
@@ -76,7 +66,7 @@ echo "----------------------------------------------"
|
|
|
76
66
|
EXPECTED_CLIENT_ID="764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
|
|
77
67
|
|
|
78
68
|
# The command should have printed the authorization URL
|
|
79
|
-
if ! grep -q "
|
|
69
|
+
if ! grep -q "To authorize colab-cli, visit this URL" "$OUTPUT_LOG"; then
|
|
80
70
|
echo "[FAILURE] OAuth prompt message not found in output."
|
|
81
71
|
rm -f "$OUTPUT_LOG"
|
|
82
72
|
exit 1
|