Repository navigation
feat(git): credential helper for every provider so hooks and the agent can clone any private repository - #1070
Conversation
…t can clone any private repository
|
@OpenHands /codereview-roasted Review exactly commit 5a0a808. If the PR head changes, report that and do not approve another commit. Review only: do not push code, merge, deploy, or change attribution. Check correctness, regression coverage, and whether the PR validation claims match the evidence. Keep code comments short. Post the substantive review and link it in your completion message. End the substantive review with these plain lines (no code fence), choosing one verdict: REVIEWED_HEAD: 5a0a808 Use VERDICT: CHANGES_REQUESTED instead when findings require changes. If unable to review, explain the blocker without an approval verdict. |
|
Uh oh! There was an unexpected error starting the job :( |
…t can clone any private repository
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@OpenHands /codereview-roasted Review exactly commit 1393ab8. If the PR head changes, report that and do not approve another commit. Review only: do not push code, merge, deploy, or change attribution. Check correctness, regression coverage, and whether the PR validation claims match the evidence. Keep code comments short. Post the substantive review and link it in your completion message. End the substantive review with these plain lines (no code fence), choosing one verdict: REVIEWED_HEAD: 1393ab8 Use VERDICT: CHANGES_REQUESTED instead when findings require changes. If unable to review, explain the blocker without an approval verdict. |
|
Uh oh! There was an unexpected error starting the job :( |
|
@OpenHands /codereview-roasted Review exactly commit 1393ab8. If the PR head changes, report that and do not approve another commit. Review only: do not push code, merge, deploy, or change attribution. Check correctness, regression coverage, and whether the PR validation claims match the evidence. Keep code comments short. Post the substantive review and link it in your completion message. End the substantive review with these plain lines (no code fence), choosing one verdict: REVIEWED_HEAD: 1393ab8 Use VERDICT: CHANGES_REQUESTED instead when findings require changes. If unable to review, explain the blocker without an approval verdict. |
|
Uh oh! There was an unexpected error starting the job :( |
|
@OpenHands /codereview-roasted Review exactly commit 1393ab8. If the PR head changes, report that and do not approve another commit. Review only: do not push code, merge, deploy, or change attribution. Check correctness, regression coverage, and whether the PR validation claims match the evidence. Keep code comments short. Post the substantive review and link it in your completion message. End the substantive review with these plain lines (no code fence), choosing one verdict: REVIEWED_HEAD: 1393ab8 Use VERDICT: CHANGES_REQUESTED instead when findings require changes. If unable to review, explain the blocker without an approval verdict. |
|
Uh oh! There was an unexpected error starting the job :( |
|
@OpenHands /codereview-roasted Review exactly commit 1393ab8. If the PR head changes, report that and do not approve another commit. Review only: do not push code, merge, deploy, or change attribution. Check correctness, regression coverage, and whether the PR validation claims match the evidence. Keep code comments short. Post the substantive review and link it in your completion message. End the substantive review with these plain lines (no code fence), choosing one verdict: REVIEWED_HEAD: 1393ab8 Use VERDICT: CHANGES_REQUESTED instead when findings require changes. If unable to review, explain the blocker without an approval verdict. |
|
I'm on it! ak684 can track my progress at all-hands.dev |
ak684
left a comment
There was a problem hiding this comment.
Code review — 1393ab8
Taste: good. The change reuses the existing Azure DevOps mechanism instead of inventing a second one, adds no new config surface, and keeps the new behaviour strictly additive.
Material findings
None. I could not produce a failure that a user, caller, or acceptance criterion would hit on this head.
What I verified on this head
- Target: head
1393ab8bed571356b29e970b9afa8c92fd4e8f7b(PR is a draft, base3b2db47). Reviewed only this commit. - Tests:
uv run pytest tests/unit/app_server tests/unit/integrations→ 3419 passed (matches the PR claim). Ran with a real PostgreSQL container. - Lint/type:
pre-commit ... --files <changed files>→ ruff, ruff-format, mypy all pass (matches the claim). CI on1393ab8is green, incl. all four Python shards andLint python. get_git_hostrefactor: the extracted block is logically identical to the inline code it replaced (same domain resolution, same ADO exception, same protocol detection before normalisation, same/api/…strip). Only callers areget_authenticated_git_urland the new helper, andProviderHandlerhas no subclasses overridingPROVIDER_DOMAINS. ADO's clone-URL path is untouched apart from a docstring mention.- Helper semantics, run for real: git invokes
!"<helper>" <provider>→<helper> <provider> get;$helperis expanded at config-write time, so git stores the absolute path. A bare token becomespassword=and git suppliescredential.<host>.username; auser:secrettoken supplies its own username; colons inside the password survive;store/eraseand non-getshort-circuit. Confirmed withgit credential fillagainst a stand-in helper. - Base URL: the helper derives
base_urlfromOH_WEBHOOKS_0_BASE_URL(which the sandboxes set to…/api/v1/webhooks) or fromweb_url, matching the ADO helper. - Secrets handling: the JWT is a reference (
user_id+provider_type, 12 h), not the provider token. Files are 600, dir/helper 700, the token is never logged (onlyprovider@host), every interpolation isshlex-quoted, and the JWT is registered as a masked secret exactly likeOH_AZURE_DEVOPS_SECRETS_ACCESS_TOKEN. - Live evidence matches the description.
baseline-sandbox-probe.json(sha-40d51ea):credential_helpersempty, 5/5 clones exit 128.candidate-*(sha-1393ab8, i.e. this head): 5/5 clones exit 0 at the expected HEADs, reads succeed, remotes are plain URLs, andcredential.https://gitlab.platform-team.all-hands.dev.helperis present. The screenshot showsHook: SessionStart ok. The evidence directory's hashes are listed inmanifest.json.
Non-blocking notes
- PR template: the
## Typesection is missing (this repo's triage gate). The PR is already a draft, which the guide treats as the preferred state, so nothing to fix mechanically — but add it before marking ready. - Sandbox reuse: under
GROUP_BY_NEWEST/ADD_TO_ANYa reused sandbox keeps the previous conversation's<provider>.tokenandcredential.<host>.helperwhen a provider token is later removed. Benign — the exchange resolves the current token, so a removed token yields 404 → the helper exits 0 → clean auth failure — and it mirrors the existing ADO behaviour. Worth an explicit cleanup someday, not a blocker. - Mint scope:
_mint_git_credential_access_tokensiterates every entry inprovider_handler.provider_tokensrather than filtering empty stored tokens. SaaS never carries empty-token providers, so "one per provider the user has a token for" holds there; an OSS entry with an empty token mints an unused JWT (empty host → skipped, or 404 at exchange). Harmless. - ADO parity claim: "the same check on Azure DevOps (unchanged path) passes on the current release" has no linked artifact in the evidence dir. The path is genuinely unchanged, so it is supported by inspection rather than a captured run.
- Forgejo username: the helper uses
x-access-tokenwhile Forgejo's clone URL puts the token in the username slot. Gitea/Forgejo accept any username with a token as the password, so it works, but "their usernames follow the existing clone-URL conventions" is imprecise for Forgejo. No live fixture (disclosed).
[RISK ASSESSMENT]
MEDIUM–HIGH. This is credential handling applied to every git provider on every conversation start, so a mistake would widen who can clone what. Mitigations are strong: the on-disk artefact is a short-lived reference JWT (not the provider token), it is masked in output and mode-600, and the mechanism is the already-shipped ADO one. Remaining validation gap: no live run for Forgejo or Bitbucket Cloud, and Bitbucket DC's username depends on an API call. Recommend a human check on those (plus an ADO re-run) before this leaves draft.
Key architectural insight: making the credential a reference (JWT → exchange → current token) rather than a copied secret is what lets one helper serve every repository on a host while still following token rotation and revocation.
This review was produced by an AI agent (OpenHands) on behalf of the requester.
REVIEWED_HEAD: 1393ab8
REVIEW_REQUEST: 59122580d71b43efbeae58effe75e0c1
VERDICT: APPROVED
Review addendum — deeper checks on the two riskiest claimsStill reviewing 1. "The selected repository keeps its authenticated remote URL, which git prefers over the helper."
So the initial request is byte-identical to today and the helper is purely a fallback. No regression for the selected repo. For GitLab, Bitbucket Cloud and Bitbucket DC both parts are already in the URL, so the helper is never consulted. 2. Providers whose host contains a subpath (self-hosted GitLab
No finding, but worth knowing: the helper is scoped to the provider's subpath, so it will not serve a repository on the same host under a different path. AI-generated review addendum (OpenHands) on behalf of the requester. |
|
All requirements are now fully met, with the two riskiest claims verified by test rather than inspection. Here's the final state. Requirements check
Posted
The review first landed as What the extra analysis addedI had left two claims resting on inspection, which was the weak spot. Both are now tested on git 2.47:
Verdict remains APPROVED with no material findings. Non-blocking items stand: missing |
Review posted for
|
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Code: I found no correctness or security defect on 1393ab8. The change reuses the existing Azure DevOps design: a short-lived reference JWT sits in a mode-600 file, and the /api/v1/webhooks/secrets exchange returns the user's current token. The get_git_host extraction is a straight move of the old inline logic. The focused suites (test_app_conversation_service_base, test_live_status_app_conversation_service, test_provider_immutability, test_additional_repositories) pass locally (465 passed), and every CI check on this head is green.
I also ran the generated setup command and helper with git 2.47 against a stub server. For a selected-repo-style URL (https://<token>@host/...), git sends the embedded token first and asks the helper only after a 401. A plain URL gets x-access-token:<fresh token> from the helper. So the selected repository behaves as before, and the helper only fills in when needed.
Blocking (merge readiness, not a code defect):
- PR template. The repo's review guide (
.agents/skills/custom-codereview-guide.md, "Automated PR Triage Before Review") requires the PR body to follow.github/pull_request_template.md, including theTypesection. The current body has no## Typesection and noHUMAN:checkbox block. Please add them. This is the only thing between this PR and approval.
Non-blocking:
- One Bitbucket Data Center token format gets a different username from the helper than from the clone URL. See the inline comment.
🔄 CHANGES REQUESTED
LLM profile: all-hands-bot-reviewer_claude-opus-5-5 · Model: openhands/claude-opus-5-5
…t can clone any private repository
|
@OpenHands /codereview-roasted Review exactly commit 6204acd. If the PR head changes, report that and do not approve another commit. Review only: do not push code, merge, deploy, or change attribution. Check correctness, regression coverage, and whether the PR validation claims match the evidence. Keep code comments short. Post the substantive review and link it in your completion message. End the substantive review with these plain lines (no code fence), choosing one verdict: REVIEWED_HEAD: 6204acd Use VERDICT: CHANGES_REQUESTED instead when findings require changes. If unable to review, explain the blocker without an approval verdict. |
|
I'm on it! ak684 can track my progress at all-hands.dev |
Code review —
|
all-hands-bot
left a comment
There was a problem hiding this comment.
This review was posted by an AI agent (OpenHands).
Follow-up on my earlier review of 1393ab8. Both earlier findings are fixed on 6204acd:
- PR template (was blocking): fixed. The body now has the
HUMAN:checkbox block,## Type(Feature checked) and## Notes, as.github/pull_request_template.mdrequires. - Bitbucket Data Center
x-token-auth:<token>username (was non-blocking): fixed, and I resolved the thread. The helper now prints onlypassword=for anx-token-auth:secret. Git therefore keeps the per-hostcredential.<host>.usernamethat_git_credential_usernamelooked up. For Bitbucket Cloud the result is the same as before, because that host's configured username is alreadyx-token-auth. The new assertion runs the real helper and covers this case.
The incremental diff touches only that case branch, its docstring and the test. I found nothing new. The focused suites pass locally (465 passed), and every CI check on this head passed or was skipped.
✅ APPROVED
|
Live check with Bitbucket Data Center (self-hosted test server, Bitbucket 9.4) on the R02 test install, head
Live coverage is now GitLab (self-hosted), Bitbucket Data Center and Azure DevOps (unchanged path). GitHub, Bitbucket Cloud and Forgejo remain unit-tested only. Unrelated finding from the same run, on the stock release: a new user's first Bitbucket Data Center sign-in returns HTTP 500 at |
|
🚀 Released in 1.82.0. |
HUMAN:
AGENT:
Why
Only the selected repository has git credentials in a conversation: its remote URL embeds the user's provider token. Any other private repository on the same provider cannot be cloned with plain
git, from the agent's terminal or from a repository hook such as a.openhands/hooks.jsonSessionStart hook that clones the repositories a task needs. Azure DevOps already works differently: a global git credential helper exchanges a per-conversation JWT for the user's current token on every git call. This gives every other provider the same mechanism.Summary
$HOME/.openhands/git-credentials/, and register them as masked conversation secrets like the Azure DevOps one.GET /api/v1/webhooks/secrets, which already serves any provider type, for the user's current token, so other repositories also get refreshed tokens. A Bitbucket token stored asuser:secretsupplies its own username; a bare token or thex-token-auth:<token>form keeps the username configured for the host (for Bitbucket Data Center, the one its service resolved for that server). The selected repository keeps its authenticated remote URL, which git prefers over the helper.ProviderHandler.get_git_hostfactors the host resolution out ofget_authenticated_git_url, so the helper is configured for the same host the clone URL uses. The Azure DevOps path is unchanged.Issue Number
FDE-243
How to Test
Unit:
uv run pytest tests/unit/app_server tests/unit/integrations(3419 passed). The new tests cover per-provider minting, secret registration, helper installation per host (including the Bitbucket Data Center username lookup and a refused plain-http host), and execute the produced setup command and the installed helper for real against a stand-in exchange endpoint, including theuser:secretandx-token-auth:<token>forms. Pre-commit (ruff, format, mypy) passes.Live, on R02 (self-hosted GitLab, baseline enterprise
sha-40d51ea): a repository whose SessionStart hook clones five private projects the user owns. Baseline: the hook ran (exit 0) but every clone failed with exit 128 and no credential helper was configured. With this change (sha-1393ab8): the hook ran, all five clones are at their expected HEADs, the agent read a file from each, the sandbox hascredential.https://<gitlab host>.helperin its global git config, and the clones' remotes are plain URLs without a token. Evidence: https://github.com/ak684/oh/tree/8794f0d0213b074d50a76c5e53c828eac1187535/runs/hook-clones-r02-gitlab-20261028. The same check on Azure DevOps (unchanged path) passes on the current release. Forgejo and Bitbucket Cloud have no live fixture; their usernames follow the existing clone-URL conventions.Video/Screenshots
Screenshot of the candidate run (hook ok, five clones read) in the evidence link above:
candidate-hook-clones-finished.png.Type
Notes
No migration or configuration change. The helper is installed only when a provider token exists for the user and
web_urlis configured; without them the sandbox is untouched. The exchange JWTs live 12 hours like the Azure DevOps one and are scoped to the user and provider.Enterprise server image for this PR: