From a40a459dab93b460de5866a877b520073151c7eb Mon Sep 17 00:00:00 2001 From: DTTerastar Date: Thu, 23 Jul 2026 21:29:23 -0400 Subject: [PATCH] =?UTF-8?q?docs(contract):=20=C2=A75=20=E2=80=94=20headles?= =?UTF-8?q?s=20env=20var=20wins=20over=20the=20saved=20token=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §5 mandates a headless `{SERVICE}_*` variable but never said how it resolves against a saved token file, so the two CLIs that have both landed on opposite answers: liftoff lets the env var win, withings-export-cli#43 lets the file win. Same variable pattern, same invocation, different behavior — exactly the drift this contract exists to prevent. Rules the environment wins, and pins the two consequences that fall out of it: `auth status` reports the env source and exits 0 without a network call, and for rotating upstreams the headless path writes the rotated token back best-effort — a failed write is a stderr warning, not an error, because a read-only rootfs is a normal container shape and a minted access token is usable whether or not the cache write landed. Verified against the family before writing it down: liftoff already conforms (internal/auth/auth.go:84, env checked before load, never persisted); crono has no token file, so precedence is vacuous there — hence the new n/a cell. withings-export-cli#43 is the one that changes to match. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo --- CONTRACT.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CONTRACT.md b/CONTRACT.md index 8f04332..7202a63 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -11,6 +11,7 @@ Each cell shows whether the section is implemented and how it is attested for th - **machine** — covered by a [`compat/`](compat/README.md) bundle that self-tests green in `quantcli/common`'s own CI on every PR. During this partial-adoption window, **machine** attests that the bundle exists and is green here; the explicit next milestone is the per-exporter consumer-wire PRs that will run the bundle directly in each `*-export-cli`'s own CI. - **human** — implemented; verified only by review at merge time. - **—** — not implemented yet. +- **n/a** — does not apply to this CLI's upstream (e.g. precedence rules for a token file the CLI never writes). | Section | crono-export | liftoff-export | withings-export | |---|---|---|---| @@ -23,6 +24,7 @@ Each cell shows whether the section is implemented and how it is attested for th | `--format json` | human | human | **machine** | | `--format csv` | — | — | **machine** | | `auth status` subcommand | human | human | human | +| Headless auth precedence (env over token file) | n/a | human | human | | `prime` subcommand | human | human | human | | Hermetic `--help` / flag-validation (§7) | **machine** | **machine** | **machine** | @@ -92,6 +94,14 @@ Auth flows differ legitimately across upstreams (env-var basic auth, OAuth2, int - An `auth status` subcommand that prints one line summarizing readiness — e.g. `logged in as you@example.com (token expires 2026-05-01)` or `missing CRONOMETER_PASSWORD`. Exit code 0 if usable, non-zero otherwise. - A headless path: where the upstream's auth model permits it, every CLI accepts environment variables that let a fresh container run without an interactive login. The variable names are `{SERVICE}_*` (e.g. `CRONOMETER_PASSWORD`, `WITHINGS_REFRESH_TOKEN`). Document them in `prime`. +### Precedence + +Where a CLI has both a headless variable and a saved token file, the environment wins: + +- **`{SERVICE}_REFRESH_TOKEN` (or the equivalent headless variable) takes precedence over any saved token file when it is set.** A container with a stale mounted config and a freshly injected secret must use the secret. Precedence is uniform across the family — an invocation that honors the variable in one CLI must not silently ignore it in another. +- `auth status` reports the environment as the source and exits 0 without a network call. Exit 0 there means "a token was supplied", not "the token works" — validity is unknowable locally. +- Where the upstream **rotates** refresh tokens on each use, the headless path writes the rotated value back to the token file on a best-effort basis, so a repeat caller can read it and re-inject it. A failed write there is a stderr warning, not an error (§4): a minted access token is usable whether or not the cache write landed, and read-only rootfs is a normal container shape. + Where the upstream forbids headless auth (e.g. interactive OAuth consent), `auth status` still works and the CLI still exposes `auth login` / `auth logout` / `auth refresh`. ## 6. The `prime` subcommand