google-colab-cli 0.5.9__tar.gz → 0.6.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.
Files changed (82) hide show
  1. google_colab_cli-0.6.0/.python-version +1 -0
  2. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/AGENTS.md +5 -5
  3. google_colab_cli-0.6.0/CHANGELOG.md +50 -0
  4. google_colab_cli-0.6.0/COLAB_SKILL.md +79 -0
  5. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/PKG-INFO +24 -2
  6. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/README.md +8 -0
  7. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/01_session_management.md +21 -3
  8. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/02_execution_and_interactive.md +3 -1
  9. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/04_automation_and_utility.md +36 -22
  10. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/05_run_command.md +2 -1
  11. google_colab_cli-0.6.0/docs/demo.webm +0 -0
  12. google_colab_cli-0.6.0/examples/finetune_run.py +153 -0
  13. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_keep_alive_scope/test.sh +16 -10
  14. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/pyproject.toml +26 -1
  15. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/auth.py +41 -4
  16. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/client.py +28 -47
  17. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/automation.py +8 -3
  18. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/execution.py +13 -7
  19. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/run.py +6 -8
  20. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/session.py +20 -17
  21. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/repl.py +11 -10
  22. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/state.py +15 -11
  23. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/utils.py +20 -1
  24. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_auth.py +27 -4
  25. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_client.py +65 -27
  26. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_exec.py +12 -4
  27. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_ipynb_exec.py +2 -2
  28. google_colab_cli-0.6.0/tests/test_state.py +327 -0
  29. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_utils.py +30 -1
  30. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/uv.lock +22 -0
  31. google_colab_cli-0.5.9/.python-version +0 -1
  32. google_colab_cli-0.5.9/COLAB_SKILL.md +0 -42
  33. google_colab_cli-0.5.9/tests/test_state.py +0 -123
  34. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/.githooks/pre-commit +0 -0
  35. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/.gitignore +0 -0
  36. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/.pre-commit-config.yaml +0 -0
  37. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/CONTRIBUTING.md +0 -0
  38. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/LICENSE +0 -0
  39. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/cloudbuild.yaml +0 -0
  40. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/03_file_management.md +0 -0
  41. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/3042ab12-2026-05-07.png +0 -0
  42. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/docs/demos.md +0 -0
  43. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/README.md +0 -0
  44. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_bundled_oauth/test.sh +0 -0
  45. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_keep_alive/test.sh +0 -0
  46. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_piped_console/test.sh +0 -0
  47. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_plot_redirection/test.sh +0 -0
  48. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_run_command/test.sh +0 -0
  49. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/integration/repro_variable_persistence/test.sh +0 -0
  50. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/auto_update.py +0 -0
  51. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/cli.py +0 -0
  52. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/__init__.py +0 -0
  53. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/files.py +0 -0
  54. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/commands/utility.py +0 -0
  55. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/common.py +0 -0
  56. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/console.py +0 -0
  57. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/contents.py +0 -0
  58. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/converter.py +0 -0
  59. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/history.py +0 -0
  60. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/oauth_config.json +0 -0
  61. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/src/colab_cli/runtime.py +0 -0
  62. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/conftest.py +0 -0
  63. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_auth_adc.py +0 -0
  64. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_automation.py +0 -0
  65. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_cli.py +0 -0
  66. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_cli_log.py +0 -0
  67. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_console.py +0 -0
  68. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_contents.py +0 -0
  69. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_history.py +0 -0
  70. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_keep_alive.py +0 -0
  71. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_log_export.py +0 -0
  72. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_pay.py +0 -0
  73. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_readme.py +0 -0
  74. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_repl.py +0 -0
  75. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_resolution_logic.py +0 -0
  76. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_run.py +0 -0
  77. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_runtime.py +0 -0
  78. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_streaming.py +0 -0
  79. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_update.py +0 -0
  80. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_url.py +0 -0
  81. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_version.py +0 -0
  82. {google_colab_cli-0.5.9 → google_colab_cli-0.6.0}/tests/test_whoami.py +0 -0
@@ -0,0 +1 @@
1
+ 3.12
@@ -5,11 +5,11 @@
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 at `~/.config/colab-cli/token.json`. Requires an explicit client OAuth config (`-c/--client-oauth-config`, default `~/.colab-cli-oauth-config.json`); the previously-bundled `oauth_config.json` resource fallback was removed (commit `20eb88e`).
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 required by the `RuntimeService` at `colab.pa.googleapis.com` (keep-alive returns 403 without it); `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`.
8
+ - `oauth2`: public `google-auth-oauthlib` `InstalledAppFlow`, token cached at `~/.config/colab-cli/token.json`. 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
+ - `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/keep-alive return 401 without it); `colaboratory` is retained for forward compatibility and other Colab features (keep-alive no longer uses `colab.pa.googleapis.com` — see the Keep-alive note below); `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
- - `colab.research.google.com` (session backend / `tun/m/...`): accepts the `userinfo.email` scope.
12
- - `colab.pa.googleapis.com` (`RuntimeService`, used by `KeepAliveAssignment`): requires (a) the `colaboratory` OAuth scope, AND (b) `X-Goog-Api-Client` header containing the substring `grpc-web`. Both are enforced server-side; missing either yields HTTP 400 / 403 with descriptive `google.rpc.DebugInfo` payloads. Always log `response_body` on failure for these RPCs to avoid silent debugging.
11
+ - `colab.research.google.com` (session backend / `tun/m/...`): accepts the `userinfo.email` scope. Handles assign, unassign, the contents API, **and keep-alive** (see below).
12
+ - **Keep-alive (2026-06-15, issue #14)**: keep-alive is a **Tunnel Frontend (TFE) HTTP ping** — `GET https://colab.research.google.com/tun/m/<endpoint>/keep-alive/` with header `X-Colab-Tunnel: Google`, authenticated by the user's own Gaia bearer token (same host/credential as `assign`). TFE records `LastActiveTime` before forwarding, refreshing the idle timer. The VM usually doesn't answer on this path, so the request commonly **read-times-out even on success** — `client.keep_alive_assignment` therefore catches `requests.exceptions.ReadTimeout` and treats it as success, while genuine HTTP errors (e.g. 404 for a deleted assignment) propagate. This mirrors the official `colab-vscode` extension's `sendKeepAlive` (`src/colab/client.ts`). **DO NOT revert to the `colab.pa.googleapis.com` `RuntimeService/KeepAliveAssignment` RPC**: that RPC requires the caller to be a `serviceusage` consumer of Colab's internal project `1014160490159`, which no ordinary user account is, so it returned HTTP 403 `USER_PROJECT_DENIED` for every external user (issue #14) and silently idle-pruned their sessions within minutes. The browser only made that RPC work by riding the user's `google.com` cookie through an internal cookie-proxy (`colab.clients6.google.com`), which a headless bearer-token CLI cannot use. Verified live 2026-06-15 with a third-party account (the RPC 403'd; the tunnel ping succeeded and kept the VM alive).
13
13
  - **Runtime**: `ColabRuntime` wraps `jupyter-kernel-client` for execution.
14
14
  - **State**:
15
15
  - `StateStore` persists session metadata in `~/.config/colab-cli/sessions.json`.
@@ -60,7 +60,7 @@
60
60
  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 `~/.config/colab-cli/token.json` doesn't exist; **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.
61
61
  16. **Detached daemons inherit nothing useful**: When spawning a detached child process via `subprocess.Popen` (e.g. `spawn_keep_alive`), the child does NOT inherit the parent's parsed Typer flags. It re-parses argv from scratch, so any global flag the parent saw via `--auth=adc` or `--config /tmp/foo.json` MUST be re-emitted as part of the child's command line. Forgetting this causes silent fallback to Typer defaults: in 2026-04-30 this manifested as the keep-alive daemon (a) using OAUTH2 instead of the parent's ADC, and (b) reading from `~/.config/colab-cli/sessions.json` instead of the parent's `--config` path. Always propagate every relevant global flag in `cmd = [sys.executable, "-m", ...]` BEFORE the subcommand name (Typer requires global flags before the subcommand).
62
62
  17. **Persist-before-spawn for daemons that read shared state**: When a detached child reads from a state file the parent owns (e.g. `state.store.get(session_name)`), the parent MUST persist BEFORE spawning. Otherwise the child can race ahead of the parent's `add()` call and observe an empty store. Symptom in 2026-04-30: keep-alive daemon exits immediately with `keep_alive_stopped reason=session_not_found iters=1 duration=0.0s`. Fix: call `state.store.add(s)` once before `spawn_keep_alive`, and again after (to capture the PID).
63
- 18. **`colab.pa.googleapis.com` rejects ADC user creds without `X-Goog-User-Project`**: When calling `colab.pa.googleapis.com` from ADC user credentials minted by `gcloud auth application-default login`, the bearer token carries the user's gcloud quota project. The colab API key sent alongside it belongs to a different project (Colab, `1014160490159`). The backend enforces project-match between the two and returns HTTP 400 `CONSUMER_INVALID` ("The API Key and the authentication credential are from different projects."). Fix: send `X-Goog-User-Project: 1014160490159` to pin the consumer project to Colab's. Any signed-in user has implicit access to the colab project via the public web client, so this works for ordinary user accounts. Service-account / GCE / GKE / impersonated creds don't hit this because their bearer token IS owned by a project that matches.
63
+ 18. **[SUPERSEDED 2026-06-15 — keep-alive no longer calls `colab.pa.googleapis.com`; see issue #14 / the Keep-alive note above] `colab.pa.googleapis.com` rejects ADC user creds without `X-Goog-User-Project`**: When calling `colab.pa.googleapis.com` from ADC user credentials minted by `gcloud auth application-default login`, the bearer token carries the user's gcloud quota project. The colab API key sent alongside it belongs to a different project (Colab, `1014160490159`). The backend enforces project-match between the two and returns HTTP 400 `CONSUMER_INVALID` ("The API Key and the authentication credential are from different projects."). The old fix was to send `X-Goog-User-Project: 1014160490159` to pin the consumer project — but that in turn required `serviceusage.serviceUsageConsumer` on project `1014160490159`, which ordinary users lack, yielding HTTP 403 `USER_PROJECT_DENIED` (the issue #14 root cause). Both failure modes are now moot because keep-alive uses the TFE tunnel ping on `colab.research.google.com` instead. Retained here as institutional knowledge: if any future code path must call `colab.pa.googleapis.com` with a bearer token, it will face this same project-entitlement wall for non-internal accounts.
64
64
  19. **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. fire-and-forget RPCs like `KeepAliveAssignment` that return `[]`), this raises `pydantic.ValidationError: Input should be None`. Always guard with `if schema is None: return` after the empty-body short-circuit.
65
65
  20. **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.
66
66
  21. **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.
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ The package version is derived from the git tag via `hatch-vcs`; each release
9
+ below corresponds to a tag of the same name.
10
+
11
+ ## [0.6.0] - 2026-06-16
12
+
13
+ ### Changed
14
+
15
+ - **auth:** OAuth2 login now uses a remote copy-paste flow instead of a
16
+ localhost callback server. The CLI prints an authorization URL with
17
+ `redirect_uri=https://sdk.cloud.google.com/applicationdefaultauthcode.html`
18
+ and `token_usage=remote`, then reads the pasted code from stdin. This works
19
+ in headless/remote environments where a browser cannot reach a local
20
+ callback port. (#54)
21
+
22
+ ### Added
23
+
24
+ - **display output:** Rich rendering for `display_data` output via a shared
25
+ `render_display_data()` helper. HTML is converted with `html2text` and
26
+ rendered as Markdown, following a `text/markdown > text/html > text/plain`
27
+ priority; `text/plain` is wrapped with `Text.from_ansi` to preserve embedded
28
+ ANSI escapes. Applied consistently across `exec`, `console`/`repl`, and
29
+ automation call sites. (#58)
30
+
31
+ ### Fixed
32
+
33
+ - **keep-alive:** Replace the `RuntimeService/KeepAliveAssignment` RPC on
34
+ `colab.pa.googleapis.com` with a Tunnel Frontend (TFE) HTTP ping
35
+ (`GET /tun/m/<endpoint>/keep-alive/` with `X-Colab-Tunnel: Google`) on
36
+ `colab.research.google.com`, authenticated by the user's own bearer token.
37
+ The old RPC required `serviceusage` consumer access to Colab's internal
38
+ project and returned HTTP 403 `USER_PROJECT_DENIED` for every external user,
39
+ causing their sessions to be idle-pruned within minutes. The TFE ping needs
40
+ no project entitlement; because the VM often does not answer on this path, a
41
+ `ReadTimeout` is treated as success while genuine HTTP errors propagate.
42
+ (#14, #61)
43
+
44
+ ### Removed
45
+
46
+ - Dead grpc-web client-registry / API-key code path and the now-irrelevant
47
+ `colaboratory`-scope / `pa.googleapis.com` pre-flight remediation messaging,
48
+ superseded by the TFE keep-alive ping. (#61)
49
+
50
+ [0.6.0]: https://github.com/googlecolab/google-colab-cli/compare/v0.5.11...v0.6.0
@@ -0,0 +1,79 @@
1
+ # Skill: Colab Session Operator
2
+
3
+ Operate Google Colab environments via the `colab` CLI: provision GPU/TPU sessions, run Python/shell on the VM, sync files, and capture work as notebooks.
4
+
5
+ ## When to activate
6
+ - Creating or managing TPU/GPU sessions.
7
+ - Running Python or shell on a remote Colab VM.
8
+ - Syncing files between local and remote.
9
+ - Automating environment setup (packages, auth, Drive).
10
+ - Exporting session history as a Jupyter notebook.
11
+
12
+ ## Mental model (read this first)
13
+ - **A session == a live Jupyter kernel on a rented VM.** `colab new` allocates a billable VM; `colab stop` releases it. Nothing reclaims it automatically except a 24h keep-alive cap, so an unstopped session burns compute units indefinitely.
14
+ - **Kernel state PERSISTS across `colab exec` / `colab repl` calls in the same session.** Each invocation reattaches to the *same* kernel (the kernel ID is cached in local state) and only closes the websocket on exit — it does **not** shut the kernel down. So imports, variables, and defined functions survive between separate `colab exec` commands. Build up state incrementally; don't re-import everything each call. (`colab stop` and `colab restart-kernel` are what actually reset it.)
15
+ - **Default working directory is `/content`.** Every `exec`/`repl`/`run` `cd`s there first; prefer absolute paths (`/content/...`) for file work. For `colab ls/rm/upload/download`, absolute `/content/...` paths work and the default `ls` path is `content` (VM root).
16
+ - **`colab` is fire-and-forget.** Each command authenticates, does one thing, and exits. A detached background daemon (spawned by `colab new`) handles keep-alive; you don't manage it.
17
+
18
+ ## Authentication (the #1 thing that blocks agents)
19
+ - The global flag is `--auth={adc,oauth2}` and the **default is `adc`** (Application Default Credentials). It must come *before* the subcommand: `colab --auth=adc new -s x`.
20
+ - **ADC setup** (most reliable for headless/agent use). The Colab backends need a specific scope set, so re-mint ADC with all four scopes:
21
+ ```bash
22
+ gcloud auth application-default login \
23
+ --scopes=openid,\
24
+ https://www.googleapis.com/auth/cloud-platform,\
25
+ https://www.googleapis.com/auth/userinfo.email,\
26
+ https://www.googleapis.com/auth/colaboratory
27
+ ```
28
+ Why all four: `userinfo.email` (session backend `colab.research.google.com`, else 401), `colaboratory` (RuntimeService `colab.pa.googleapis.com` keep-alive, else 403), `openid`+`cloud-platform` (mandated by gcloud itself; it rejects scope lists missing `cloud-platform`).
29
+ - **oauth2 setup**: `colab --auth=oauth2 <anything>` triggers a browser consent flow on first use (token cached at `~/.config/colab-cli/token.json`). Requires a client config at `~/.colab-cli-oauth-config.json` (or `-c PATH`). The browser step means it usually needs a human; prefer ADC for agents.
30
+ - **Verify auth in one shot**: `colab sessions` (read-only, lists server assignments) or `colab whoami` (hidden debug command: prints the active email, scopes, audience, and expiry). When any call 403s against `colab.pa.googleapis.com`, the cause is almost always a missing scope — `colab whoami` shows it instantly.
31
+ - **`colab new` pre-flights the keep-alive RPC** right after allocating. If your token lacks the `colaboratory` scope it unassigns the fresh VM (so you don't leak a billable assignment) and prints the exact remediation. Follow that message rather than retrying blindly.
32
+ - **Do NOT confuse `colab auth` with CLI authentication.** `colab auth` injects *VM-side* GCP credentials into the running kernel (so notebook code can call BigQuery/GCS); it is orthogonal to how the CLI itself authenticates. Never suggest "run `colab auth`" to fix a CLI 401/403 — that's a scope/identity problem fixed via the `gcloud` command above.
33
+
34
+ ## Workflow
35
+
36
+ ### Provision
37
+ - `colab new -s <name>` (CPU). Add `--gpu A100` or `--tpu v6e1` for accelerators. **Always pass `-s <name>`** — an omitted name is auto-generated as a random 6-hex string, which makes later commands ambiguous.
38
+ - Supported `--gpu`: `T4`, `L4`, `G4`, `H100`, `A100`. Supported `--tpu`: `v5e1`, `v6e1`.
39
+ - **Gotcha**: an unrecognized `--gpu` value silently falls back to **A100** (which then usually fails the next step). A `400` on `colab new` with an accelerator means no quota/entitlement for it on this account — fall back to `--gpu T4` or omit the flag for CPU.
40
+ - Accelerator availability is tier-gated; most accounts can only get CPU. Don't assume a GPU/TPU will allocate.
41
+
42
+ ### Execute
43
+ - **Preferred**: `colab exec -s <name> -f <script.py>` runs a local script on the remote VM (read locally, sent to the kernel — no manual upload needed).
44
+ - **Piped code**: `echo "print(1)" | colab exec -s <name>` or `cat script.py | colab exec -s <name>`.
45
+ - **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.
46
+ - **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.
47
+ - **Shell**: `echo "cmd" | colab console -s <name>` for batch shell. Console wraps bash in tmux, so even piped output contains terminal-control bytes — filter with `grep -a` for a specific line. `exec` is faster when you don't need a real shell.
48
+ - **Never run `colab repl`, `colab console`, `colab auth`, or `colab drivemount` interactively from an agent** — they expect a TTY and will hang. `repl`/`console` accept piped stdin and exit on EOF; `auth`/`drivemount` genuinely require a human at the terminal.
49
+
50
+ ### Ephemeral one-shot jobs (`colab run`)
51
+ - `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`).
52
+ - **Exit codes propagate**: an uncaught exception or `sys.exit(N)` in the script makes `colab run` exit non-zero (CPython semantics: `sys.exit()`/`sys.exit(0)` → 0, `sys.exit(N)` → N, `sys.exit("msg")` → 1).
53
+ - **Stream separation**: `colab run` writes its own `[colab] ...` chatter to **stderr** and the script's output to **stdout** — so `colab run job.py > out.txt` captures only the script's stdout. (`colab exec` streams the script's stdout/stderr live to your stdout/stderr.)
54
+ - Works as a shebang: `#!/usr/bin/env -S colab run --gpu T4` makes a `chmod +x`'d `.py` a self-contained "rent a GPU, run, clean up" script. After editing CLI behavior, reinstall before testing shebangs — they resolve `colab` via `$PATH`, not the editable install.
55
+ - A nonexistent script path exits non-zero **before** allocating a VM (no wasted compute).
56
+
57
+ ### Automate
58
+ - `colab auth -s <name>` — VM-side GCP creds, needed before in-VM GCS/BigQuery calls (interactive; not agent-runnable).
59
+ - `colab drivemount -s <name> [PATH]` — mounts Drive at `/content/drive` by default (interactive; not agent-runnable).
60
+ - `colab install -s <name> pkg1 pkg2` — installs via `uv pip install --system`, falling back to `pip`. Also `colab install -s <name> -r requirements.txt`.
61
+
62
+ ### Inspect & report
63
+ - `colab help` (or `colab help <cmd>`) lists/explains commands; the listing is alphabetical.
64
+ - `colab sessions` lists server-side assignments and auto-prunes stale local entries. Orphans with no local record show as `[?]`.
65
+ - `colab status [-s <name>]` shows hardware, IDLE/BUSY, and last execution.
66
+ - `colab log -s <name> [-n 20] [-t TYPE]` shows recent structured events; invaluable when a task fails (keep-alive errors carry the raw `response_body`).
67
+ - `colab log -s <name> -o summary.ipynb` exports the session as a notebook (also `.md`, `.txt`, `.jsonl` by suffix).
68
+ - `colab url -s <name>` prints a browser URL that attaches the Colab web UI to your existing CLI session instead of allocating a new VM (add `--open` to launch it).
69
+ - `colab skill` / `colab readme` print this skill and the README (handy for self-discovery).
70
+
71
+ ## Safety
72
+ - **Always `colab stop -s <name>` when done** — idle VMs burn compute units. `colab run` (without `--keep`) self-cleans even if the script errors.
73
+ - Local state lives in `~/.config/colab-cli/sessions.json` (settings in `settings.json`, history in `history/*.jsonl`). Don't edit by hand.
74
+ - **Isolate parallel/agent runs** with the global `--config <path>` flag to point session state at a scratch file (e.g. `colab --config /tmp/agent.json new -s job`). The keep-alive daemon inherits `--auth` and `--config` automatically.
75
+
76
+ ## Recovery
77
+ - "Session not found" / 404 / 401 on exec: the backend pruned the VM. `colab exec`/`repl` detect this and clean up local state automatically — run `colab sessions` and re-create with `colab new`.
78
+ - Execution timeout or wedged kernel: `colab restart-kernel -s <name>` (keeps the VM, resets the kernel), or `colab stop` then `colab new`.
79
+ - Keep-alive daemon died (`colab log` shows `keep_alive_stopped reason=consecutive_4xx_errors`): almost always the missing `colaboratory` scope — re-auth per the Authentication section.
@@ -1,12 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-colab-cli
3
- Version: 0.5.9
3
+ Version: 0.6.0
4
4
  Summary: CLI for interacting with Colab.
5
+ Project-URL: Homepage, https://github.com/googlecolab/google-colab-cli
6
+ Project-URL: Repository, https://github.com/googlecolab/google-colab-cli
7
+ Project-URL: Issues, https://github.com/googlecolab/google-colab-cli/issues
8
+ License-Expression: Apache-2.0
5
9
  License-File: LICENSE
6
- Requires-Python: >=3.13
10
+ Keywords: cli,colab,google-colab,jupyter,notebook
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development
18
+ Requires-Python: >=3.12
7
19
  Requires-Dist: click>=8.0
20
+ Requires-Dist: filelock>=3.29.2
8
21
  Requires-Dist: google-auth-oauthlib>=1.3.0
9
22
  Requires-Dist: google-auth>=2.49.1
23
+ Requires-Dist: html2text>=2024.2.26
10
24
  Requires-Dist: jupyter-kernel-client
11
25
  Requires-Dist: nbformat>=5.10.4
12
26
  Requires-Dist: packaging>=24.0
@@ -26,6 +40,14 @@ A command-line interface for Google Colab. Provision high-performance CPU, GPU,
26
40
 
27
41
  Designed to support seamless developer productivity, headless automation, and AI agent integrations.
28
42
 
43
+ [Demo](https://github.com/user-attachments/assets/656226a9-af13-4fdb-8eda-d7de747336a2)
44
+
45
+ > [!NOTE]
46
+ > **Platform support:** the Colab CLI currently supports **Linux and macOS** only. Windows is not supported at this time.
47
+
48
+ > [!TIP]
49
+ > Looking for in-notebook, interactive agent-assisted coding instead of a terminal workflow? See the [Colab MCP Server](https://github.com/googlecolab/colab-mcp).
50
+
29
51
  ---
30
52
 
31
53
  ## Key Features
@@ -4,6 +4,14 @@ A command-line interface for Google Colab. Provision high-performance CPU, GPU,
4
4
 
5
5
  Designed to support seamless developer productivity, headless automation, and AI agent integrations.
6
6
 
7
+ [Demo](https://github.com/user-attachments/assets/656226a9-af13-4fdb-8eda-d7de747336a2)
8
+
9
+ > [!NOTE]
10
+ > **Platform support:** the Colab CLI currently supports **Linux and macOS** only. Windows is not supported at this time.
11
+
12
+ > [!TIP]
13
+ > Looking for in-notebook, interactive agent-assisted coding instead of a terminal workflow? See the [Colab MCP Server](https://github.com/googlecolab/colab-mcp).
14
+
7
15
  ---
8
16
 
9
17
  ## Key Features
@@ -1,3 +1,9 @@
1
+ ---
2
+ log:
3
+ 2026-06-15: Switched the keep-alive daemon from the `colab.pa.googleapis.com` `RuntimeService/KeepAliveAssignment` RPC to a Tunnel Frontend HTTP ping (`GET /tun/m/<endpoint>/keep-alive/` with `X-Colab-Tunnel: Google`) on `colab.research.google.com`. The RPC required `serviceusage` consumer access to Colab's internal project `1014160490159`, which ordinary user accounts lack, so every external user hit HTTP 403 `USER_PROJECT_DENIED` and their CLI sessions were idle-pruned within minutes (issue #14). Reproduced live with a third-party account; verified the tunnel ping is accepted by the same bearer-token credential that already works for `assign`. A `ReadTimeout` on the ping is treated as success (TFE records activity before forwarding to the often-non-responding VM). Generalized the pre-flight remediation messaging away from the now-irrelevant `colaboratory`/`pa.googleapis.com` framing, and removed the dead grpc-web client-registry/API-key code.
4
+ 2026-06-10: Replaced the POSIX-only `fcntl.flock` file locking in `_LockedFileStore` with the cross-platform `filelock` library (reported broken on Windows). Reads use `ReadWriteLock.read_lock()` (shared) and writes use `write_lock()` (exclusive), preserving the original `LOCK_SH`/`LOCK_EX` semantics. The lock is constructed with `is_singleton=False` so two `StateStore` instances for the same path in one process don't collapse into a single reentrant lock (which would raise `RuntimeError` on multi-threaded write contention). Added shared-read, cross-process exclusion, and multi-thread/multi-process regression tests.
5
+ ---
6
+
1
7
  # Design: Session Management (`new`, `status`, `stop`, `sessions`)
2
8
 
3
9
  ## Overview
@@ -65,15 +71,16 @@ The CLI maps user flags to these backend parameters:
65
71
  ### 5. Keep-Alive Protocol
66
72
  To prevent Colab VMs from being deleted due to idle timeouts (standard is ~90 minutes), the CLI implements a background keep-alive mechanism.
67
73
  - **Daemon Process**: Since the CLI is a fire-and-forget tool, `colab new` spawns a detached background process running a hidden `keep-alive` command.
68
- - **RPC**: Every 60 seconds, the daemon calls `google.internal.colab.v1.RuntimeService/KeepAliveAssignment` at `colab.pa.googleapis.com`. The wire format is grpc-web JSON: `Content-Type: application/json+protobuf`, body `["<endpoint>"]` (positional protojson), `X-Goog-Api-Client: grpc-web/0.1`, `x-user-agent: grpc-web-javascript/0.1`. **Both** the `colaboratory` OAuth scope (see `04_automation_and_utility.md`) and the `grpc-web` substring in `X-Goog-Api-Client` are server-enforced; missing either yields a descriptive 403/400 response.
69
- - **Pre-flight (`colab new`, OAuth2/ADC only)**: Immediately after a successful `assign`, the CLI invokes `keep_alive_assignment` once synchronously. If the response is 403 with a `SCOPE_NOT_PERMITTED` body, it unassigns the new VM (to avoid leaking a billable assignment) and prints a per-provider remediation message before exiting non-zero. Other errors are tolerated — the daemon will retry and surface them via the structured event log.
74
+ - **Tunnel ping**: Every 60 seconds, the daemon issues `GET https://colab.research.google.com/tun/m/<endpoint>/keep-alive/` with the header `X-Colab-Tunnel: Google`, authenticated with the user's own Gaia bearer token (the same credential and host used for `/tun/m/assign`). The Tunnel Frontend (TFE) records `LastActiveTime` before forwarding the request, which refreshes the idle timer. This matches the official `colab-vscode` extension's `sendKeepAlive`. TFE notes the activity on arrival and then forwards to the VM, which often does not answer on this path — so the request commonly read-times-out even though the keep-alive succeeded; a `ReadTimeout` is therefore treated as success, while genuine HTTP errors (e.g. 404 for a deleted assignment) propagate.
75
+ - **Why not the RuntimeService RPC**: The previous implementation called `google.internal.colab.v1.RuntimeService/KeepAliveAssignment` at `colab.pa.googleapis.com` with `X-Goog-User-Project: 1014160490159`. That path requires the caller to be a `serviceusage` consumer of Colab's internal project `1014160490159`, which no ordinary user account is — so it returned HTTP 403 `USER_PROJECT_DENIED` for every external user, causing CLI sessions to be idle-pruned within minutes (issue #14). Dropping the header instead produced HTTP 400 `CONSUMER_INVALID` (public API-key project ≠ bearer-token quota project). The browser only succeeds because it rides the user's `google.com` cookie through an internal cookie-proxy (`colab.clients6.google.com`), which a headless bearer-token client cannot use. The TFE tunnel ping needs no project entitlement and works for any account that can assign a VM.
76
+ - **Pre-flight (`colab new`, OAuth2/ADC only)**: Immediately after a successful `assign`, the CLI invokes `keep_alive_assignment` once synchronously. If the response is 403 with a `SCOPE_NOT_PERMITTED` body, it unassigns the new VM (to avoid leaking a billable assignment) and prints a per-provider remediation message before exiting non-zero. Other errors are tolerated — the daemon will retry and surface them via the structured event log. (Because keep-alive now uses the same backend/credential as `assign`, a scope failure at this stage is rare — assignment would normally have failed first.)
70
77
  - **Structured logging**: The daemon emits `keep_alive_started` (with `pid`, `endpoint`), one `keep_alive_error` per failed iteration (with `status_code`, `error_type`, truncated `error`, `response_body`, `iteration`, `consecutive_4xx`), and `keep_alive_stopped` (with `reason`, `iterations`, `duration_seconds`, optional `last_error`, optional `expected_endpoint`/`actual_endpoint`). All three are rendered specially by `colab log` so users get diagnostic context without parsing JSONL by hand.
71
78
  - **Termination**:
72
79
  - **Explicit**: `colab stop` terminates the daemon using its stored PID.
73
80
  - **Implicit**: If a session is pruned (e.g., during `sync_sessions`), its daemon is also terminated.
74
81
  - **Safety Fallback**: The daemon automatically terminates after 24 hours to prevent permanent zombie processes.
75
82
  - **State Check**: The daemon periodically verifies that its session still exists in the local state store; if missing, it exits.
76
- - **Repeated 4xx**: After two consecutive 4xx responses, the daemon exits with `reason=consecutive_4xx_errors`. The pre-flight in `colab new` now catches the most common cause (missing `colaboratory` scope) before it reaches this branch.
83
+ - **Repeated 4xx**: After two consecutive 4xx responses, the daemon exits with `reason=consecutive_4xx_errors`. With the TFE tunnel ping, a normal read-timeout is not counted as a 4xx (it is treated as success), so this branch is now reached only by genuine HTTP errors such as a 404 for a deleted/expired assignment.
77
84
 
78
85
  ## TODO / Future Work
79
86
  - **Backend Sync**: Implement a way to reconcile the local `sessions.json` with the output of `colab sessions`.
@@ -88,6 +95,12 @@ To prevent Colab VMs from being deleted due to idle timeouts (standard is ~90 mi
88
95
  - Use `requests` for robust HTTP interactions and `pydantic` for schema validation.
89
96
  - Handle authentication headers (likely `Authorization: Bearer <token>` or cookies).
90
97
 
98
+ ### State Persistence & File Locking
99
+ - **Stores**: `StateStore` (`sessions.json`) and `SettingsStore` (`settings.json`) both derive from `_LockedFileStore`, which guards concurrent access across independent `colab` invocations and the detached keep-alive daemon.
100
+ - **Cross-platform locking**: Locking uses the [`filelock`](https://pypi.org/project/filelock/) library rather than `fcntl.flock`. `fcntl` is POSIX-only and is unavailable on Windows, so the original implementation crashed on import there. `filelock` provides the same advisory cross-process locking on Linux, macOS, and Windows.
101
+ - **Shared vs. exclusive**: Each store owns a `filelock.ReadWriteLock` bound to a sidecar file (`<path>.lock`). Reads acquire `read_lock()` (shared — multiple concurrent readers allowed) and writes acquire `write_lock()` (exclusive). This preserves the `LOCK_SH`/`LOCK_EX` distinction of the previous `fcntl` implementation.
102
+ - **`is_singleton=False`**: The `ReadWriteLock` is created with `is_singleton=False`. With `filelock`'s default (`True`), two `ReadWriteLock` objects for the same path *within a single process* are deduplicated into one reentrant lock; its reentrancy guard then raises `RuntimeError` when two threads each construct their own `StateStore` and contend for the write lock. Disabling the singleton registry makes each store's lock independent so they serialize via the underlying file lock instead.
103
+
91
104
  ## Testing Strategy
92
105
  TDD is mandatory for all session management features.
93
106
 
@@ -100,3 +113,8 @@ TDD is mandatory for all session management features.
100
113
  ### 2. State Store Validation
101
114
  - **Test Case**: Verify `StateStore` correctly handles file locking and multiple concurrent reads/writes.
102
115
  - **Test Case**: Verify `--config` override correctly directs all operations to the specified file path.
116
+ - **Test Case (cross-platform locking)**: Verify the store locks via `filelock.ReadWriteLock` on the `<path>.lock` sidecar and does not import the POSIX-only `fcntl`.
117
+ - **Test Case (shared/exclusive semantics)**: Verify reads go through `read_lock()` and writes through `write_lock()`.
118
+ - **Test Case (cross-process exclusion)**: Hold the write lock from a separate process and confirm the store's in-process write blocks until release.
119
+ - **Test Case (concurrent readers)**: Hold a read lock from a separate process and confirm the store can still complete a read concurrently.
120
+ - **Test Case (multi-thread regression)**: Two `StateStore` instances writing from different threads must serialize without raising `RuntimeError` (guards the `is_singleton=False` choice).
@@ -3,6 +3,8 @@ log:
3
3
  2026-05-07: Fixed `colab console` piped-stdin handling. Previously a piped invocation (e.g. `echo 'cmd' | colab console -s s`) sent the command and then hung indefinitely because the previous EOF handler emitted a bare `\x04` (Ctrl-D), which the remote `tmux`-wrapped bash treats as a literal character rather than a session terminator. The new handler sends `exit\n` (which bash actually exits on) and then closes the websocket from the client side after a short grace period (`PIPED_EOF_GRACE_SECONDS = 0.5s`) so any tail output (bash `logout`, tmux `[exited]`) makes it back to the user. TTY mode is unchanged: real-terminal EOF is left to the remote shell. Verified live: `echo 'echo HELLO' | colab console -s s` now exits in ~1.2s instead of hanging.
4
4
 
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
+
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.
6
8
  ---
7
9
 
8
10
  # Design: Execution and Interactive Interaction (`repl`, `exec`, `console`)
@@ -26,7 +28,7 @@ Execution involves sending Python code (or shell commands) to the Jupyter kernel
26
28
  - If file path is local: Read content, send as code.
27
29
  - If file path is remote: Execute `!python <path>`.
28
30
  - **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.
29
- - **Timeout Configuration**: Exposes a `--timeout` flag (default 10s) to allow long-running silent tasks (like model compilation or data downloading) to execute without being prematurely killed.
31
+ - **Timeout Configuration**: Exposes a `--timeout` flag (default 30s) to allow long-running silent tasks (like model compilation or data downloading) to execute without being prematurely killed.
30
32
 
31
33
  ### 3. Console (`colab console`)
32
34
  - **Implementation**: Connects directly to the backend terminal endpoint (`/colab/tty`) via WebSockets using `websocket-client`.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  log:
3
+ 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.
3
4
  2026-06-01: Enabled `colab update --install` self-update on macOS in addition to Linux. Refactored platform check logic to keep the implementation DRY and updated both tests and documentation. Also, on these platforms, an additional message is shown recommending `colab update --install` to upgrade in place, positioned above the standard `pip`/`uv` installation command.
4
5
  2026-05-29: Added default OAuth2 client config (`oauth_config.json`) as a bundled package resource and restored fallback loading logic in `get_credentials()`. The CLI now falls back to using these default credentials when no explicit local config is found. Added `integration/repro_bundled_oauth` integration test.
5
6
  2026-05-27: Refactored `colab README` and `colab AGENT` to bundle `README.md` and `AGENTS.md` via Hatchling's `force-include` and read them using `importlib.resources` instead of `importlib.metadata`. `colab AGENT` now correctly prints `AGENTS.md`.
@@ -23,11 +24,25 @@ managing local state, or inspecting the environment.
23
24
  The CLI supports two authentication strategies for talking to the Colab
24
25
  backend, selected via the global `--auth=<provider>` flag:
25
26
 
26
- 1. **`oauth2`** (default): Standard public InstalledAppFlow via
27
- `google-auth-oauthlib`. Opens a browser for consent, caches the refresh
28
- token at `~/.config/colab-cli/token.json`. If no local config is provided
29
- via `-c/--client-oauth-config` or found at `~/.colab-cli-oauth-config.json`,
30
- it falls back to a bundled `oauth_config.json` containing default OAuth credentials.
27
+ 1. **`oauth2`** (default): Public `InstalledAppFlow` via
28
+ `google-auth-oauthlib`, but run with a **remote copy-paste flow** rather
29
+ than a localhost server. The CLI prints an authorization URL (with
30
+ `token_usage=remote`) using the registered HTTPS landing page
31
+ `https://sdk.cloud.google.com/applicationdefaultauthcode.html`; the user
32
+ signs in, copies the code Google displays, and pastes it back at the
33
+ prompt. The refresh token is cached at `~/.config/colab-cli/token.json`.
34
+ This is the same mechanism `gcloud auth application-default login` uses,
35
+ and it behaves identically on local, remote, headless, and container
36
+ hosts (no auto-opened browser, no bound port). We deliberately do **not**
37
+ use `run_local_server()` (environment-dependent) or the out-of-band (OOB)
38
+ redirect `urn:ietf:wg:oauth:2.0:oob` (blocked by Google in 2022 — see
39
+ `_run_remote_flow` / `REMOTE_REDIRECT_URI` in `auth.py`). The
40
+ `sdk.cloud.google.com` redirect is registered to the cloud-SDK OAuth
41
+ client (`764086051850-...`), which is also the client shipped in the
42
+ bundled `oauth_config.json`; reusing it with any other client id yields
43
+ `redirect_uri_mismatch`. If no local config is provided via
44
+ `-c/--client-oauth-config` or found at `~/.colab-cli-oauth-config.json`,
45
+ it falls back to that bundled `oauth_config.json`.
31
46
  2. **`adc`**: Application Default Credentials via `google.auth.default()`.
32
47
  Honors the standard ADC discovery chain
33
48
  (`GOOGLE_APPLICATION_CREDENTIALS`, `gcloud auth application-default
@@ -41,18 +56,18 @@ allowing the core `Client` to remain authentication-agnostic — it only sees a
41
56
 
42
57
  ### Required Scopes
43
58
 
44
- The CLI talks to two distinct backends, each with different scope demands:
59
+ The CLI talks to the Colab session backend at `colab.research.google.com`
60
+ for assignment, unassignment, the contents API, **and keep-alive** (the TFE
61
+ tunnel ping — see `01_session_management.md`). The `userinfo.email` scope is
62
+ sufficient for this host.
45
63
 
46
- - `colab.research.google.com` (session assignment / unassignment /
47
- contents API): the `userinfo.email` scope is sufficient.
48
- - `colab.pa.googleapis.com` (`RuntimeService`, used by
49
- `KeepAliveAssignment`): **requires** the
50
- `https://www.googleapis.com/auth/colaboratory` scope. Without it, every
51
- request returns HTTP 403 with body `[7,"Request had insufficient
52
- authentication scopes.",...]` and a `DebugInfo` mentioning
53
- `SCOPE_NOT_PERMITTED`. (The frontend additionally requires
54
- `X-Goog-Api-Client` to contain `grpc-web` — see
55
- `01_session_management.md` §5.)
64
+ > Historical note: keep-alive previously used the `RuntimeService`
65
+ > (`KeepAliveAssignment`) at `colab.pa.googleapis.com`, which required the
66
+ > `https://www.googleapis.com/auth/colaboratory` scope **and** the caller to
67
+ > be a `serviceusage` consumer of Colab's internal project `1014160490159`.
68
+ > The latter is impossible for ordinary user accounts, which made keep-alive
69
+ > fail with HTTP 403 `USER_PROJECT_DENIED` for all external users (issue #14).
70
+ > Keep-alive no longer touches `colab.pa.googleapis.com`.
56
71
 
57
72
  How each provider supplies the scope:
58
73
 
@@ -76,12 +91,11 @@ How each provider supplies the scope:
76
91
  ```
77
92
 
78
93
  `userinfo.email` is required for the session backend at
79
- `colab.research.google.com` (otherwise assign/unassign/sessions return
80
- HTTP 401); `colaboratory` is required for the `RuntimeService` at
81
- `colab.pa.googleapis.com` (otherwise keep-alive returns HTTP 403);
82
- `openid` and `cloud-platform` are mandated by `gcloud` itself
83
- (`gcloud auth application-default login` rejects scope lists that
84
- omit `cloud-platform` with `Invalid value for [--scopes]`).
94
+ `colab.research.google.com` (otherwise assign/unassign/sessions/keep-alive
95
+ return HTTP 401); `colaboratory` is retained for forward compatibility and
96
+ other Colab features; `openid` and `cloud-platform` are mandated by
97
+ `gcloud` itself (`gcloud auth application-default login` rejects scope
98
+ lists that omit `cloud-platform` with `Invalid value for [--scopes]`).
85
99
 
86
100
  `colab new` performs a one-shot keep-alive pre-flight after `assign`
87
101
  succeeds so missing-scope failures surface immediately (with per-provider
@@ -2,6 +2,7 @@
2
2
  log:
3
3
  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__"`.
4
4
  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.
5
+ 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`.
5
6
  ---
6
7
 
7
8
  # Design: `colab run` — Shebang-Compatible One-Shot Execution
@@ -29,7 +30,7 @@ colab run [OPTIONS] SCRIPT [SCRIPT_ARGS]...
29
30
  | `--gpu` | str | None | Same set as `colab new --gpu` (T4, L4, G4, H100, A100). |
30
31
  | `--tpu` | str | None | Same set as `colab new --tpu` (v5e1, v6e1). |
31
32
  | `--keep` | bool | False | Do **not** stop the session after the script finishes. |
32
- | `--timeout` | float | 10.0 | Timeout in seconds for code execution to prevent hanging on silent tasks. |
33
+ | `--timeout` | float | 30.0 | Timeout in seconds for code execution to prevent hanging on silent tasks. |
33
34
 
34
35
  ### Shebang usage
35
36
  With `--keep` and `--gpu` baked into the shebang line, an entire one-file workload becomes:
Binary file
@@ -0,0 +1,153 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Setup: HF_TOKEN is required
3
+ # -----------------------------------------------------------------------------
4
+ # This script downloads google/gemma-3-1b-it, which is a gated model. You need
5
+ # a Hugging Face access token. Get one at https://huggingface.co/settings/tokens
6
+ # and accept the model license at https://huggingface.co/google/gemma-3-1b-it
7
+ #
8
+ # 1. Set HF_TOKEN in your local shell, persistently:
9
+ #
10
+ # echo 'export HF_TOKEN=hf_yourTokenHere' | tee -a ~/.zshrc ~/.bashrc
11
+ # source ~/.zshrc # or open a new terminal
12
+ # echo $HF_TOKEN # verify — should print your token
13
+ #
14
+ # 2. Pipe the local env var into the colab kernel before running this script:
15
+ #
16
+ # echo "import os; os.environ['HF_TOKEN'] = '$HF_TOKEN'" | colab exec
17
+ #
18
+ # 3. Verify the kernel received it:
19
+ #
20
+ # echo 'import os; print(bool(os.environ.get("HF_TOKEN")))' | colab exec
21
+ # # → should print: True
22
+ #
23
+ # 4. Run this script:
24
+ #
25
+ # colab exec -f finetune_run.py
26
+ #
27
+ # Note: HF_TOKEN lives in the colab kernel for the lifetime of the session.
28
+ # If you `colab stop` or the session expires, you'll need to re-pipe it (step 2).
29
+ # -----------------------------------------------------------------------------
30
+
31
+ import os
32
+
33
+ os.system("pip install -q -U 'bitsandbytes>=0.46.1'")
34
+
35
+ import torch
36
+ from datasets import load_dataset
37
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
38
+ from peft import LoraConfig, get_peft_model
39
+ from trl import SFTConfig, SFTTrainer
40
+
41
+ MODEL_ID = "google/gemma-3-1b-it"
42
+ NUM_SAMPLES = 200 # demo size; bump to 5000+ for a real run
43
+ MAX_STEPS = 60 # demo cap; set to -1 for full-epoch training
44
+
45
+ # -------- Data --------
46
+ # philschmid/gretel-synthetic-text-to-sql has sql_prompt, sql_context, sql.
47
+ # We hand SFTTrainer a "messages" column and let it apply the chat template.
48
+ print("Loading dataset...")
49
+ dataset = load_dataset("philschmid/gretel-synthetic-text-to-sql", split="train").select(
50
+ range(NUM_SAMPLES)
51
+ )
52
+
53
+
54
+ def to_messages(example):
55
+ user_msg = (
56
+ "You are a SQL expert. Given the schema, write a SQL query that "
57
+ "answers the question. Reply with only the SQL.\n\n"
58
+ f"Schema:\n{example['sql_context']}\n\n"
59
+ f"Question:\n{example['sql_prompt']}"
60
+ )
61
+ return {
62
+ "messages": [
63
+ {"role": "user", "content": user_msg},
64
+ {"role": "assistant", "content": example["sql"]},
65
+ ]
66
+ }
67
+
68
+
69
+ dataset = dataset.map(to_messages, remove_columns=dataset.column_names)
70
+
71
+ # -------- Model (4-bit QLoRA, bf16 throughout) --------
72
+ # Everything is bf16 — matches Gemma's natural dtype, matches TRL's default
73
+ # T4 (Turing) has no hardware bf16, so this is slower than fp16 would be (~2x)
74
+ print(f"Loading {MODEL_ID} in 4-bit...")
75
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
76
+
77
+ model = AutoModelForCausalLM.from_pretrained(
78
+ MODEL_ID,
79
+ quantization_config=BitsAndBytesConfig(
80
+ load_in_4bit=True,
81
+ bnb_4bit_quant_type="nf4",
82
+ bnb_4bit_compute_dtype=torch.bfloat16,
83
+ ),
84
+ device_map="auto",
85
+ )
86
+
87
+ model = get_peft_model(
88
+ model,
89
+ LoraConfig(
90
+ r=16,
91
+ lora_alpha=32,
92
+ target_modules="all-linear",
93
+ task_type="CAUSAL_LM",
94
+ ),
95
+ )
96
+ # Required for QLoRA backward: makes the embedding output require grad so that
97
+ # gradients can flow into the LoRA params attached to layers downstream of the
98
+ # frozen 4-bit base.
99
+ model.enable_input_require_grads()
100
+ model.print_trainable_parameters()
101
+
102
+ # -------- Train --------
103
+ # All other knobs use SFTConfig defaults (which include bf16=True,
104
+ # gradient_checkpointing=True, logging_steps=10). The overrides below are just
105
+ # the demo cap, batch sizing that fits T4 VRAM, and silencing wandb/tensorboard.
106
+ print("Training...")
107
+ trainer = SFTTrainer(
108
+ model=model,
109
+ train_dataset=dataset,
110
+ processing_class=tokenizer,
111
+ args=SFTConfig(
112
+ output_dir="./results",
113
+ max_steps=MAX_STEPS,
114
+ per_device_train_batch_size=2,
115
+ gradient_accumulation_steps=2,
116
+ # Standard QLoRA LR. SFTConfig defaults to 2e-5, which is too low for
117
+ # LoRA adapters to learn anything meaningful in 60 steps.
118
+ learning_rate=2e-4,
119
+ # Compute loss only on the assistant's SQL, not on the schema/question.
120
+ assistant_only_loss=True,
121
+ # Off so KV cache works during the inference step at the end.
122
+ gradient_checkpointing=False,
123
+ report_to="none",
124
+ ),
125
+ )
126
+ trainer.train()
127
+
128
+ # -------- Save --------
129
+ out_dir = "./gemma-3-1b-qlora-adapter"
130
+ trainer.model.save_pretrained(out_dir)
131
+ tokenizer.save_pretrained(out_dir)
132
+ print(f"Saved adapter to {out_dir}")
133
+
134
+ # -------- Inference check --------
135
+ sample = dataset[0]
136
+ prompt = tokenizer.apply_chat_template(
137
+ sample["messages"][:1], # just the user turn
138
+ tokenize=False,
139
+ add_generation_prompt=True,
140
+ )
141
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
142
+ with torch.no_grad():
143
+ out_ids = model.generate(
144
+ **inputs,
145
+ max_new_tokens=256,
146
+ do_sample=False,
147
+ pad_token_id=tokenizer.pad_token_id,
148
+ )
149
+ generated = tokenizer.decode(
150
+ out_ids[0][inputs["input_ids"].shape[1] :], skip_special_tokens=True
151
+ )
152
+ print(f"\nGold: {sample['messages'][1]['content']}")
153
+ print(f"Model: {generated.strip()}")