|
1
|
--- |
|
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. |
|
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. |
|
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. |
|
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`. |
|
7
|
2026-05-27: Extended `colab update --install` to detect if the CLI was installed via `uv tool install` (by checking if `sys.executable` contains `/uv/`) and if so, use `uv tool install -U google-colab-cli` to upgrade. |
|
8
|
2026-05-27: Updated auto-update upgrade hint to recommend `pip install --upgrade google-colab-cli` instead of `colab`, aligning with the PyPI package name. |
|
9
|
2026-05-27: `colab url` now emits BOTH the `?dbu=<urlencoded path>` query parameter (existing) AND a new `#datalabBackendUrl=<full URL>` hash fragment (new). Format: `https://<host>/notebooks/empty.ipynb?dbu=%2Ftun%2Fm%2F<endpoint>#datalabBackendUrl=<host>/tun/m/<endpoint>`. Why both: some Colab frontend code paths consult the hash fragment first and ignore `dbu` entirely, so the previously-emitted query-only form failed silently for those users (the frontend fell through to allocating a fresh VM via `/tun/m/assign`). The fragment value is a FULL URL with scheme + host (NOT just the path) and is emitted RAW (no URL encoding) because browsers don't decode the fragment before passing `location.hash` to page JS — Colab's parser calls `new URL(rawString)` directly. The fragment host always matches `--host` so Colab's same-origin enforcement on embedded backend URLs doesn't block the connection, and sandbox/dev users (`--host https://colab.sandbox.google.com`) get a sandbox fragment automatically. Three new test cases in `tests/test_url.py` cover the raw-encoding requirement (`%3A`/`%2F` must NOT appear in the fragment), the both-signals-present invariant, and `--open` propagating the fragment to `webbrowser.open()`. Integration-verified live against synthetic session state with three host shapes (default, sandbox, trailing-slash); all produced correctly-shaped URLs with no `//` artifacts. |
|
10
|
2026-05-07: Added a developer-only `colab whoami` subcommand (hidden from `colab --help`). Mints an access token via the same `auth.get_credentials(...)` path the rest of the CLI uses (honoring the global `--auth=...` flag), refreshes the credentials, then queries `https://oauth2.googleapis.com/tokeninfo` to print the email, scopes, audience, and expiry of whatever the CLI is about to send. Built specifically to short-circuit the "why is my call to colab.pa.googleapis.com 403-ing" debugging loop — the answer is almost always "missing scope" or "wrong identity", both of which `whoami` makes immediately visible. Hidden via `app.command(hidden=True)`; reachable via `colab whoami` or `colab whoami --help`. Suppressed from the daily-update banner check (added to `_AUTO_UPDATE_SUPPRESSED` in `cli.py`) so the banner doesn't obscure the auth output. |
|
11
|
2026-05-11: Removed the local-file update source (`update_file_path` setting and `_fetch_local` helper); `colab update` now consults PyPI only. Switched the default `update_url` to the canonical PyPI JSON API (`https://pypi.org/pypi/google-colab-cli/json`), which already exposes the `info.version` schema the auto-update subsystem expects. Re-added `colab update --install` as a public self-install path that runs `pip install -U google-colab-cli` against the current `sys.executable`; Linux-only (other platforms exit non-zero with an explanatory message), and a silent no-op when the cached `latest_version` is already at or below the current install. |
|
12
|
2026-05-12: Added an optional `timeout=` parameter to `ColabRuntime.execute_code` that flows through to both the `execute()` and `execute_interactive()` branches. `colab auth` and `colab drivemount` now pass `timeout=600` (10 min) via a shared `INTERACTIVE_AUTOMATION_TIMEOUT_SEC` constant in `commands/automation.py`. Background: `jupyter_kernel_client` defaults to a 10s wall-clock timeout that is consumed even when the kernel is idle waiting on `input_request`. With the drivefs hook intercepting that request and prompting the user to OAuth in their browser, any user that takes >10s to click through (essentially everyone) hit `TimeoutError` and saw "drivemount failed" even though the mount had actually succeeded server-side. The fix is scoped narrowly to the two human-in-the-loop subcommands; non-interactive paths (`colab exec`, `colab run`, `colab install`, `colab repl --pipe`, `colab console --pipe`) keep the upstream default since they receive continuous iopub traffic that resets the practical inactivity ceiling. |
|
13
|
--- |
|
14
|
|
|
15
|
# Design: Automation and Utility (`auth`, `install`, `log`, `pay`, `version`, `update`, `whoami`) |
|
16
|
|
|
17
|
## Overview |
|
18
|
|
|
19
|
These subcommands are implemented by executing Python code on the Colab VM, |
|
20
|
managing local state, or inspecting the environment. |
|
21
|
|
|
22
|
## Authentication Strategies (CLI Backend) |
|
23
|
|
|
24
|
The CLI supports two authentication strategies for talking to the Colab |
|
25
|
backend, selected via the global `--auth=<provider>` flag: |
|
26
|
|
|
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`. |
|
46
|
2. **`adc`**: Application Default Credentials via `google.auth.default()`. |
|
47
|
Honors the standard ADC discovery chain |
|
48
|
(`GOOGLE_APPLICATION_CREDENTIALS`, `gcloud auth application-default |
|
49
|
login`, GCE/GKE metadata server). Useful when running the CLI from |
|
50
|
environments that already have ambient Google credentials. |
|
51
|
|
|
52
|
The choices are encoded as the `AuthProvider` string-enum in `auth.py`. The |
|
53
|
`get_credentials(config_path, provider)` entry point dispatches on this enum, |
|
54
|
allowing the core `Client` to remain authentication-agnostic — it only sees a |
|
55
|
`requests.AuthorizedSession`. |
|
56
|
|
|
57
|
### Required Scopes |
|
58
|
|
|
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. |
|
63
|
|
|
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`. |
|
71
|
|
|
72
|
How each provider supplies the scope: |
|
73
|
|
|
74
|
- **`oauth2`**: `PUBLIC_SCOPES` already includes `colaboratory`, so the |
|
75
|
InstalledAppFlow consent screen lists it. Existing cached tokens at |
|
76
|
`~/.config/colab-cli/token.json` that were minted before this change must |
|
77
|
be deleted to trigger a fresh consent flow. |
|
78
|
- **`adc`**: `google.auth.default(scopes=PUBLIC_SCOPES)` is called, and for |
|
79
|
credential subclasses that support `with_scopes` (service accounts, |
|
80
|
GCE/GKE metadata, impersonated) we re-apply via `creds.with_scopes(...)`. |
|
81
|
User credentials from `gcloud auth application-default login` ignore the |
|
82
|
`scopes=` kwarg AND raise `NotImplementedError` on `with_scopes`; those |
|
83
|
users must explicitly re-authenticate: |
|
84
|
|
|
85
|
``` |
|
86
|
gcloud auth application-default login \ |
|
87
|
--scopes=openid,\ |
|
88
|
https://www.googleapis.com/auth/cloud-platform,\ |
|
89
|
https://www.googleapis.com/auth/userinfo.email,\ |
|
90
|
https://www.googleapis.com/auth/colaboratory |
|
91
|
``` |
|
92
|
|
|
93
|
`userinfo.email` is required for the session backend at |
|
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]`). |
|
99
|
|
|
100
|
`colab new` performs a one-shot keep-alive pre-flight after `assign` |
|
101
|
succeeds so missing-scope failures surface immediately (with per-provider |
|
102
|
remediation guidance) rather than silently after ~1 minute via the daemon. |
|
103
|
|
|
104
|
## Approach |
|
105
|
|
|
106
|
### 1. Authentication (`colab auth`) |
|
107
|
|
|
108
|
- **Action**: Execute code on the VM to trigger user-interactive |
|
109
|
authentication using the classic Gcloud fallback. |
|
110
|
- **Code**: `python import os os.environ['USE_AUTH_EPHEM'] = '0' from |
|
111
|
google.colab import auth auth.authenticate_user()` |
|
112
|
- **Handling**: Setting `USE_AUTH_EPHEM` to `'0'` forces the kernel to print a |
|
113
|
standard `gcloud` verification URL and trigger an `input_request` message on |
|
114
|
the `iopub` channel. The CLI intercepts this via a `stdin_hook` and prompts |
|
115
|
the user locally, returning the code to unlock the kernel. |
|
116
|
|
|
117
|
### 2. Package Installation (`colab install`) |
|
118
|
|
|
119
|
- **Action**: Execute `pip` on the VM. |
|
120
|
- **Code**: `python import sys, subprocess |
|
121
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "..."])` |
|
122
|
- **Requirements File**: Upload `requirements.txt` if provided with `-r` and |
|
123
|
then run `pip install -r`. |
|
124
|
|
|
125
|
### 3. Drive Mounting (`colab drivemount`) |
|
126
|
|
|
127
|
- **Action**: Execute `drive.mount()` and transparently proxy Colab's |
|
128
|
proprietary credential propagation flow. |
|
129
|
- **Code**: `python from google.colab import drive |
|
130
|
drive.mount('/content/drive')` |
|
131
|
- **Handling**: Because `drivefs` enforces the ephemeral side-channel |
|
132
|
propagation (`colab_request` over websocket), the CLI intercepts these |
|
133
|
messages using `ColabRuntime.colab_request_hook`. When intercepted, the CLI |
|
134
|
automatically interacts with the Colab backend |
|
135
|
(`/tun/m/credentials-propagation/`), prompts the user with the Google OAuth |
|
136
|
consent URL if needed, and dispatches the required `colab_reply` message to |
|
137
|
the `stdin` channel to unlock the kernel thread. |
|
138
|
- **Timeout**: The kernel is silent (no iopub traffic) the entire time the |
|
139
|
user is OAuthing in their browser. To avoid the upstream 10s |
|
140
|
`jupyter_kernel_client` default raising `TimeoutError` mid-flow, this |
|
141
|
subcommand passes `timeout=INTERACTIVE_AUTOMATION_TIMEOUT_SEC` (600s) to |
|
142
|
`ColabRuntime.execute_code`. Same applies to `colab auth`. |
|
143
|
|
|
144
|
### 4. Logging and Notebook Capture (`colab log`) |
|
145
|
|
|
146
|
- **Action**: Capture the session's command history and outputs. |
|
147
|
- **Storage**: Maintain a local JSON-L file of all major operations, |
|
148
|
executions, and stdin interactions in |
|
149
|
`~/.config/colab-cli/history/<session_name>.jsonl`. |
|
150
|
- **Viewing**: `colab log list` and `colab log show <session>`. |
|
151
|
- **Conversion (Planned)**: Future expansion to convert history logs to |
|
152
|
`.ipynb` or `.html`. |
|
153
|
|
|
154
|
### 5. Subscription Management (`colab pay`) |
|
155
|
|
|
156
|
- **Action**: Open the Colab signup page in the user's browser. |
|
157
|
- **Implementation**: Uses |
|
158
|
`webbrowser.open("https://colab.research.google.com/signup")`. |
|
159
|
|
|
160
|
### 6. Version Information (`colab version`) |
|
161
|
|
|
162
|
- **Action**: Show the current version of the Colab CLI. |
|
163
|
- **Implementation**: |
|
164
|
- Attempts to retrieve the version using |
|
165
|
`importlib.metadata.version("colab")`. |
|
166
|
- If not installed (e.g., running from source), it falls back to the short |
|
167
|
Git commit hash using `git rev-parse --short HEAD`. |
|
168
|
- Dynamic versioning is supported in the build system via `hatch-vcs`. |
|
169
|
|
|
170
|
### 7. Auto-Update (`colab update`) |
|
171
|
|
|
172
|
- **Action**: Check if a new version of the Colab CLI is available. |
|
173
|
- **Auto-check**: The CLI automatically checks for updates once every 24 hours |
|
174
|
during the execution of any command. Independently, the cached |
|
175
|
`latest_version` (see below) is consulted on **every** invocation so the |
|
176
|
upgrade banner remains visible between fetches without requiring a network |
|
177
|
round-trip. |
|
178
|
- **Suppressed subcommands**: To keep machine-parseable output clean, the |
|
179
|
daily fetch and the cached banner are suppressed for `update` (which |
|
180
|
runs its own check), `version`, `log`, `pay`, `url`, `help`, and |
|
181
|
`whoami`. The list lives as `_AUTO_UPDATE_SUPPRESSED` in the global |
|
182
|
Typer callback in `cli.py`. |
|
183
|
- **Manual-check**: `colab update` forces a check and prints the status. |
|
184
|
- **Implementation**: |
|
185
|
- Fetches a PyPI-style JSON document from a configurable `update_url` |
|
186
|
(default: `https://pypi.org/pypi/google-colab-cli/json`) and reads |
|
187
|
`info.version`. |
|
188
|
- Compares the fetched version with the current CLI version using |
|
189
|
PEP 440 / semantic versioning, falling back to string equality when a |
|
190
|
version is unparseable. |
|
191
|
- Persists the following fields in `~/.config/colab-cli/settings.json`: |
|
192
|
- `update_url`: source configuration. |
|
193
|
- `last_check`: timestamp of the last fetch (drives the daily |
|
194
|
throttle). |
|
195
|
- `enable_update_check`: master switch for both the daily fetch and |
|
196
|
the cached banner. |
|
197
|
- `latest_version`: highest version observed during the most |
|
198
|
recent successful check. Updated whenever a strictly-newer |
|
199
|
version is observed (never downgraded), and preserved verbatim |
|
200
|
across failed checks so transient network issues do not erase |
|
201
|
the cache. |
|
202
|
- **Notification**: If a new version is found, a non-intrusive message is |
|
203
|
printed to the console with a `Run 'pip install --upgrade google-colab-cli' to |
|
204
|
update.` hint. On Linux and macOS platforms where `--install` self-update is supported, |
|
205
|
an additional hint `You can run 'colab update --install' to upgrade in place.` |
|
206
|
is displayed above the pip/uv install command. The cached banner shown between |
|
207
|
fetches uses the generic `Run 'colab update' to update.` hint. |
|
208
|
- **Self-install (`--install`)**: An opt-in `--install` flag (default |
|
209
|
`False`) makes `colab update` upgrade the CLI in place (**Linux and macOS**). |
|
210
|
It detects how the CLI was installed: |
|
211
|
- If `sys.executable` contains `/uv/tools` (indicating it was installed via |
|
212
|
`uv tool install`), it runs `uv tool install -U google-colab-cli`. |
|
213
|
- Otherwise, runs `pip install -U google-colab-cli` using `sys.executable` |
|
214
|
to ensure the upgrade lands in the same interpreter. |
|
215
|
On other platforms, the command exits non-zero with an explanatory |
|
216
|
message. When the cached `latest_version` is already at or below the |
|
217
|
current install, the flag is a silent no-op so it is safe to wire into |
|
218
|
automation. If the upgrade command exits non-zero, `colab update --install` |
|
219
|
propagates the same exit code. |
|
220
|
|
|
221
|
### 8. Identity Inspection (`colab whoami`) [developer-only] |
|
222
|
|
|
223
|
- **Action**: Resolve the active credentials, mint an access token, and |
|
224
|
print the email, audience, scopes, and expiry of that token. |
|
225
|
- **Visibility**: Registered with `hidden=True` so it does not appear in |
|
226
|
`colab --help`. Discoverable via source code, `colab whoami --help`, or |
|
227
|
word-of-mouth. The intent is to keep the public surface focused on |
|
228
|
end-user commands while still giving developers a one-shot debugging |
|
229
|
aid. |
|
230
|
- **Implementation**: |
|
231
|
- Calls `auth.get_credentials(state.client_oauth_config, |
|
232
|
provider=state.auth_provider)` — the exact same code path the |
|
233
|
`Client` uses — so the token reflects what the rest of the CLI |
|
234
|
would actually send. |
|
235
|
- Always calls `creds.refresh(Request())` before reading |
|
236
|
`creds.token`. Service-account, GCE/GKE-metadata, and some |
|
237
|
impersonated credentials lazy-mint the token; without an explicit |
|
238
|
refresh `creds.token` is `None` even for valid credentials. |
|
239
|
- Hits `https://oauth2.googleapis.com/tokeninfo?access_token=<token>` |
|
240
|
via stdlib `urllib.request` rather than the already-authorized |
|
241
|
`requests.AuthorizedSession`. The tokeninfo endpoint accepts the |
|
242
|
token as a query parameter and does NOT want a `Bearer` header |
|
243
|
alongside it. |
|
244
|
- Renders `expires_in` (seconds) as minutes for readability. |
|
245
|
- On HTTP 4xx from tokeninfo (typical for revoked/expired tokens), |
|
246
|
the JSON error body is surfaced verbatim rather than being |
|
247
|
swallowed; the developer needs to see *why* the token was |
|
248
|
rejected. |
|
249
|
- **Output shape**: |
|
250
|
``` |
|
251
|
Auth provider: adc |
|
252
|
Email: user@example.com |
|
253
|
Audience: 764086051850-...apps.googleusercontent.com |
|
254
|
Expires in: 47m |
|
255
|
Scopes: |
|
256
|
- email |
|
257
|
- https://www.googleapis.com/auth/cloud-platform |
|
258
|
- https://www.googleapis.com/auth/colaboratory |
|
259
|
- https://www.googleapis.com/auth/userinfo.email |
|
260
|
- openid |
|
261
|
``` |
|
262
|
|
|
263
|
### 9. README and AGENT (`colab README`, `colab AGENT`) |
|
264
|
|
|
265
|
- **Action**: Print the bundled `README.md` or `AGENTS.md` file. |
|
266
|
- **Implementation**: |
|
267
|
- Uses `importlib.resources.files("colab_cli").joinpath(...)` to read the |
|
268
|
bundled `README.md` (for `colab README`) or `AGENTS.md` (for `colab AGENT`) |
|
269
|
from the package resources. |
|
270
|
- The files are bundled into the package via Hatchling's `force-include` |
|
271
|
configuration in `pyproject.toml`. |
|
272
|
- If reading from resources fails (e.g. during development when not |
|
273
|
installed), it falls back to reading the files from the project root. |
|
274
|
- Prints the content to stdout. |
|
275
|
|
|
276
|
## Implementation Details |
|
277
|
|
|
278
|
- **Code Injection**: Use a standard `run_code(session, code)` helper via |
|
279
|
`ColabRuntime`. |
|
280
|
- **History Management**: Use `HistoryLogger` class to append structured |
|
281
|
events to session-specific `.jsonl` files. |
|
282
|
- **Interactive Prompts**: Instrumented `stdin_hook` and `colab_request_hook` |
|
283
|
to record interactive user input and proprietary backend requests. |
|
284
|
|
|
285
|
## Testing Strategy |
|
286
|
|
|
287
|
TDD is mandatory for all automation features. |
|
288
|
|
|
289
|
### 1. Mock Kernel Injection |
|
290
|
|
|
291
|
- **Test Case**: Verify `colab auth` correctly injects `from google.colab |
|
292
|
import auth; auth.authenticate_user()`. |
|
293
|
- **Test Case**: Verify `colab install` correctly injects `pip install` or `uv |
|
294
|
install` commands to the remote VM kernel. |
|
295
|
- **Test Case**: Verify `colab drivemount` correctly injects `drive.mount()` |
|
296
|
commands and registers the `colab_request_hook` to intercept credential |
|
297
|
propagation events. |
|
298
|
|
|
299
|
### 2. History Capture |
|
300
|
|
|
301
|
- **Test Case**: Verify all code sent via `exec` is correctly appended to the |
|
302
|
JSON-L history file for that session. |
|
303
|
- **Test Case**: Verify `colab log` correctly generates an `.ipynb` from a |
|
304
|
populated history file. |
|
305
|
|
|
306
|
### 3. `whoami` Identity Resolution |
|
307
|
|
|
308
|
- **Test Case**: Mock the credentials + `urllib.request.urlopen` to return a |
|
309
|
fake tokeninfo payload; verify the printed output contains the email, the |
|
310
|
active auth provider name, the scopes (one per line), and a human-readable |
|
311
|
expires-in (minutes, not raw seconds). |
|
312
|
- **Test Case**: When `urlopen` raises `HTTPError(400)` (revoked/expired |
|
313
|
token), `whoami` exits non-zero with a message identifying the failure |
|
314
|
rather than emitting an unhandled traceback. |
|
315
|
- **Test Case**: `colab --help` does NOT mention `whoami` (regression |
|
316
|
against accidental un-hiding) but `colab whoami --help` still shows the |
|
317
|
command's own help text. |
|
318
|
- **Test Case**: `creds.refresh()` is called before `creds.token` is read |
|
319
|
(regression against silently-`None` tokens for service-account / |
|
320
|
GCE-metadata creds). |
|
321
|
|
|
322
|
### 4. `README` and `AGENT` Commands |
|
323
|
|
|
324
|
- **Test Case**: Verify `colab README` prints the expected content when package metadata is available. |
|
325
|
- **Test Case**: Verify `colab AGENT` prints the same content. |
|
326
|
- **Test Case**: Verify fallback to local `README.md` file when metadata is not available. |
|
327
|
- **Test Case**: Verify error exit when both metadata and local file are unavailable. |