# welcome-to-codevalet-as-a-contributor Onboarding for new contributors to the codevalet Gitea instance. The production Gitea host is published through the Pangolin HTTPS edge at `https://gitea.cvgitea.ddns.net`. Produces an authenticated local checkout of `forge-stack-orchestrator` with git operations against Gitea authenticated silently through an OAuth2 (PKCE) token shared with the orchestrator's devpi gateway. ## Quick start ```bash just setup ``` `just setup` runs `doctor`, `init-env`, `check-gitea`, `login`, `check-access`, and `clone-orchestrator` in sequence and prompts only when a decision requires a human (missing Gitea username, existing stored session, existing orchestrator clone). Idempotent. Individual recipes remain available; see `just --list`. ## Prerequisites Linux, macOS, or Windows-via-WSL with: - `git`, `bash`, `curl` - Python 3.11+ (a `uv python install 3.11` interpreter counts) - [`just`](https://github.com/casey/just#installation) - [`uv`](https://docs.astral.sh/uv/#installation) - graphical web browser (optional; see *Headless and SSH hosts*) - `~/.local/bin` on `PATH` `just doctor` verifies all of these and, for each miss, prints the exact install command for the detected platform (`apt` / `dnf` / `pacman` / `zypper` / `brew`) and a consolidated block at the end. No recipe runs `sudo` or installs anything system-wide. A Gitea account on `FORGE_GITEA_URL` with membership in the `codevalet` organisation is also required. Current production value: `https://gitea.cvgitea.ddns.net`. ## Configuring `.env` `just init-env` copies `.env.example` → `.env`. The only required edit is `FORGE_GITEA_USERNAME`. Defaults: | Variable | Default | Change when | | --- | --- | --- | | `FORGE_GITEA_URL` | `https://gitea.cvgitea.ddns.net` | different Gitea instance | | `FORGE_GITEA_ORG` | `codevalet` | fork or sibling org | | `FORGE_ORCHESTRATOR_REPO_URL` | `https://gitea.cvgitea.ddns.net/codevalet/forge-stack-orchestrator.git` | different fork | | `FORGE_ORCHESTRATOR_BRANCH` | *(empty: default branch)* | branch pin | | `FORGE_WORKSPACE_ROOT` | `.` (clone at `./forge-stack-orchestrator`, gitignored) | clone elsewhere | | `FSDGG_CLI_CLIENT_ID` | registered PKCE CLI client | never | | `FSDGG_CLI_REDIRECT_URI` | `http://127.0.0.1:38111/callback` | port conflict only; must stay `http://` + loopback per RFC 8252 §7.3 (`127.0.0.1`, `[::1]`, or `localhost`) and match the Gitea OAuth app registration | **`FSDGG_CLI_REDIRECT_URI` is not the Gitea URL.** The Gitea server is at `FORGE_GITEA_URL` (remote, HTTPS). The redirect URI is the local loopback HTTP listener the CLI binds on the local machine so Gitea can hand back the OAuth authorisation code; OAuth 2.0 for Native Apps (RFC 8252 §7.3) prohibits any non-loopback / non-HTTP scheme here, and no public CA will issue a cert for `127.0.0.1` so HTTPS on loopback is not a meaningful option. On a shared or remote host, SSH-forward the port (see *Headless and SSH hosts*) rather than trying to publish the callback over the network. `FORGE_GITEA_URL` is the canonical public HTTPS endpoint. Do not append `:6006`; Pangolin terminates TLS on the public edge and the onboarding repo, orchestrator clone URL, and global git credential scope all assume the standard portless form. `.env` is gitignored. OAuth client IDs are public by design; PKCE requires no client secret. ## Recipes | Recipe | Effect | | --- | --- | | `just setup` | Full interactive onboarding. `FORGE_SETUP_YES=1` accepts every default. | | `just setup --headless` | Same, but skips `webbrowser.open`; alias `--no-browser`. | | `just welcome` | Onboarding plan. | | `just doctor` | Prerequisite checks with copy-pasteable fixes. | | `just init-env` | Copies `.env.example` → `.env`. Never overwrites. | | `just check-gitea` | Hits `FORGE_GITEA_URL/api/v1/version`. | | `just login` | Browser PKCE OAuth2 flow; installs the git credential helper. | | `just login-headless` | `just login` without opening a browser. | | `just status` | Stored-token state. Exits non-zero when not live. | | `just refresh` | Forces a token refresh (normally automatic). | | `just logout` | Clears stored Gitea tokens; keeps the credential helper and orchestrator-gateway fields. | | `just relogin` | `logout` + `login`. | | `just check-access` | `git ls-remote` against the orchestrator. | | `just clone-orchestrator` | Clones into `$FORGE_WORKSPACE_ROOT`. Idempotent. | | `just next-steps` | Commands to run inside the orchestrator checkout. | | `just uninstall` | Reverses `login` and `clone-orchestrator`. | | `just test` | Full test suite. | ## Switching Gitea users ```bash just relogin # or: just logout && just login ``` `just logout` leaves the credential helper installed and preserves orchestrator-gateway fields in `~/.forge-stack-devpi-gateway-gitea/client-auth.json`. Git operations against `FORGE_GITEA_URL` fail loudly between `logout` and the next `login`. ## Wrong-user guard `just login` refuses to persist tokens that do not match `FORGE_GITEA_USERNAME`: 1. The authorise URL carries `prompt=login` (OIDC Core §3.1.2.1), forcing Gitea to re-display its login form even under an active session; when `FORGE_GITEA_USERNAME` is set, `login_hint=` is also included. 2. After the token exchange, `userinfo.preferred_username` is compared case-insensitively to `FORGE_GITEA_USERNAME`. On mismatch the token is discarded, the auth file is left untouched, the Gitea logout URL (`/user/logout?redirect_to=/user/login`) is opened in the browser, and the CLI exits non-zero with recovery steps. ## After `just clone-orchestrator` ```bash cd ./forge-stack-orchestrator # or $FORGE_WORKSPACE_ROOT/ just bootstrap just repos-sync just dev-env-refresh just test-all ``` `just login` already wrote the OAuth token to the shared auth file, so the orchestrator's recipes reuse it without a second login. If the orchestrator reports a missing gateway bearer, run `just repos-login` inside the orchestrator checkout; it layers a gateway bearer on top of the existing Gitea token without a second Gitea login. ## `just login` internals 1. Reads `FORGE_GITEA_URL`, `FSDGG_CLI_CLIENT_ID`, optional `FORGE_GITEA_USERNAME` from `.env`. 2. GET `/.well-known/openid-configuration`; no endpoints are hardcoded. 3. PKCE verifier (`secrets.token_urlsafe(64)`) and S256 challenge (RFC 7636). 4. HMAC-signed CSRF `state`; session key held in process memory only. 5. Authorise URL carries `prompt=login` and, if set, `login_hint=`. 6. Opens the default browser (or prints the URL under `--no-browser`) and starts a one-shot loopback HTTP listener on the port from `FSDGG_CLI_REDIRECT_URI`. 7. Verifies the returned state, POSTs the code + verifier to the token endpoint, receives `access_token`, `refresh_token`, `expires_in`. 8. GET `/login/oauth/userinfo` for the authenticated username. 9. If `FORGE_GITEA_USERNAME` is set and does not match, discard the token and open the Gitea logout URL (see *Wrong-user guard*). 10. Writes `~/.forge-stack-devpi-gateway-gitea/client-auth.json` atomically (`.tmp` + `chmod 0600` + `rename`). Gateway-owned fields are preserved. 11. Installs `git-credential-forge` + `forge_auth.py` under `~/.local/bin/` and sets `git config --global credential..helper`. Scope is `FORGE_GITEA_URL` only; other git hosts are untouched. ## Token refresh Access-token lifetime is set by Gitea (typically one hour). When it expires the credential helper calls the token endpoint with the stored refresh token and retries the git operation once; no user interaction. When the refresh token also expires (Gitea default: 30 days), the helper emits a one-line pointer at `just login` and the git operation fails. ## Headless and SSH hosts ```bash just setup --headless # full onboarding, no webbrowser.open just login-headless # login step only ``` Both print the authorise URL on stderr. Paste it into any browser that can reach the loopback callback port (`FSDGG_CLI_REDIRECT_URI`, default `38111`). For a remote host: ```bash ssh -L 38111:127.0.0.1:38111 # then, on : just setup --headless ``` `--no-browser` is an alias for `--headless`. `just setup --headless` refreshes silently when the access token is stale but the refresh token is still valid, avoiding the URL-paste step entirely. Combining `--headless` with `FORGE_SETUP_YES=1` while a fresh browser OAuth flow is required is contradictory: the authorise URL must be pasted into a browser, but `FORGE_SETUP_YES=1` forbids interaction. `just setup` detects this and exits immediately rather than hanging. Either drop `FORGE_SETUP_YES=1`, or run `just login` once on a host with a browser to populate a valid refresh token before running `just setup --headless` on the headless host. ## Troubleshooting | Symptom | Resolution | | --- | --- | | `just doctor` reports a missing tool | Run the `fix:` command printed beside it. | | `~/.local/bin` not on `PATH` | Add `export PATH="$HOME/.local/bin:$PATH"` to the shell rc and reopen. | | `just check-gitea` → connection refused | Verify `FORGE_GITEA_URL`, confirm it is `https://gitea.cvgitea.ddns.net` for production, and do not append `:6006`. | | `just login` → browser does not open | Run `just login-headless`. | | `just login` → timed out waiting for OAuth callback | Consent was not completed in the browser; re-run. | | `just login` → cannot bind `127.0.0.1:38111` | Another `just login` is running; wait or kill it. | | "Why is the redirect URI `http://127.0.0.1`? The gateway is remote and HTTPS." | `FSDGG_CLI_REDIRECT_URI` is the CLI's local loopback listener, not the Gitea or gateway URL. OAuth 2.0 for Native Apps (RFC 8252 §7.3) requires `http` on a loopback address. The Gitea server (`FORGE_GITEA_URL`) is the remote HTTPS endpoint, reached during the authorise step. On a remote host, SSH-forward the redirect port. | | `just setup --headless` → "cannot complete a fresh login under --headless + FORGE_SETUP_YES=1" | Drop `FORGE_SETUP_YES=1`, or run `just login` once on a host with a browser to populate a refresh token, then re-run `just setup --headless`. | | `just login` rejects with a username-mismatch error | Follow the logout link printed, sign in as `FORGE_GITEA_USERNAME`, re-run. | | `just check-access` → `Repository not found` | Account not in the `codevalet` org yet. | | `just check-access` → asks for a password | `just login` did not complete. Re-run. | | Git prompts for a password on pull/push | Refresh token expired. Run `just relogin`. | | `just status` shows `live: False` | Run `just refresh`; also happens automatically on the next git op. | | `just clone-orchestrator` prints `already cloned` | Intended; idempotent. | | `just login` exits with `Gitea server_error: "a grant exists with different scope"` | Run `just revoke-grant` (opens `/user/settings/applications` and prints the matching `FSDGG_CLI_CLIENT_ID`). Revoke the matching app, then re-run `just login`. Required only once after a scope-set change. Full reference: `docs/oauth-grant-scope-mismatch.md`. | | Reset local state | `just uninstall`. | ## Security properties - Only a public OAuth client ID ships in `.env.example`; PKCE removes the need for a client secret. - `.env` holds configuration only. Tokens live in `~/.forge-stack-devpi-gateway-gitea/client-auth.json` at mode `0600`. - The credential helper is scoped to `FORGE_GITEA_URL`; requests for other hosts flow through git's normal credential chain. - The OAuth `state` is HMAC-signed with an in-memory session key; a replayed state from another session does not verify. - Writes to the auth file are atomic (`.tmp` + `rename`); a crash during `just login` or `just refresh` leaves the previous valid state intact. - `just check-access` and `just clone-orchestrator` neuter `GIT_TERMINAL_PROMPT`, `GIT_ASKPASS`, and the VSCode / X11 askpass variables so auth failures surface loudly instead of triggering GUI credential prompts. ## Tests ```bash just test ``` See `tests/README.md`.