fix(git): supply the stored HTTPS token to every git invocation - #35
Merged
Conversation
Capstan encrypted the git HTTPS token, stored it and reported
hasHttpsToken=true, but never handed it to a git process. Every pull of a
private HTTPS repository died with:
fatal: could not read Username for 'https://github.com'
gitCommand() exec'd plain git with no credential helper, no askpass, no
authenticated URL and no environment (git.go:197).
The credential now reaches git through an inline credential.helper snippet
installed with -c for the duration of one invocation, reading the secret out
of the child process environment. That keeps it out of argv (readable via
/proc/<pid>/cmdline), out of .git/config on disk — and therefore out of the
stack's backup snapshot — and out of anything that prints the remote.
Rewriting origin to https://user:token@host was rejected for exactly those
reasons.
gitCmd() is now the single place the backend executes git, so the credential
applies to pull, fetch, status and every other subcommand without a
hand-maintained list of the ones that reach the network. Settings win over
the GIT_HTTPS_TOKEN environment variable; with nothing configured the
invocation is unchanged apart from GIT_TERMINAL_PROMPT=0, which turns a
would-be hang into an error.
git output is scrubbed of the token before it reaches an error string, since
that text travels into AppError.Details, the action log and the API response.
Tests run git's own smart-HTTP server (git-http-backend over CGI) behind
Basic auth, so the pull is proved against a remote that genuinely demands
credentials. Verified to fail for the right reason against the unfixed code:
"fatal: could not read Username for 'http://127.0.0.1:38441': terminal
prompts disabled".
agent-os-qqw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes
agent-os-qqw. Unblocks the last acceptance criterion ofagent-os-36o(P0).The bug
Capstan collects a git HTTPS token, encrypts it with
STORAGE_KEY, stores it and reportshasHttpsToken: truein Settings — then never supplies it to any git operation.gitCommand()(backend/internal/services/git.go:197onmain) exec'd plain git with no credential helper, no askpass, no authenticated URL and no environment.git_https_tokenwas read and written only by the settings handler and referenced nowhere ininternal/services.Observed during the
agent-os-36oDR drill, inside a running container, against a real clone of a private repo whose token had been stored through the API:The fix
An inline
credential.helpershell snippet, installed with-cfor the duration of a single invocation, answering only thegetoperation and reading the secret out of the child process environment.The rejected alternatives, and why:
https://user:token@host.git/configon disk, into that stack's backup snapshot, and into every log line or API response that prints the remote-c http.<remote>.extraheader="Authorization: Basic ..."/proc/<pid>/cmdlineGIT_ASKPASSscriptThe snippet contains only variable names, so argv stays clean, and
-cmeans nothing is written to any config file.gitCmd()is now the single place the backend executes git —grep -rn '"git"' --include=*.go internal/ cmd/returns one non-test hit — so the credential covers pull, fetch, status and everything else without a hand-maintained list of the subcommands that reach the network. An omission from such a list is precisely the failure mode being fixed.Settings win over the
GIT_HTTPS_TOKENenvironment variable. With nothing configured the invocation is unchanged apart fromGIT_TERMINAL_PROMPT=0, which turns a would-be hang into an error.git's output is scrubbed of the token before it reaches an error string; that text travels into
AppError.Details, the action log and the API response.Tests
The tests run git's own smart-HTTP server (
git-http-backendover CGI) behind Basic auth, so the pull is proved against a remote that genuinely demands credentials. Nothing leaves the machine and no real token is involved.TestPull_UsesStoredHTTPSTokenTestPull_WithoutStoredTokenStillFailsTestPull_TokenNeverPersistsToGitConfig.git/configand from the origin URL after a pullTestGitCmd_TokenTravelsInEnvNotArgvTestGitCmd_NoCredentialWhenNoneConfiguredTestGitCommand_RedactsTokenFromOutputTestHTTPSCredentials_SettingsBeatEnvironmentConfirmed to fail for the right reason against the unfixed code, per the standing rule that a bug fix needs a test that would not also have passed before it:
Deliberately reverting
gitCommand()to itsmainbody failsTestPull_UsesStoredHTTPSToken,TestPull_TokenNeverPersistsToGitConfigandTestGitCommand_RedactsTokenFromOutput, and passes the four that test the new helper in isolation.Gates
Frontend untouched.
Not in scope
Two adjacent problems surfaced while reading this code, both filed separately rather than folded in:
UpdateDirectoryCredentials) are written but never read back —GetDirectoryblanks the token on the way out, so no caller can reach it;UpsertDirectoryisINSERT OR REPLACEover amodels.Directorybuilt by the scanner with empty credential fields, so every scan wipes any per-directory credential that was stored.Only the global Settings token is wired up here, which is what
agent-os-qqw's acceptance criterion covers.