google-colab-cli 0.7.6__tar.gz → 0.8.0__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.6 → google_colab_cli-0.8.0}/AGENTS.md +5 -4
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/CHANGELOG.md +10 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/PKG-INFO +4 -5
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/README.md +3 -4
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/01_session_management.md +2 -1
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/02_execution_and_interactive.md +3 -9
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/06_ssh_access.md +3 -2
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/demos.md +4 -12
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/README.md +0 -1
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/skills/colab-operator/SKILL.md +2 -2
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/execution.py +0 -30
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/converter.py +0 -2
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_cli.py +15 -12
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_exec.py +18 -0
- google_colab_cli-0.7.6/integration/repro_piped_console/test.sh +0 -96
- google_colab_cli-0.7.6/src/colab_cli/console.py +0 -197
- google_colab_cli-0.7.6/tests/test_console.py +0 -261
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/.githooks/pre-commit +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/.gitignore +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/.pre-commit-config.yaml +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/.python-version +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/CONTRIBUTING.md +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/LICENSE +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/cloudbuild.yaml +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/03_file_management.md +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/04_automation_and_utility.md +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/05_run_command.md +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/3042ab12-2026-05-07.png +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/docs/demo.webm +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/examples/finetune_run.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_bundled_oauth/test.sh +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_legacy_token_migration/test.sh +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_plot_redirection/test.sh +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_run_command/test.sh +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_ssh/test.sh +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_variable_persistence/test.sh +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/integration/repro_windows_ssh/test.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/pyproject.toml +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/auth.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/auto_update.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/cli.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/client.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/__init__.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/automation.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/files.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/run.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/session.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/ssh.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/usage.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/commands/utility.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/common.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/consumption.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/contents.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/history.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/oauth_config.json +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/repl.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/runtime.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/state.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/src/colab_cli/utils.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/conftest.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/mock_kernel.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_auth.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_auth_adc.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_automation.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_cli_log.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_client.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_common.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_consumption.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_consumption_client.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_contents.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_history.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ipynb_exec.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_local_credentials.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_log_export.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_pay.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_readme.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_repl.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_resolution_logic.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_run.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_runtime.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_runtime_options.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ssh.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ssh_autocreate.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ssh_cmd.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ssh_lifecycle.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ssh_wire_contract.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_ssh_workdir.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_state.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_streaming.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_update.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_url.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_utils.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_version.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_whoami.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/tests/test_windows_ssh.py +0 -0
- {google_colab_cli-0.7.6 → google_colab_cli-0.8.0}/uv.lock +0 -0
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
## Subcommand Workflows
|
|
39
39
|
- **Session Management**: `new`, `sessions`, `status`, `stop`.
|
|
40
|
-
- **Execution**: `repl`, `exec`, `
|
|
40
|
+
- **Execution**: `repl`, `exec`, `ssh`.
|
|
41
41
|
- **Files**: `ls`, `rm`, `upload`, `download`, `edit`.
|
|
42
42
|
- **Automation**: `auth`, `drivemount`, `install`, `log`, `pay`, `version`, `update`.
|
|
43
43
|
|
|
@@ -69,7 +69,7 @@ The package version is derived from the git tag via `hatch-vcs` (see `pyproject.
|
|
|
69
69
|
10. **Live Probes Allocate Real Resources**: Probing the Colab API to debug an issue creates real, billable assignments — every successful POST `/tun/m/assign` reserves a VM. Prefer GET-only (read) probes whenever possible. For any state-mutating call, (a) record every endpoint you create as you go, and (b) clean up via `client.unassign(endpoint)` (or `colab stop`) before declaring the investigation done. Then verify with `colab sessions` that nothing was orphaned.
|
|
70
70
|
11. **Push Freshness**: The remote may have advanced during a session (other contributors land commits while you work). ALWAYS `git fetch <remote>` immediately before pushing or merging. If `git log main..<remote>/main` is non-empty, reset local `main` to the remote, rebase feature branches onto it, retest, then push. NEVER force-push `main` to recover from divergence.
|
|
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
|
-
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
|
|
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`, unpiped `colab repl`, interactive `colab ssh`). 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
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.
|
|
@@ -83,11 +83,12 @@ As an AI agent operating via non-interactive shell tools (`run_shell_command`),
|
|
|
83
83
|
- **I CAN Run:**
|
|
84
84
|
- Automated tests (`pytest`), linting (`ruff`), and headless execution scripts.
|
|
85
85
|
- Subcommands that don't pause for user input (e.g., `colab new`, `colab status`, `colab stop`, `colab ls`, `colab install`, `colab exec <file.py>`).
|
|
86
|
-
- **Piped `colab repl
|
|
86
|
+
- **Piped `colab repl`**: as of 2026-05-07 it supports piped stdin and exits cleanly on EOF (`echo 'print(1)' | colab repl -s s`). The unpiped, interactive variant still cannot be run autonomously.
|
|
87
|
+
- **One-shot shell commands**: `colab ssh -s s -- cmd` runs `cmd` non-interactively and exits with its status (add `</dev/null` if my shell tool runs commands in a terminal); where ssh can't work, use `echo '!cmd' | colab exec -s s` (each `!` line is its own shell and IPython expands `{expr}` and `$name` in it).
|
|
87
88
|
- Specially crafted mock scripts that simulate timeouts or API calls.
|
|
88
89
|
- **I CANNOT Run (Requires User Assistance):**
|
|
89
90
|
- **`colab auth`**: This command relies on the traditional Gcloud fallback `input_request` (via `USE_AUTH_EPHEM='0'`), which prompts the user via Python's `input()` to click a URL, sign in, and paste back an authorization code. My shell tool will hang indefinitely on this `input()`.
|
|
90
91
|
- **`colab drivemount`**: This command prompts the user via `sys.stdin.readline()` (specifically querying `/dev/tty` to ensure input is captured) to press `Enter` after granting OAuth consent in the browser. My shell tool will timeout/hang waiting for `Enter`.
|
|
91
|
-
- **Interactive (TTY) `colab repl` / `colab
|
|
92
|
+
- **Interactive (TTY) `colab repl` / `colab ssh`**: When stdin is a real terminal these commands open interactive sessions that require real-time keystroke streaming. My shell tools cannot support this. (Piped stdin to `colab repl` or `colab exec` is fine — see above.)
|
|
92
93
|
|
|
93
94
|
Whenever working on interactive commands, I must build the core logic, write mock tests, and explicitly ask the user to run the live test in their terminal to verify success.
|
|
@@ -22,6 +22,16 @@ below corresponds to a tag of the same name.
|
|
|
22
22
|
|
|
23
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
24
|
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- **console:** **BREAKING:** Remove the `colab console` subcommand. Use
|
|
28
|
+
`colab ssh -s NAME` for an interactive shell on the runtime and
|
|
29
|
+
`colab ssh -s NAME -- CMD` for one-shot commands; where ssh can't work, pipe
|
|
30
|
+
`!`-prefixed shell commands to `colab exec -s NAME`. Each `!` line is its own
|
|
31
|
+
shell and IPython expands `{expr}` and `$name` in it, so start a multi-line
|
|
32
|
+
script with `%%bash` and keep untrusted text out of `!` lines. Breaking, so
|
|
33
|
+
it ships in a new major version.
|
|
34
|
+
|
|
25
35
|
### Fixed
|
|
26
36
|
|
|
27
37
|
- **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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-colab-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -54,7 +54,7 @@ Designed to support seamless developer productivity, headless automation, and AI
|
|
|
54
54
|
## Key Features
|
|
55
55
|
|
|
56
56
|
* **Instant VM Provisioning:** Spin up CPU, GPU (T4, L4, G4, H100, A100), or TPU (v5e1, v6e1) runtimes in seconds.
|
|
57
|
-
* **Robust Code Execution:** Run local Python scripts, Jupyter Notebooks (`.ipynb`), or piped `stdin` code; launch interactive REPLs or
|
|
57
|
+
* **Robust Code Execution:** Run local Python scripts, Jupyter Notebooks (`.ipynb`), or piped `stdin` code; launch interactive REPLs or SSH shells (`colab ssh`).
|
|
58
58
|
* **Ephemeral Job Runner (`colab run`):** Provision a fresh VM, execute a local script with forwarded arguments, retrieve output files, and automatically tear down the runtime in a single command.
|
|
59
59
|
* **Activity-Based Liveness:** Sessions stay alive as long as the kernel is active, with the Colab backend automatically maintaining liveness.
|
|
60
60
|
* **Seamless Workspace Automation:** Mount Google Drive, authenticate Google Cloud Platform (GCP) credentials, and install dependencies with high-performance `uv` package management.
|
|
@@ -118,8 +118,7 @@ Run `colab <command> --help` to view specific options, defaults, and detailed he
|
|
|
118
118
|
| `colab run [--gpu GPU] [--tpu TPU] [--high-mem] [--keep] SCRIPT [ARGS...]` | Run a local script on a fresh VM, forwarding arguments, then release it |
|
|
119
119
|
| `colab exec [-s NAME] [-f FILE] [--output-image PATH]` | Execute Python code from stdin, a local `.py` file, or a `.ipynb` notebook |
|
|
120
120
|
| `colab repl [-s NAME] [--output-image PATH]` | Start an interactive Python REPL on the VM (exits cleanly on piped EOF) |
|
|
121
|
-
| `colab
|
|
122
|
-
| `colab ssh [-s NAME] [--proxy-mode] [-i KEY] [--gpu GPU] [--tpu TPU] [--high-mem]` | Open an SSH shell to the runtime over WebSocket, or act as an OpenSSH `ProxyCommand` bridge for IDE remote-dev |
|
|
121
|
+
| `colab ssh [-s NAME] [--proxy-mode] [-i KEY] [--gpu GPU] [--tpu TPU] [--high-mem] [-- CMD ...]` | Open an SSH shell to the runtime over WebSocket, run a one-shot remote command (`-- CMD`), or act as an OpenSSH `ProxyCommand` bridge for IDE remote-dev |
|
|
123
122
|
|
|
124
123
|
### File Operations
|
|
125
124
|
| Command | Description |
|
|
@@ -181,7 +180,7 @@ colab stop -s analysis
|
|
|
181
180
|
## Usage Notes
|
|
182
181
|
|
|
183
182
|
* **Machine shape:** Use `--high-mem` with `colab new`, `colab run`, or `colab ssh` (when auto-creating a runtime) to request a high-RAM machine shape. Requires Colab Pro or Pro+ entitlement for supported accelerators (CPU, T4, A100, etc.). L4 and TPU runtimes ignore this flag because they only offer one shape. Machine shape is shown in `colab sessions` and `colab status`.
|
|
184
|
-
* **TTY Requirements:**
|
|
183
|
+
* **TTY Requirements:** `colab repl` and interactive `colab ssh` expect a local TTY. When running inside automated scripts or pipelines, pipe stdin to `colab repl` (e.g., `echo "print(1)" | colab repl`) to trigger its non-interactive mode, or run one-shot shell commands with `colab ssh -s NAME -- CMD` (e.g., `colab ssh -s NAME -- ls /content`); pass untrusted values as separate arguments (after `--` where the program accepts it), since a single argument is a remote shell line. `colab ssh` needs a local `ssh` client and key, so where it can't work (no client or key, a tool not on the minimal ssh environment's `PATH`, or the runtime's concurrent SSH connections in use), pipe the commands to `colab exec` with IPython's `!` prefix (e.g., `echo '!ls /content' | colab exec`); each `!` line is its own shell and IPython expands `{expr}` and `$name` in it, so start a multi-line script with `%%bash` and keep untrusted text out of `!` lines.
|
|
185
184
|
* **Transparent Code Execution:** When calling `colab exec -f file.py`, the CLI reads the file locally and transmits its content to the remote kernel. You do not need to manually upload files before execution.
|
|
186
185
|
* **Storage & State Paths:** Session tokens and metadata are stored at `~/.config/colab-cli/sessions.json`. Global CLI settings are located at `~/.config/colab-cli/settings.json`. These can be customized or isolated via the global `--config` flag.
|
|
187
186
|
|
|
@@ -17,7 +17,7 @@ Designed to support seamless developer productivity, headless automation, and AI
|
|
|
17
17
|
## Key Features
|
|
18
18
|
|
|
19
19
|
* **Instant VM Provisioning:** Spin up CPU, GPU (T4, L4, G4, H100, A100), or TPU (v5e1, v6e1) runtimes in seconds.
|
|
20
|
-
* **Robust Code Execution:** Run local Python scripts, Jupyter Notebooks (`.ipynb`), or piped `stdin` code; launch interactive REPLs or
|
|
20
|
+
* **Robust Code Execution:** Run local Python scripts, Jupyter Notebooks (`.ipynb`), or piped `stdin` code; launch interactive REPLs or SSH shells (`colab ssh`).
|
|
21
21
|
* **Ephemeral Job Runner (`colab run`):** Provision a fresh VM, execute a local script with forwarded arguments, retrieve output files, and automatically tear down the runtime in a single command.
|
|
22
22
|
* **Activity-Based Liveness:** Sessions stay alive as long as the kernel is active, with the Colab backend automatically maintaining liveness.
|
|
23
23
|
* **Seamless Workspace Automation:** Mount Google Drive, authenticate Google Cloud Platform (GCP) credentials, and install dependencies with high-performance `uv` package management.
|
|
@@ -81,8 +81,7 @@ Run `colab <command> --help` to view specific options, defaults, and detailed he
|
|
|
81
81
|
| `colab run [--gpu GPU] [--tpu TPU] [--high-mem] [--keep] SCRIPT [ARGS...]` | Run a local script on a fresh VM, forwarding arguments, then release it |
|
|
82
82
|
| `colab exec [-s NAME] [-f FILE] [--output-image PATH]` | Execute Python code from stdin, a local `.py` file, or a `.ipynb` notebook |
|
|
83
83
|
| `colab repl [-s NAME] [--output-image PATH]` | Start an interactive Python REPL on the VM (exits cleanly on piped EOF) |
|
|
84
|
-
| `colab
|
|
85
|
-
| `colab ssh [-s NAME] [--proxy-mode] [-i KEY] [--gpu GPU] [--tpu TPU] [--high-mem]` | Open an SSH shell to the runtime over WebSocket, or act as an OpenSSH `ProxyCommand` bridge for IDE remote-dev |
|
|
84
|
+
| `colab ssh [-s NAME] [--proxy-mode] [-i KEY] [--gpu GPU] [--tpu TPU] [--high-mem] [-- CMD ...]` | Open an SSH shell to the runtime over WebSocket, run a one-shot remote command (`-- CMD`), or act as an OpenSSH `ProxyCommand` bridge for IDE remote-dev |
|
|
86
85
|
|
|
87
86
|
### File Operations
|
|
88
87
|
| Command | Description |
|
|
@@ -144,7 +143,7 @@ colab stop -s analysis
|
|
|
144
143
|
## Usage Notes
|
|
145
144
|
|
|
146
145
|
* **Machine shape:** Use `--high-mem` with `colab new`, `colab run`, or `colab ssh` (when auto-creating a runtime) to request a high-RAM machine shape. Requires Colab Pro or Pro+ entitlement for supported accelerators (CPU, T4, A100, etc.). L4 and TPU runtimes ignore this flag because they only offer one shape. Machine shape is shown in `colab sessions` and `colab status`.
|
|
147
|
-
* **TTY Requirements:**
|
|
146
|
+
* **TTY Requirements:** `colab repl` and interactive `colab ssh` expect a local TTY. When running inside automated scripts or pipelines, pipe stdin to `colab repl` (e.g., `echo "print(1)" | colab repl`) to trigger its non-interactive mode, or run one-shot shell commands with `colab ssh -s NAME -- CMD` (e.g., `colab ssh -s NAME -- ls /content`); pass untrusted values as separate arguments (after `--` where the program accepts it), since a single argument is a remote shell line. `colab ssh` needs a local `ssh` client and key, so where it can't work (no client or key, a tool not on the minimal ssh environment's `PATH`, or the runtime's concurrent SSH connections in use), pipe the commands to `colab exec` with IPython's `!` prefix (e.g., `echo '!ls /content' | colab exec`); each `!` line is its own shell and IPython expands `{expr}` and `$name` in it, so start a multi-line script with `%%bash` and keep untrusted text out of `!` lines.
|
|
148
147
|
* **Transparent Code Execution:** When calling `colab exec -f file.py`, the CLI reads the file locally and transmits its content to the remote kernel. You do not need to manually upload files before execution.
|
|
149
148
|
* **Storage & State Paths:** Session tokens and metadata are stored at `~/.config/colab-cli/sessions.json`. Global CLI settings are located at `~/.config/colab-cli/settings.json`. These can be customized or isolated via the global `--config` flag.
|
|
150
149
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
log:
|
|
3
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
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: Removed the `colab console` subcommand; the long-lived connections that skip mid-session token refresh are now `repl` and `ssh`.
|
|
5
6
|
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
7
|
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
8
|
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).
|
|
@@ -123,7 +124,7 @@ end to end; mocked timeouts alone do not establish backend compatibility.
|
|
|
123
124
|
VM liveness is automatically maintained by the Colab backend based on active kernel executions and interactions. The CLI runs no keep-alive daemon. A session persists until explicitly terminated via `colab stop` or until the backend reclaims it due to inactivity.
|
|
124
125
|
|
|
125
126
|
### 6. Runtime Proxy Token Refresh
|
|
126
|
-
The runtime proxy token (`runtimeProxyInfo.token`) authenticates every request to the VM and expires after `tokenExpiresInSeconds` (1 hour). `SessionState.token_expires_at` stores the absolute expiry, computed when the token is received. Commands that talk to the VM load their session through `State.get_session(name)`, which, when the token is missing an expiry or is within 5 minutes of it, calls `GET /tun/m/assignments`, takes the fresh token/URL for the matching endpoint, and persists it. If the endpoint is no longer assigned, the session is pruned. `sync_sessions` also writes back the fresh tokens it already fetches. Long-lived connections (`repl`, `
|
|
127
|
+
The runtime proxy token (`runtimeProxyInfo.token`) authenticates every request to the VM and expires after `tokenExpiresInSeconds` (1 hour). `SessionState.token_expires_at` stores the absolute expiry, computed when the token is received. Commands that talk to the VM load their session through `State.get_session(name)`, which, when the token is missing an expiry or is within 5 minutes of it, calls `GET /tun/m/assignments`, takes the fresh token/URL for the matching endpoint, and persists it. If the endpoint is no longer assigned, the session is pruned. `sync_sessions` also writes back the fresh tokens it already fetches. Long-lived connections (`repl`, `ssh`) only present the token on initial connect and aren't refreshed mid-session.
|
|
127
128
|
|
|
128
129
|
## TODO / Future Work
|
|
129
130
|
- **Backend Sync**: Implement a way to reconcile the local `sessions.json` with the output of `colab sessions`.
|
|
@@ -7,9 +7,11 @@ log:
|
|
|
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
8
|
|
|
9
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.
|
|
10
|
+
|
|
11
|
+
2026-10-29: Removed the `colab console` subcommand and its `/colab/tty` transport (`console.py`, its tests, and `integration/repro_piped_console/`). Use `colab ssh -s NAME` for an interactive shell and `colab ssh -s NAME -- CMD` for one-shot commands, or pipe `!`-prefixed shell commands to `colab exec` where ssh can't work.
|
|
10
12
|
---
|
|
11
13
|
|
|
12
|
-
# Design: Execution and Interactive Interaction (`repl`, `exec
|
|
14
|
+
# Design: Execution and Interactive Interaction (`repl`, `exec`)
|
|
13
15
|
|
|
14
16
|
## Overview
|
|
15
17
|
Execution involves sending Python code (or shell commands) to the Jupyter kernel running on the Colab VM and processing the stream of output messages.
|
|
@@ -32,12 +34,6 @@ Execution involves sending Python code (or shell commands) to the Jupyter kernel
|
|
|
32
34
|
- **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.
|
|
33
35
|
- **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.
|
|
34
36
|
|
|
35
|
-
### 3. Console (`colab console`)
|
|
36
|
-
- **Implementation**: Connects directly to the backend terminal endpoint (`/colab/tty`) via WebSockets using `websocket-client`.
|
|
37
|
-
- **Interactive**: Bypasses the Jupyter kernel entirely to provide a raw, PTY-backed bash session on the Colab VM.
|
|
38
|
-
- **Terminal Management**: Configures `sys.stdin` to raw mode using `termios` and `tty`, passing single characters to the socket and writing raw ANSI escape sequences directly to `sys.stdout.buffer`. Hooks into `SIGWINCH` to communicate local terminal dimensions (`cols`/`rows`) to the remote bash environment so output rendering works perfectly during resizing.
|
|
39
|
-
- **Piped stdin**: Detected via `sys.stdin.isatty()`. When piped, the input characters are forwarded one at a time to the remote pty, and on EOF the client sends `exit\n` and then closes the websocket itself after `PIPED_EOF_GRACE_SECONDS` (0.5s) so the user's shell goodbye text drains back. The remote `/colab/tty` endpoint wraps bash in tmux, which intercepts a bare `\x04` as a literal character — that is why we send `exit\n` rather than Ctrl-D.
|
|
40
|
-
|
|
41
37
|
## Implementation Details
|
|
42
38
|
- **Kernel Management**: `ColabRuntime` (from `colab-agent`) already handles message signing and message types.
|
|
43
39
|
- **Output Streaming**: Continuous polling or asynchronous message handling to provide real-time output.
|
|
@@ -54,6 +50,4 @@ TDD is mandatory for all execution features.
|
|
|
54
50
|
### 2. TTY and Piping
|
|
55
51
|
- **Test Case**: Mock `sys.stdin.isatty()` to verify `colab repl` correctly switches between interactive mode and one-shot piped execution.
|
|
56
52
|
- **Test Case**: Verify large piped inputs are handled without buffer overflow or truncation.
|
|
57
|
-
- **Test Case**: `colab console` with piped stdin sends `exit\n` and calls `ws.close()` on EOF (regression: previously sent `\x04` only and hung).
|
|
58
|
-
- **Test Case**: `colab console` in TTY mode does not synthesize an exit on EOF (the user owns the session lifecycle).
|
|
59
53
|
- **Test Case**: `print_kitty` is a no-op when `sys.stdout.isatty()` is false (regression: previously emitted ANSI/base64 into pipes and files).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
log:
|
|
3
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: Removed the `colab console` subcommand, so `colab ssh` is the interactive shell on the runtime. Dropped console behavior from the Windows validation caveat.
|
|
4
5
|
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
6
|
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
7
|
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.
|
|
@@ -194,8 +195,8 @@ Portable tests also check registration without SIGHUP and the local null
|
|
|
194
195
|
device. Existing POSIX shell quoting and teardown tests remain in the suite.
|
|
195
196
|
|
|
196
197
|
These and the existing SSH tests passed on Windows, Linux, and macOS with
|
|
197
|
-
Python 3.12. This covers SSH compatibility only;
|
|
198
|
-
|
|
198
|
+
Python 3.12. This covers SSH compatibility only; local file editing and
|
|
199
|
+
self-update require separate platform validation.
|
|
199
200
|
|
|
200
201
|
`uv run python integration/repro_windows_ssh/test.py` provides a native Windows
|
|
201
202
|
live test using cached OAuth2 credentials. It creates a CPU runtime, executes a
|
|
@@ -8,7 +8,7 @@ Eleven scenarios that exercise common workflows, plus a final "bridging back to
|
|
|
8
8
|
**Methodology**
|
|
9
9
|
- Auth: `--auth=adc`. To set up: `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`.
|
|
10
10
|
- Accelerator: every session uses **CPU**. Provisioning real accelerators is gated by per-account quota and would not work for most readers; the workflows themselves are accelerator-agnostic, so where a demo's narrative mentions a GPU or TPU the prose flags the substitution.
|
|
11
|
-
- Interactive subcommands — `colab auth`, `colab drivemount`,
|
|
11
|
+
- Interactive subcommands — `colab auth`, `colab drivemount`, interactive `colab ssh`, and unpiped `colab repl` — are **not run** here because they require human interaction at a TTY. Demos that would normally use them include an inline note explaining what they do and the workflow continues with the non-interactive parts.
|
|
12
12
|
- `enable_update_check` is set to `false` in `~/.config/colab-cli/settings.json` for the duration of recording so the daily upgrade banner doesn't pollute output.
|
|
13
13
|
- `PYTHONWARNINGS=ignore` is set in the environment to suppress the ADC quota-project warning that `google.auth` emits on every call from end-user credentials.
|
|
14
14
|
|
|
@@ -138,7 +138,7 @@ uv run colab --auth=adc stop -s trainer
|
|
|
138
138
|
|
|
139
139
|
## Demo 3: Interactive troubleshooting (piped)
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
`colab exec` and `colab repl` accept piped stdin and exit on EOF, so they compose well with shell pipelines and other CLI tools. This demo lists `/content`, creates and removes a scratch file, and then queries free space from a one-shot REPL.
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
144
|
uv run colab --auth=adc new -s debug
|
|
@@ -149,15 +149,7 @@ uv run colab --auth=adc new -s debug
|
|
|
149
149
|
[colab] Session READY.
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
*Note:*
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
echo 'df -h /content' | uv run colab --auth=adc console -s debug 2>&1 | grep -aE 'overlay|/dev/'
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
```output
|
|
159
|
-
overlay 108G 21G 87G 20% /
|
|
160
|
-
```
|
|
152
|
+
*Note:* for an interactive shell on the VM use `colab ssh -s debug` (interactive, so not run here); one-shot shell commands run with `colab ssh -s debug -- df -h /content`, or, where ssh can't work, piped to `colab exec` with IPython's `!` prefix, e.g. `echo '!df -h /content' | colab exec -s debug` (neither is part of this recording).
|
|
161
153
|
|
|
162
154
|
```bash
|
|
163
155
|
uv run colab --auth=adc ls -s debug /content
|
|
@@ -272,7 +264,7 @@ uv run colab --auth=adc stop -s reporter
|
|
|
272
264
|
|
|
273
265
|
## Demo 5: Bulk data via GCS
|
|
274
266
|
|
|
275
|
-
Pull a batch of objects down from a Google Cloud Storage bucket, transform them on the VM, and pull the results back. A full-fidelity workflow is `colab new --gpu L4` -> `colab auth` (so VM-side `gcloud` works) -> `gcloud storage cp gs://bucket/raw/*.jpg /content/images/` (
|
|
267
|
+
Pull a batch of objects down from a Google Cloud Storage bucket, transform them on the VM, and pull the results back. A full-fidelity workflow is `colab new --gpu L4` -> `colab auth` (so VM-side `gcloud` works) -> `gcloud storage cp gs://bucket/raw/*.jpg /content/images/` (as a `!`-prefixed shell command piped to `colab exec`) -> install pillow/torchvision -> process -> download. We skip the auth step here (interactive; the user has to click through OAuth) and substitute synthetic image generation in place of the GCS pull, which keeps the input -> install -> batch-process -> download shape intact.
|
|
276
268
|
|
|
277
269
|
```bash
|
|
278
270
|
uv run colab --auth=adc new -s data-proc
|
|
@@ -14,7 +14,6 @@ End-to-end tests that run against a **live Colab backend** (unlike the mocked un
|
|
|
14
14
|
| `repro_plot_redirection/` | `colab exec` of a matplotlib script with `--output-image` redirection. |
|
|
15
15
|
| `repro_run_command/` | `colab run <script.py>` runs a local script on a fresh VM and cleans up; `--keep` leaves the session alive. |
|
|
16
16
|
| `repro_variable_persistence/` | Variables persist across `colab exec` calls in the same session. |
|
|
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
17
|
| `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
18
|
| `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. |
|
|
20
19
|
| `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` |
|
|
@@ -53,8 +53,8 @@ by running `uv tool install google-colab-cli` or `pip install google-colab-cli`.
|
|
|
53
53
|
- **Piped code**: `echo "print(1)" | colab exec -s <name>` or `cat script.py | colab exec -s <name>`.
|
|
54
54
|
- **Notebooks**: `colab exec -s <name> -f nb.ipynb` runs each code cell and writes results to `<basename>_output.ipynb` next to the input. A `# @title Foo` first line labels the cell in progress output.
|
|
55
55
|
- **Plots/images**: PNG/JPEG outputs are intercepted. Use `--output-image <path>` on `exec`/`repl` to save to a known location (otherwise a temp path is printed). Inline terminal-image escapes are auto-suppressed when stdout isn't a TTY, so piped/captured output stays clean.
|
|
56
|
-
- **Shell**: `
|
|
57
|
-
- **Never run `colab repl`, `colab
|
|
56
|
+
- **Shell**: run a one-shot shell command with `colab ssh -s <name> -- <cmd>`: it runs in `/content` and exits with the command's status. Output is byte-exact when stdout is piped or redirected; ssh allocates a PTY only when stdin and stdout are both terminals, so a shell tool that runs commands in a terminal should add `</dev/null` and redirect the output to a file (a pipe keeps `colab ssh`'s exit status only under `set -o pipefail`). One argument is a shell command line (quote it for pipes or globs: `colab ssh -s <name> -- 'nvidia-smi | head -5'`); several arguments are an exact argv, so pass untrusted values as separate arguments after `--` where the program accepts it (e.g. `colab ssh -s <name> -- ls -- "$f"`), never inside a shell line or a piped script. Pipe a multi-line script to `colab ssh -s <name> -- bash`. Use `colab exec` for Python code. When ssh can't work (no local `ssh` client, no `~/.ssh/id_ed25519` or `id_ecdsa` key and no `-i KEY`, a tool not on the minimal ssh environment's `PATH` such as `gcloud` or `node`, or the runtime's limited concurrent SSH connections in use, which the server answers with 429, e.g. while a person or an IDE is connected), fall back to `exec` with IPython's `!` prefix: `echo '!df -h /content' | colab exec -s <name>` (single quotes stop your local shell expanding `!`). Each `!` line runs in its own shell with no input (a command that prompts hangs), and IPython evaluates `{expr}` as Python even inside quotes and expands `$name` outside single quotes. Start a multi-line script with a `%%bash` line (not with `--env`, which prepends Python lines), use non-interactive flags such as `apt-get -y`, and never put untrusted text in a `%%bash` script or a `!` line (even via `{var}` or `$var`): pass it with `--env NAME=VALUE` and run `subprocess.run(["cmd", "--", os.environ["NAME"]])` from Python. For an interactive shell on the VM, a human runs `colab ssh -s <name>`.
|
|
57
|
+
- **Never run `colab repl`, `colab ssh` without `-- CMD`, `colab auth`, or `colab drivemount` interactively from an agent** — `repl`, `auth` and `drivemount` expect a TTY and will hang or fail, and bare `colab ssh` with no saved session creates a billable runtime; an interactive shell is for a human. `repl` accepts piped stdin and exits on EOF; `auth`/`drivemount` genuinely require a human at the terminal.
|
|
58
58
|
|
|
59
59
|
### Ephemeral one-shot jobs (`colab run`)
|
|
60
60
|
- `colab run [--gpu T4] [--tpu v6e1] [--keep] [-s NAME] script.py [args...]` = `new` + `exec` + `stop` in one command. It provisions a fresh VM, runs the script with `sys.argv` and `__name__ == "__main__"` set like native `python script.py args`, then tears the VM down (unless `--keep`).
|
|
@@ -26,7 +26,6 @@ from typing_extensions import Annotated
|
|
|
26
26
|
|
|
27
27
|
from colab_cli.runtime import ColabRuntime
|
|
28
28
|
from colab_cli.utils import handle_image, is_terminal_error, render_display_data
|
|
29
|
-
from colab_cli.console import connect_console
|
|
30
29
|
|
|
31
30
|
_console = Console()
|
|
32
31
|
|
|
@@ -378,35 +377,6 @@ def repl(
|
|
|
378
377
|
state.store.add(s)
|
|
379
378
|
|
|
380
379
|
|
|
381
|
-
def console(
|
|
382
|
-
session: Annotated[
|
|
383
|
-
Optional[str], typer.Option("-s", "--session", help="Session name")
|
|
384
|
-
] = None,
|
|
385
|
-
):
|
|
386
|
-
"""Connect to raw TTY console"""
|
|
387
|
-
from colab_cli.common import state
|
|
388
|
-
|
|
389
|
-
name = state.resolve_session(session)
|
|
390
|
-
s = state.get_session(name)
|
|
391
|
-
state.history.log_event(s.name, "console_started", {})
|
|
392
|
-
s.running = "console"
|
|
393
|
-
state.store.add(s)
|
|
394
|
-
try:
|
|
395
|
-
connect_console(s)
|
|
396
|
-
except Exception as e:
|
|
397
|
-
if is_terminal_error(e):
|
|
398
|
-
typer.echo(
|
|
399
|
-
f"[colab] Session '{name}' appears to be lost (404/401). Cleaning up."
|
|
400
|
-
)
|
|
401
|
-
state.prune_session(name)
|
|
402
|
-
raise typer.Exit(1)
|
|
403
|
-
raise e
|
|
404
|
-
finally:
|
|
405
|
-
s.running = None
|
|
406
|
-
state.store.add(s)
|
|
407
|
-
|
|
408
|
-
|
|
409
380
|
def register(app: typer.Typer):
|
|
410
381
|
app.command(name="exec")(exec_command)
|
|
411
382
|
app.command()(repl)
|
|
412
|
-
app.command()(console)
|
|
@@ -105,8 +105,6 @@ def convert_history_to_ipynb(
|
|
|
105
105
|
|
|
106
106
|
elif etype == "execution":
|
|
107
107
|
code = event.get("code", "")
|
|
108
|
-
# Check for shell commands (starting with ! or from piped console)
|
|
109
|
-
# If it's a raw shell command from a console pipe, wrap it in %%bash if it doesn't have !
|
|
110
108
|
if event.get("source") == "piped" and not code.startswith("!"):
|
|
111
109
|
code = "%%bash\n" + code
|
|
112
110
|
|
|
@@ -16,7 +16,9 @@ import time
|
|
|
16
16
|
from datetime import datetime, timedelta, timezone
|
|
17
17
|
from unittest.mock import MagicMock, patch
|
|
18
18
|
|
|
19
|
+
import click
|
|
19
20
|
import pytest
|
|
21
|
+
import typer
|
|
20
22
|
from typer.testing import CliRunner
|
|
21
23
|
|
|
22
24
|
from colab_cli.cli import app
|
|
@@ -410,7 +412,7 @@ def _extract_command_names(help_output: str) -> list[str]:
|
|
|
410
412
|
inside the `╭─ Commands ─...` rich box. Returns names in the order they
|
|
411
413
|
appear.
|
|
412
414
|
"""
|
|
413
|
-
lines = help_output.splitlines()
|
|
415
|
+
lines = click.unstyle(help_output).splitlines()
|
|
414
416
|
in_commands = False
|
|
415
417
|
names = []
|
|
416
418
|
for line in lines:
|
|
@@ -463,18 +465,19 @@ def test_cli_no_args():
|
|
|
463
465
|
assert result.exit_code in [0, 2]
|
|
464
466
|
|
|
465
467
|
|
|
466
|
-
def
|
|
467
|
-
|
|
468
|
-
mock_session_state.name = "s1"
|
|
469
|
-
mock_session_state.token = "t1"
|
|
470
|
-
mock_session_state.url = "http://test.com"
|
|
471
|
-
mock_store.get.return_value = mock_session_state
|
|
468
|
+
def test_cli_console_removed():
|
|
469
|
+
assert "console" not in typer.main.get_command(app).commands
|
|
472
470
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
471
|
+
result = runner.invoke(app, ["--help"])
|
|
472
|
+
assert result.exit_code == 0, result.output
|
|
473
|
+
names = _extract_command_names(result.output)
|
|
474
|
+
assert names, result.output
|
|
475
|
+
assert "ssh" in names, result.output
|
|
476
|
+
assert "console" not in names, result.output
|
|
477
|
+
|
|
478
|
+
result = runner.invoke(app, ["console"])
|
|
479
|
+
assert result.exit_code == click.UsageError.exit_code, result.output
|
|
480
|
+
assert "No such command" in result.output, result.output
|
|
478
481
|
|
|
479
482
|
|
|
480
483
|
@patch("colab_cli.commands.files.ContentsClient")
|
|
@@ -87,6 +87,24 @@ def test_cli_exec_stdin(mock_store, mock_runtime_class, mock_common_state):
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def test_cli_exec_stdin_forwards_shell_escape(
|
|
91
|
+
mock_store, mock_runtime_class, mock_common_state
|
|
92
|
+
):
|
|
93
|
+
mock_session = MagicMock()
|
|
94
|
+
mock_session.name = "s1"
|
|
95
|
+
mock_session.kernel_id = None
|
|
96
|
+
mock_session.session_id = None
|
|
97
|
+
mock_store.get.return_value = mock_session
|
|
98
|
+
|
|
99
|
+
mock_common_state.resolve_session.return_value = "s1"
|
|
100
|
+
mock_runtime = mock_runtime_class.return_value
|
|
101
|
+
mock_runtime.execute_code.return_value = []
|
|
102
|
+
|
|
103
|
+
result = runner.invoke(app, ["exec", "-s", "s1"], input="!echo hi")
|
|
104
|
+
assert result.exit_code == 0
|
|
105
|
+
mock_runtime.execute_code.assert_any_call("!echo hi", output_hook=ANY, timeout=30.0)
|
|
106
|
+
|
|
107
|
+
|
|
90
108
|
def test_cli_exec_env_injects_prelude(
|
|
91
109
|
mock_store, mock_runtime_class, mock_common_state
|
|
92
110
|
):
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Copyright 2026 Google LLC
|
|
3
|
-
#
|
|
4
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
# you may not use this file except in compliance with the License.
|
|
6
|
-
# You may obtain a copy of the License at
|
|
7
|
-
#
|
|
8
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
#
|
|
10
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
# See the License for the specific language governing permissions and
|
|
14
|
-
# limitations under the License.
|
|
15
|
-
#
|
|
16
|
-
# Integration Test: Piped console exits cleanly on EOF
|
|
17
|
-
# Verifies that `echo cmd | colab console -s s` runs the command on the remote
|
|
18
|
-
# /colab/tty endpoint and then exits within a few seconds.
|
|
19
|
-
#
|
|
20
|
-
# Regression: prior to 2026-05-07 this hung indefinitely because the EOF
|
|
21
|
-
# handler sent \x04 (Ctrl-D), which the remote tmux-wrapped bash treated as a
|
|
22
|
-
# literal character rather than a session terminator. The fix sends "exit\n"
|
|
23
|
-
# and closes the websocket from the client side after a short grace period.
|
|
24
|
-
|
|
25
|
-
set -e
|
|
26
|
-
|
|
27
|
-
TMP_DIR=$(mktemp -d)
|
|
28
|
-
SESSION_FILE="$TMP_DIR/sessions.json"
|
|
29
|
-
SESSION_NAME="test-piped-console"
|
|
30
|
-
|
|
31
|
-
# OAuth2 is usable if a token is cached in the OS keyring.
|
|
32
|
-
has_oauth2_token() {
|
|
33
|
-
timeout 10 uv run python -c "import sys, keyring; sys.exit(0 if keyring.get_password('google-colab-cli', 'oauth_token') else 1)" > /dev/null 2>&1
|
|
34
|
-
}
|
|
35
|
-
# Auth selection (OAuth2 if token cached, else ADC).
|
|
36
|
-
if has_oauth2_token; then
|
|
37
|
-
AUTH_FLAGS="--auth=oauth2"
|
|
38
|
-
elif command -v gcloud > /dev/null && gcloud auth application-default print-access-token > /dev/null 2>&1; then
|
|
39
|
-
ADC_TOKEN=$(gcloud auth application-default print-access-token 2>/dev/null)
|
|
40
|
-
ADC_SCOPES=$(curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=$ADC_TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin).get('scope',''))" 2>/dev/null)
|
|
41
|
-
if echo "$ADC_SCOPES" | grep -q "userinfo.email"; then
|
|
42
|
-
AUTH_FLAGS="--auth=adc"
|
|
43
|
-
else
|
|
44
|
-
echo "Error: ADC token lacks the userinfo.email scope."
|
|
45
|
-
exit 1
|
|
46
|
-
fi
|
|
47
|
-
else
|
|
48
|
-
echo "Error: No usable auth provider found." >&2
|
|
49
|
-
exit 1
|
|
50
|
-
fi
|
|
51
|
-
|
|
52
|
-
cleanup_session() {
|
|
53
|
-
uv run colab $AUTH_FLAGS --config "$SESSION_FILE" stop -s "$SESSION_NAME" 2>/dev/null || true
|
|
54
|
-
}
|
|
55
|
-
trap "cleanup_session; rm -rf $TMP_DIR" EXIT
|
|
56
|
-
|
|
57
|
-
echo "[*] Creating session for piped-console test using $AUTH_FLAGS..."
|
|
58
|
-
uv run colab $AUTH_FLAGS --config "$SESSION_FILE" new -s "$SESSION_NAME"
|
|
59
|
-
|
|
60
|
-
# Marker string we'll grep for in the captured output. Made unique enough that
|
|
61
|
-
# accidental matches in tmux status lines or shell prompts are impossible.
|
|
62
|
-
MARKER="PIPED-CONSOLE-OK-$(date +%s)-$$"
|
|
63
|
-
|
|
64
|
-
OUTPUT_FILE="$TMP_DIR/console-output.txt"
|
|
65
|
-
|
|
66
|
-
echo "[*] Running: echo 'echo $MARKER' | colab console (must exit within 30s)..."
|
|
67
|
-
START=$(date +%s)
|
|
68
|
-
# 30s upper bound guards against the prior hang regression. The fix typically
|
|
69
|
-
# completes in ~1-2s; anything beyond that means the EOF/close path broke.
|
|
70
|
-
if ! timeout 30 bash -c "echo 'echo $MARKER' | uv run colab $AUTH_FLAGS --config '$SESSION_FILE' console -s '$SESSION_NAME'" > "$OUTPUT_FILE" 2>&1; then
|
|
71
|
-
EXIT=$?
|
|
72
|
-
if [ $EXIT -eq 124 ]; then
|
|
73
|
-
echo "[FAILURE] Piped console hung past the 30s timeout (regression — was the EOF handler reverted?)."
|
|
74
|
-
else
|
|
75
|
-
echo "[FAILURE] Piped console exited non-zero: $EXIT"
|
|
76
|
-
fi
|
|
77
|
-
cat "$OUTPUT_FILE"
|
|
78
|
-
exit 1
|
|
79
|
-
fi
|
|
80
|
-
ELAPSED=$(($(date +%s) - START))
|
|
81
|
-
echo "[*] Piped console returned in ${ELAPSED}s."
|
|
82
|
-
|
|
83
|
-
if ! grep -q "$MARKER" "$OUTPUT_FILE"; then
|
|
84
|
-
echo "[FAILURE] Marker '$MARKER' not found in console output (the command did not actually execute on the VM)."
|
|
85
|
-
cat "$OUTPUT_FILE"
|
|
86
|
-
exit 1
|
|
87
|
-
fi
|
|
88
|
-
echo "[*] Confirmed: the piped command actually ran on the remote VM."
|
|
89
|
-
|
|
90
|
-
# Sanity: the doc claims ~1.2s round-trip. Anything > 10s is suspicious even
|
|
91
|
-
# if it eventually exited (probably means the grace window was bumped wildly).
|
|
92
|
-
if [ "$ELAPSED" -gt 10 ]; then
|
|
93
|
-
echo "[WARNING] Piped console took ${ELAPSED}s — slow, but not a hang."
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
echo "[SUCCESS] Piped console integration test passed (elapsed=${ELAPSED}s)."
|