fix(agent-team): authenticate git push with Basic auth, not Bearer

The box-side dispatch smoke test failed at `git clone` (exit 128, "could not
read Username"): GitHub's git smart-HTTP transport authenticates an installation
token via BASIC auth (username x-access-token), not Bearer. Bearer is the REST
API form (minting + workflow_dispatch + run-list all use it correctly) but git
rejects it.

app_branch_pusher now sets the http.extraHeader to
`Authorization: Basic <base64("x-access-token:" + token)>`. Verified on the box:
Bearer -> exit 128, Basic -> clone OK. _scrub now also redacts the base64
credential blob (it decodes to the token). Test updated to assert the Basic form
and that the raw token never appears literally in the header.
This commit is contained in:
Adam Moussa 2026-06-24 18:03:59 -04:00
parent 7c2303a76c
commit 265e32d9c9
2 changed files with 23 additions and 11 deletions

View file

@ -604,21 +604,27 @@ def app_branch_pusher(token_provider: Any, *, _run: Any = None) -> BranchPusher:
token = token_provider.token()
# Plain remote — the token is NEVER in the URL/argv/stored origin.
remote = f"https://github.com/{owner}/{repo}.git"
# Auth is injected via http.extraHeader through git's GIT_CONFIG_* env
# vars (NOT argv) on the network steps (clone + push). Env is visible
# only to the same uid via /proc/<pid>/environ, never via ps argv. The
# header key is SCOPED to the github.com host
# GitHub's git smart-HTTP transport authenticates an installation token via
# BASIC auth (username ``x-access-token``), NOT Bearer — Bearer is the REST
# API form and git rejects it ("could not read Username" → exit 128). Encode
# ``x-access-token:<token>`` and inject it as an Authorization header through
# git's GIT_CONFIG_* env vars (NOT argv) on the network steps (clone + push).
# Env is visible only to the same uid via /proc/<pid>/environ, never via ps
# argv. The header key is SCOPED to the github.com host
# (``http.https://github.com/.extraHeader``, the GitHub-Actions checkout
# pattern) so git never sends the Authorization header to any other host
# it might be redirected to.
# pattern) so git never sends the Authorization header to any other host it
# might be redirected to.
basic = base64.b64encode(f"x-access-token:{token}".encode()).decode("ascii")
auth_env = {
"GIT_CONFIG_COUNT": "1",
"GIT_CONFIG_KEY_0": "http.https://github.com/.extraHeader",
"GIT_CONFIG_VALUE_0": f"Authorization: Bearer {token}",
"GIT_CONFIG_VALUE_0": f"Authorization: Basic {basic}",
}
def _scrub(s: str) -> str:
return s.replace(token, "***")
# Scrub BOTH the raw token and the base64 credential blob (which decodes
# to the token) so neither can survive in any surfaced error message.
return s.replace(token, "***").replace(basic, "***")
with tempfile.TemporaryDirectory() as tmp:
tmpdir = Path(tmp)

View file

@ -374,12 +374,18 @@ def test_app_branch_pusher_keeps_token_out_of_argv_and_uses_plain_remote() -> No
for arg in call["cmd"]:
assert "ghs_TESTTOKEN" not in arg
# Auth rides in GIT_CONFIG_* env on the network steps only (clone + push).
# Auth rides in GIT_CONFIG_* env on the network steps only (clone + push), as
# BASIC auth (username x-access-token) — git smart-HTTP rejects Bearer.
expected_basic = "Authorization: Basic " + base64.b64encode(
b"x-access-token:ghs_TESTTOKEN"
).decode("ascii")
clone_env = recorded[0]["env"]
assert clone_env["GIT_CONFIG_KEY_0"] == "http.https://github.com/.extraHeader"
assert clone_env["GIT_CONFIG_VALUE_0"] == "Authorization: Bearer ghs_TESTTOKEN"
assert clone_env["GIT_CONFIG_VALUE_0"] == expected_basic
push_env = recorded[-1]["env"]
assert push_env["GIT_CONFIG_VALUE_0"] == "Authorization: Bearer ghs_TESTTOKEN"
assert push_env["GIT_CONFIG_VALUE_0"] == expected_basic
# The raw token never appears literally in the auth header (it is base64'd).
assert "ghs_TESTTOKEN" not in clone_env["GIT_CONFIG_VALUE_0"]
# The non-network steps (checkout/apply/commit) carry no auth env.
for call in recorded[1:4]:
assert call["env"] is None