f514602697
Part A — a fetch/pull/push against a remote that needs a login no longer dies on "terminal prompts disabled": the server answers 409 EGITAUTH with the host, the git bar asks for a username + password/access token and resends the same action. AgentHub keeps its OWN credential store (<CLAUDE_HOME>/agenthub-git-credentials/, 0700/0600), one login per host, shared by every user of the install, managed from Settings -> Git credentials. A login is only sent to one of the repo's own remote hosts, saved only after git accepted it, never returned by any route, and reaches git through an exclusive credential helper (never argv). Part B — merge a branch (or the diverged upstream) from the git bar; a merge that stops on conflicts lists them in the Changes panel with Keep mine / Take theirs / Mark resolved, then Complete merge or Abort merge (confirm gate). Clean tree, repo-root lane and an existing ref only; pull/switch/hunk actions are blocked mid-merge; staging a conflicted file with markers left is refused. Rebase/reset/stash stay out.
58 lines
3.1 KiB
Markdown
58 lines
3.1 KiB
Markdown
# Git credentials for dashboard fetch/pull/push (issue #45, part A)
|
|
|
|
## Problem
|
|
|
|
The dashboard runs git with no terminal and `GIT_TERMINAL_PROMPT=0`. When no
|
|
credential helper on the machine holds a credential for the remote's host, git
|
|
wants to prompt, cannot, and fails:
|
|
`fatal: could not read Username for 'https://git.smartgift.io.vn': terminal prompts disabled`.
|
|
Reproduced with `git -c credential.helper= push --dry-run`.
|
|
|
|
## Decisions (approved 2026-09-25)
|
|
|
|
- AgentHub keeps its OWN credentials, shared by everyone using the AgentHub
|
|
install (multi-user login is planned; accounts and settings are shared).
|
|
- The dashboard asks for a credential when git needs one, then retries.
|
|
- Merge and conflict resolution is part B and gets its own design.
|
|
|
|
## Design
|
|
|
|
**Store** — `server/lib/git-credentials.js`, one file
|
|
`<CLAUDE_HOME>/agenthub-git-credentials/credentials.json` (dir 0700, file 0600,
|
|
written through a temp file), one record per `protocol + host`:
|
|
`{protocol, host, username, password, savedAt}`. No route returns `password`;
|
|
the list reports `hasToken`. Routes: `GET /api/git-credentials`,
|
|
`DELETE /api/git-credentials/:protocol/:host`.
|
|
|
|
**Delivery** — `server/lib/git-credential-helper.js`, a git credential helper
|
|
speaking git's own protocol. For a network action, when AgentHub holds a
|
|
credential for one of the repo's remote hosts, or the request carries one, git
|
|
runs with `-c credential.helper= -c credential.helper=<ours>`: ONLY our helper.
|
|
The machine's helpers are left out on purpose — a stale token there would win
|
|
over the one just typed, and git's post-success `store` would copy a
|
|
"use once" token into the OS user's own helper. With nothing from AgentHub for
|
|
the host, the command is unchanged. The secret never reaches argv: the helper
|
|
reads the store file, or `AGENTHUB_GIT_ONCE` in that one git process's env.
|
|
|
|
**Flow** — an auth failure (`could not read Username|Password`,
|
|
`Authentication failed`, `terminal prompts disabled`, `Invalid username or
|
|
password`) answers `409 EGITAUTH` with `{protocol, host, username?}`. The git
|
|
bar shows a form (username, password/token, "remember for everyone on this
|
|
AgentHub", on by default) and resends the action with
|
|
`credentials: {protocol, host, username, password, remember}`. The server
|
|
accepts it only for a host that is one of the repo's remotes, runs the action,
|
|
and saves the credential only after git succeeded and `remember` is set.
|
|
|
|
**Limits** — one credential per host, so everyone on one AgentHub pushes as
|
|
the same account. `fetch --all` over remotes on several hosts: a host with no
|
|
AgentHub credential gets no machine helper either while one does. Over plain
|
|
HTTP on a LAN the token crosses the network unencrypted; the form says so.
|
|
|
|
## Testing
|
|
|
|
Store: save/list/remove, 0600, list never carries the password, bad host
|
|
refused. Helper: real `git credential fill` through our helper, stored and
|
|
once. Route: a local HTTP remote answering 401 → `EGITAUTH` with the host; a
|
|
resend with credentials reaches that server as Basic auth; nothing saved on
|
|
failure. Client: the form appears on `EGITAUTH`, resends, never renders a token.
|