forge: read the API token only from the token file #178
No reviewers
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/tools!178
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/forge-token-file-only"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What changes:
forgeno longer reads the API token from theFORGEJO_TOKENenvironment variable. The token comes only from the token file,~/.config/git/forgejo-tokenby default or the path inFORGE_TOKEN_FILE, and that file must be mode 0600.Why: an environment variable is inherited by every child process an agent spawns, so a token there reaches every build, test, and helper and can leak through env dumps, logs, and transcripts. A mode-0600 file is read only by code that deliberately opens it.
Fail loud, not silent: if
FORGEJO_TOKENis set and non-empty, everyforgecommand except help output exits 1 before making any request or starting any child process, and prints one sentence saying the variable is no longer read and the token belongs in a mode-0600 file named byFORGE_TOKEN_FILE. An empty variable is ignored as before. Nobody can believe the variable is in use when it is not.The 0600 promise is enforced: a token file with any group or other permission bit is refused before it is read, with one sentence naming the path and
chmod 600. Symlinks are judged by their target. Any deployment whose token file is group- or world-readable will get that refusal on its firstforgecall; the fix is the chmod the message names.What stays identical on this machine: nothing sets
FORGEJO_TOKENhere (not the shell environment, not/etc, not the home-manager files), and~/.config/git/forgejo-tokenis the agenix-delivered symlink whose target is-rw------- allod:users, so everyforgecall keeps working exactly as today.forge auth statusnow always names the token file as the source.What proves it: the Go tests inject tokens only through a temp file behind
FORGE_TOKEN_FILE. New tests pin that a setFORGEJO_TOKENis refused with the exact message onauth status,label listwith and without-R,token verify, and an unknown command, with zero requests recorded and the repo-inference seam failing the test if git would have been spawned; that help still prints; that 0640, 0604, 0644 and 0620 token files are refused and 0600 and 0400 accepted, through a symlink too; and that the file path alone authenticates with the file contents in theAuthorizationheader. Both new checks were shown to fail when removed.gofmt,go vet,go test(both build tags), andnix flake checkare all green.If it is wrong: revert this PR. Anyone who really did rely on the variable, or has a loose token file, sees the one-line refusal on the first
forgecall, moves the token into a 0600 file, and pointsFORGE_TOKEN_FILEat it.Closes allod/tools#57
This is option 3 from the issue: keep an environment override, but only for the file path, never for the token value.
Scope
cmd/forge/main.goruncallsrefuseEnvTokenonce afterparseGlobalArgsand beforedispatch, exempting only help requests, so the refusal is ahead oftoken verify(which never loads the configured credential) and ofrequireRepo(which spawns git to infer the repository before any command asks for a credential).cmd/forge/helpers.goloadTokenreads the file as before but first stats it (following symlinks) and refuses any group or other permission bit; the no-token message now points atFORGE_TOKEN_FILEinstead of the variable.cmd/forge/authcmd.godrops the env-var source label. Usage text (cmd/forge/usage.go) anddocs/forge.md(Config table, token paragraph,auth statusparagraph) describe the file-only model, and themain.goheader no longer claims the retired bash script is at the repository root.README.mdnever mentioned the variable, so it is unchanged.The token's path through the process is unchanged:
internal/forgeapi/client.gostates in its package comment that the token is set on an in-processhttp.RequestAuthorizationheader, is never logged, never wrapped into an error, and never leaves that file except as the outgoing header. This PR only narrows where the token comes from; where it goes is untouched.Other repos: grepping
/home/allod/work/allod(archetypes, nexus, vm, deploy, profiles, memory, inventory, secrets, strategy, .profile) forFORGEJO_TOKENfinds no consumer that sets or exports it. Every hit is insideallod/tools: theforgesources and tests changed here, thepr-explainenvironment scrub deny-list (which removes the variable from runner environments, and stays), and two shell scripts,pm/renderandpm/integrity-check, that readFORGEJO_TOKENfor their owncurlcalls and do not invokeforge. Those two need nothing here: PR allod/tools#179 deletes thepmtree entirely.tests/pr-explain/command.shkeeps exportingFORGEJO_TOKENas a must-not-leak canary. That suite only ever drives the mock forge script behindALLOD_PR_EXPLAIN_FORGE, which never reads the variable, so the canary still proves the scrub and never trips the new refusal; a comment now says so.Risk
Low. Behaviour changes only for a caller that sets
FORGEJO_TOKENor whose token file is group- or world-readable, and that caller gets an immediate, explicit exit 1 rather than a silent fallback. On this deployment the variable is not set anywhere and the token file target is 0600. A script elsewhere that relied onFORGEJO_TOKEN=... forge ...must write the token to a 0600 file and setFORGE_TOKEN_FILE.pr-explainin production calls the realforge, so an operator with a staleFORGEJO_TOKENexport would see the refusal there too; that is the intended loud failure.Validation
All run from the repo devShell in the worktree, on the committed tree at
bbf9fbc(the review fix) and earlier atd3858a2.nix develop -c gofmt -l .: emptynix develop -c go vet ./...: oknix develop -c go test ./...: ok for all six packages (cmd/allod, cmd/flake-update-cascade, cmd/forge, internal/flakelock, internal/forgeapi, internal/gitremote)nix develop -c go vet -tags site ./...: oknix develop -c go test -tags site ./...: ok for all six packagesnix flake check: all 7 checks passed (go-checks, allod-parity-tests including the pr-explain suites, flake-status-suite, flake-update-cascade-suites, and the three package builds)refuseEnvToken()call inruncommented out,go test ./cmd/forge -run TestForgejoTokenEnvRefusedfails all five refusal subtests; with the permission condition inloadTokenmade unreachable,-run TestTokenFilePermissionsfails all four loose-mode subtests and the symlink subtest; restored, both pass.go build ./cmd/forge) from/, a non-git directory:echo x | FORGEJO_TOKEN=x forge token verifyandFORGEJO_TOKEN=x forge label listboth print the refusal and exit 1 withFORGE_URLpointed at a closed port;forge auth statusagainst a 0644 token file prints the chmod refusal and exits 1;FORGEJO_TOKEN=x forge --helpprints usage and exits 0.env | grep FORGEJO_TOKENand a grep of/etc,~/.bashrc,~/.profile,~/.configon this VM find no setting of the variable;stat -L ~/.config/git/forgejo-tokenreports-rw------- allod:users.Review findings, each answered in commit
bbf9fbc(additive, no force-push):refuseEnvTokennow runs once inrun()right afterparseGlobalArgs, beforedispatch, exempting only help requests (isHelpRequest).token verifyand a command without-Rfrom a non-git directory both hit the refusal first;TestForgejoTokenEnvRefusedcoversauth status,label listwith and without-R,token verify, and an unknown command, asserting the exact message, zero recorded requests, and a repo-inference seam that fails the test if git would have been spawned, plus a help-exemption subtest. Confirmed on the real binary from/.usage.gonow says "a set, non-empty FORGEJO_TOKEN is refused, never read", matching the docs and code.loadTokenstats the token file after the readability check (os.Stat, so symlinks are judged by their target) and refuses withforge: token file <path> is readable by group or others; run chmod 600 on itwhen any group or other bit is set, before reading the contents or making a request.TestTokenFilePermissionscovers 0640, 0604, 0644, 0620 (refused), 0600 and 0400 (accepted), and a symlink whose target flips from 0600 to 0644. The PR body now states that a deployment with a loose token file gets this refusal on its first call and that the fix is the named chmod; this VM's target is-rw------- allod:users.main.goheader: now says the bash script is retired and that divergences are marked "diverges from bash".pmtree, instead of calling it a follow-up.Validation rerun on
bbf9fbcfrom the devShell:gofmt -l .empty;go vet ./...andgo vet -tags site ./...ok;go test ./...andgo test -tags site ./...ok for all six packages;nix flake checkall 7 checks passed. Sabotage: commenting out therefuseEnvToken()call fails all five refusal subtests, and disabling the permission condition fails all five loose-mode and symlink subtests; restored, both pass. The PR body's summary, Scope, Risk, and Validation sections are updated to match.bbf9fbc467toc86f577559