- CLI: Modular
Typerbased entry point incli.pywith subcommands incommands/. - Common:
common.pycentralizes sharedState(lazy-loading) and session resolution. - Client:
ColabClienthandles API interactions (assignment, unassignment). - Auth:
auth.pyexposes a singleget_credentials(config_path, provider)facade that dispatches on theAuthProviderenum. Two providers are supported, selected via the global--auth=oauth2|adcflag (defaultoauth2):oauth2: publicgoogle-auth-oauthlibInstalledAppFlow, 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 bundledsrc/colab_cli/oauth_config.jsonresource (re-added in PR #41 /9f44fe2, 2026-05-29 — the earlier "removed in20eb88e" note was stale/incorrect; the file exists andauth.py:_get_google_auth_credentialsloads it viaimportlib.resources). As of 2026-06-11 the flow is a remote copy-paste flow, not a localhost server:_run_remote_flowsetsredirect_uri=https://sdk.cloud.google.com/applicationdefaultauthcode.html+token_usage=remote, prints the URL, and reads the pasted code viainput(). NEVER revert to OOB (urn:ietf:wg:oauth:2.0:oob) — Google blocked it in 2022 ("OOB flow has been blocked"); thesdk.cloud.google.comredirect is registered only to the bundled cloud-SDK client (764086051850-...), so any other client id getsredirect_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).adc: Google Application Default Credentials viagoogle.auth.default(). The CLI passesscopes=PUBLIC_SCOPES(which includescolaboratory) and re-applies viacreds.with_scopes()for credential types that support it. User credentials minted bygcloud auth application-default loginignore thescopes=kwarg AND raiseNotImplementedErroronwith_scopes: ADC users must explicitly re-authenticate withgcloud 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.emailis required by the session backend atcolab.research.google.com(assign/unassign/sessions return 401 without it);colaboratoryis retained for forward compatibility and other Colab features;openidandcloud-platformare mandated bygclouditself, which rejects scope lists that omitcloud-platformwithInvalid value for [--scopes]. Service-account / GCE / GKE / impersonated creds get the right scopes transparently viawith_scopes.
- Backend Hosts: Two distinct backends with different requirements:
colab.research.google.com(session backend /tun/m/...): accepts theuserinfo.emailscope. Handles assign, unassign, CCU info, and the contents API.
- Runtime:
ColabRuntimewrapsjupyter-kernel-clientfor execution. - State:
StateStorepersists session metadata in~/.config/colab-cli/sessions.json.- Persistent settings are in
~/.config/colab-cli/settings.json.
- History:
HistoryLoggerrecords structured events in~/.config/colab-cli/history/*.jsonl.
-
Minimalism: Favor standard library where possible (e.g.,
urllib) while utilizingTyperfor CLI ergonomics. -
Piping: Always consider piped input (
stdin) vs. interactive TTY. -
Trace Alignment: When implementing new endpoints, validate against captured browser traces (HAR files).
-
TDD (Test-Driven Development): Always implement tests first. Verify they fail before implementing the solution to make them pass. Every design must include a testing strategy and specific test cases.
-
Jupyter Protocol Deviations: Google Colab uses custom extensions to the Jupyter protocol. Examples include
colab_requestmessages over theiopubchannel andinput_replywrappingcolab_replypayloads on thestdinchannel. These require monkey-patching or specialized handlers withinjupyter-kernel-client(e.g.,wsclient.kernel_socket.on_messageinterceptors). -
Integration Testing: Unit tests and mocks are not enough. Before declaring any feature complete, you MUST perform a real-world, end-to-end integration test against a live Colab environment using the CLI. Never rely solely on mocked unit tests to verify a feature's correctness.
- Integration tests are located in
integration/(e.g.,integration/repro_plot_redirection/test.sh). - To run an integration test, use:
uv run bash integration/repro_<name>/test.sh. uv runensures thecolabcommand (entry point) is available in the shell environment.
- Integration tests are located in
-
Continuous Improvement: Whenever the user provides feedback, workflow advice, or corrections, immediately encode that advice into this
AGENTS.mdfile. The goal is to learn from review and never repeat the same errors.
- Workflow:
- Draft: Plan and start the task. Create a new git branch before working on new features or changes.
- Refine: Implement changes and verify with tests and linting. Run tests using
uv run pytest tests/and resolve any lint errors usinguv run ruff check . --fix. - Finalize: Ensure everything is complete and correct. Whenever features are added or behaviors change, you MUST re-review the corresponding design document in
docs/and update it to reflect the new state. You should also add a brief log entry to the frontmatter of the updated design document with the current date summarizing the change. Finally, commit the finished changes to the git branch for review.
- Session Management:
new,sessions,status,stop. - Execution:
repl,exec,console. - Files:
ls,rm,upload,download,edit. - Automation:
auth,drivemount,install,log,pay,version,update.
The package version is derived from the git tag via hatch-vcs (see pyproject.toml), so a release is just (1) a CHANGELOG.md entry and (2) a vX.Y.Z tag on the merged commit. Follow this workflow ONLY when the user explicitly requests a release (e.g. "cut a release", "tag v0.7.0"). NEVER propose or perform a release proactively.
- Verify a clean, current
main:git fetch origin && git checkout main && git pull --ff-only origin main. Confirmgit statusis clean. Ifmainhas diverged or there are unmerged feature branches the user expects in the release, stop and ask. - Identify unreleased commits: Find the last release tag with
git describe --tags --abbrev=0, then list commits since then withgit log <last-tag>..HEAD --oneline. These are what the new changelog section must cover. Cross-reference each commit's PR number (the(#NN)suffix in the merge commit subject) — every entry in the changelog should cite at least one PR. - Infer the next version (semver): Categorize each commit using the Keep-a-Changelog buckets already present in
CHANGELOG.md(Added,Changed,Fixed,Removed,Deprecated,Security). Pick the bump:- Major (
X.0.0): any breaking change to the CLI surface, flag semantics, on-disk state schema (sessions.json/settings.json), or persisted token format. - Minor (
0.X.0): at least oneAddedentry (new subcommand, new flag, new capability) with no breaking changes. - Patch (
0.0.X): onlyFixed,Changed,Removed(non-breaking cleanup),Deprecated, orSecurityentries. Confirm the inferred version with the user before proceeding — never tag without that confirmation, even when the bump seems obvious.
- Major (
- Draft and commit the CHANGELOG entry on a branch: Create a release branch (e.g.
release-v0.7.0), then prepend a new## [X.Y.Z] - YYYY-MM-DDsection above the previous release. Group entries by category in the order already established inCHANGELOG.md(Changed,Added,Fixed,Removed). Each bullet should be one short paragraph naming the user-visible behavior change (not the implementation detail) and ending with the PR reference(#NN). Append a new link reference at the bottom:[X.Y.Z]: https://github.com/googlecolab/google-colab-cli/compare/v<PREV>...vX.Y.Z. Commit with messagedocs: add CHANGELOG.md for vX.Y.Z release, push the branch, and open a PR withgh pr create. - Wait for the changelog PR to merge: Do NOT tag the pre-merge commit on your branch. The tag must land on the squash-merge commit that appears on
main(this is whathatch-vcswill see and what users willgit checkout). After the user reports the PR is merged (or you observe it viagh pr view <num> --json state,mergeCommit),git fetch origin && git checkout main && git pull --ff-only origin main. - Tag the merged changelog commit and push the tag: Verify
git log -1 --onelineis the merged changelog commit (subject matchesdocs: add CHANGELOG.md for vX.Y.Z release (#NN)). Create an annotated tag whose message is the changelog section body:git tag -a vX.Y.Z -m "vX.Y.Z" -m "<changelog section body>". Push withgit push origin vX.Y.Z. Do NOT create a GitHub Release entry viagh release create— the tag alone is sufficient becausehatch-vcsreads it directly, and the canonical release notes already live inCHANGELOG.md. - Verify: Run
git describe --tags --exact-match HEAD(should printvX.Y.Z) anduv run colab version(the version string should match). Confirm withgh api repos/googlecolab/google-colab-cli/tags --jq '.[0].name'that the tag is visible on the remote.
- Direct Execution: Code for
auth,drivemount, etc., should be injected and executed on the VM kernel. - Contents API: Use the Jupyter Contents API for file management as seen in the browser traces.
- Transparent Storage: Local state must be overridable via flags.
- No netrc: Avoid
netrcfor token persistence in this project. - Mocking Interactivity: When testing commands that branch on
stdin.isatty(), use theis_stdin_ttyhelper inexecution.pyand mock it viamocker.patch("colab_cli.commands.execution.is_stdin_tty", return_value=...). This ensures tests don't hang in CI/agent environments. - State Isolation: Always patch the
colab_cli.common.statesingleton in tests to control session persistence and client behavior. Refer totests/conftest.pyfor the standard global fixture. - Fire-and-Forget Architecture: The Colab CLI is a "fire-and-forget" tool. Avoid using background threads or persistent daemon processes within command flows. Commands authenticate, perform their task, and exit cleanly.
- Verify the Local Install: A globally-installed
colabmay exist onPATH(e.g. at~/.local/bin/colab) and can shadow the project's editable install whenuv runis invoked from outside the repo. ALWAYS run shell commands with the repo as the working directory (e.g. via theworkdirparameter, nevercd && cmd) souv run colab ...resolves to.venv/bin/colab. Confirm withwhich colabanduv run which colabif a CLI test produces unexpected results (e.g. flag-not-recognized errors for flags you just added). Shebang invocations always resolve via$PATH, so a script like#!/usr/bin/env -S colab run ...will pick up the stale global tool even when the editable install is current — when testing shebang-based behavior after a code change, always runuv tool install --reinstall --force --from . colabfirst, then verify withcolab version(the version string includes the git short SHA). Encoded 2026-05-12 after the SystemExit-suppression fix appeared not to work inexamples/hello_colab.pybecause the shebang resolved to a uv-tool install pinned to the prior commit. - Isolate the Regression First: When a user reports an error in code you just touched, do NOT assume your change caused it. First, reproduce the failure on
main(or the branch point) to determine whether the bug is pre-existing. Only after confirming the regression is yours should you start debugging the new code. Encoded after spending a turn debugging "ADC brokecolab new" only to discovercolab new --gpu A100was already failing onmaindue to an A100-quota-vs-default issue unrelated to ADC. - Live Probes Allocate Real Resources: Probing the Colab API to debug an issue creates real, billable assignments — every successful POST
/tun/m/assignreserves 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 viaclient.unassign(endpoint)(orcolab stop) before declaring the investigation done. Then verify withcolab sessionsthat nothing was orphaned. - 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. Ifgit log main..<remote>/mainis non-empty, reset localmainto the remote, rebase feature branches onto it, retest, then push. NEVER force-pushmainto recover from divergence. - 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. - Run Integration Tests Yourself: Re-read AGENTS.md "Agent Execution Limitations" before claiming you can't run a test. The CANNOT-run list is exclusively interactive commands (
colab auth,colab drivemount,colab repl,colab console). Tests built oncolab new/colab stop/colab logare 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 wasuv run bash integration/repro_run_command/test.sh. - 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.
- Two distinct auths — never confuse them: This codebase has two unrelated authentication concerns: (a) CLI-to-Colab-control-plane: how
Clientauthenticates HTTP requests tocolab.research.google.comandcolab.pa.googleapis.com. Driven by the global--auth={oauth2,adc}flag andauth.py:get_credentials. The OAuth2 path is bootstrapped automatically bygoogle_auth_oauthlib'sInstalledAppFlowon first invocation when~/.config/colab-cli/token.jsondoesn't exist; no separate command is needed — any--auth=oauth2invocation (e.g.colab --auth=oauth2 sessions) triggers the browser consent flow. (b) VM-side credentials: how thecolab authsubcommand 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 theUSE_AUTH_EPHEM='0'gcloud-fallback path executed on the kernel viaColabRuntime. NEVER tell a user to "runcolab auth" as a prerequisite for fixing CLI-side auth issues — they're orthogonal layers and the suggestion misleads. - Pydantic validation requires a schema:
_issue_requestaccepts an optionalschema=and historically calledTypeAdapter(schema).validate_python(...)unconditionally. Whenschema=None(a caller that doesn't care about the response body — e.g. unassign or fire-and-forget endpoints that return[]), this raisespydantic.ValidationError: Input should be None. Always guard withif schema is None: returnafter the empty-body short-circuit. - Suggest the branch-diff review command after committing: The user reviews changes with
git diff main..<branch-name>(full cumulative diff againstmain, not just the latest commit). After landing one or more commits on a feature branch, ALWAYS suggest the exact command — e.g. "Review withgit diff main..sort-help-commands" — instead ofgit show <sha>(which only shows a single commit and misses context when a branch has multiple commits). Encoded 2026-05-05 after suggestinggit show 9d9c7dafor a branch the user wanted to review holistically. - 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 urlfirst-cut shipped the wrong URL format because of unverified output. - Clean up orphaned assignments before finishing live tests: After running a live integration test,
colab sessionsmay show server-side assignments that the localcolab stopcouldn't see (e.g. assignments leaked from earlier in the session, or from crashed prior runs). Always runcolab sessionsas the final cleanup step, and for any[?]-marked orphan, runpython -c "from colab_cli.common import state; state.client.unassign('<endpoint>')"(the CLI doesn't expose a direct unassign-by-endpoint command). Re-verify withcolab sessionsreturning "No active sessions". Encoded 2026-05-05 after the firstcolab urllive test left an orphan from a prior conversation turn that would have idled-out and billed compute units. - Forward unknown args through Typer with
context_settings: Typer/Click consumes any token starting with-/--as a flag of the parent command unless told otherwise. For a subcommand likecolab run script.py --some-script-flag(where--some-script-flagbelongs to the user's script, not tocolab), declare it withapp.command(name="run", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})and accept the positional withAnnotated[Optional[List[str]], typer.Argument(...)] = None. Also userepr()(not f-string interpolation) when embedding those forwarded strings into kernel-side Python source —repr()produces a safe round-trippable literal regardless of the user's shell-passed quotes, backslashes, or non-ASCII bytes. Encoded 2026-05-12 while implementingcolab run.
As an AI agent operating via non-interactive shell tools (run_shell_command), there are strict limits on what I can test autonomously without human intervention:
- I CAN Run:
- Automated tests (
pytest), linting (ruff), and headless execution scripts. - Subcommands that don't pause for user input (e.g.,
colab new,colab status,colab stop,colab ls,colab install,colab exec <file.py>). - Piped
colab replandcolab console: as of 2026-05-07 both commands support piped stdin and exit cleanly on EOF (echo 'cmd' | colab console -s sreturns in ~1.2s). The unpiped, interactive variants still cannot be run autonomously. - Specially crafted mock scripts that simulate timeouts or API calls.
- Automated tests (
- I CANNOT Run (Requires User Assistance):
colab auth: This command relies on the traditional Gcloud fallbackinput_request(viaUSE_AUTH_EPHEM='0'), which prompts the user via Python'sinput()to click a URL, sign in, and paste back an authorization code. My shell tool will hang indefinitely on thisinput().colab drivemount: This command prompts the user viasys.stdin.readline()(specifically querying/dev/ttyto ensure input is captured) to pressEnterafter granting OAuth consent in the browser. My shell tool will timeout/hang waiting forEnter.- Interactive (TTY)
colab repl/colab console: When stdin is a real terminal these commands drop into interactive raw-TTY modes that require real-time keystroke streaming. My shell tools cannot support this. (Piped stdin is fine — see above.)
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.