feat(git-identity): import an existing SSH key and sign commits with it

Settings → Git gains Commit signing: one picker signs every commit
(terminal, Changes panel, agents) with one of the account's keys —
gpg.format ssh, user.signingkey = the key's private file, commit.gpgsign
true in its own ~/.gitconfig; a signing key the user set up themselves is
replaced only after a confirm (EFOREIGNSIGN without `replace`). Import a key
you have takes a key file or paste (OpenSSH or PEM, ed25519 or RSA >= 2048)
into ~/.config/agenthub/ssh/ as a named key, written like a generated one
(member: write-as, 0600), and by default signs with it in the same step.
A passphrase key, a .pub, an ecdsa key, a mismatched paste and the same key
twice are refused; only the public half ever comes back. Deleting the
signing key stops signing so commits keep working.

The SSH tab is renamed "Git (GitHub, GitLab, Gitea, Backlog…)" with the
Gitea and Backlog steps, and the File Explorer's ESECRET toast now says
that top-level dot folders of a home are edited in a terminal, and where
the SSH keys live instead.
This commit is contained in:
2026-11-02 16:12:57 +07:00
parent 2d6c751fb2
commit 7e41c81d53
15 changed files with 988 additions and 24 deletions
+39 -7
View File
@@ -3356,6 +3356,22 @@ export const api = {
/** DELETE /api/me/git/ssh-key?id= — one key by its id (a named one). */
removeKeyById: (id: string) =>
request<GitIdentity>(`/me/git/ssh-key?id=${encodeURIComponent(id)}`, { method: "DELETE" }),
/**
* POST /api/me/git/ssh-key/import — a private key the user already has
* (the file's text); `sign` signs commits with it at once, `replace`
* over a signing key they set themselves. Only its public half returns.
*/
importKey: (body: { privateKey: string; name?: string; sign?: boolean; replace?: boolean }) =>
request<GitIdentity & { imported: GitSshKey | null; signError: ApiErrorBody | null }>(
"/me/git/ssh-key/import",
{ method: "POST", body: JSON.stringify(body) }
),
/** PUT /api/me/git/signing — sign commits with key `key` (its id), or stop (null). */
setSigning: (key: string | null, replace = false) =>
request<GitIdentity>("/me/git/signing", {
method: "PUT",
body: JSON.stringify({ key, replace }),
}),
/** PUT /api/me/git/ssh-terminal — use the key for git typed in the account's terminal. */
setTerminalSsh: (on: boolean) =>
request<GitIdentity>("/me/git/ssh-terminal", { method: "PUT", body: JSON.stringify({ on }) }),
@@ -5232,6 +5248,21 @@ export interface GitCredential {
hasToken: true;
}
/** One SSH key of `GET /api/me/git` — its public half only. */
export interface GitSshKey {
id?: string;
type?: "ed25519" | "rsa";
name?: string | null;
publicKey: string;
fingerprint: string;
}
/** An API error's `{code, message}`. */
export interface ApiErrorBody {
code: string;
message: string;
}
/** `GET /api/me/git`: the user's own SSH key (public half only) and HTTPS logins. */
export interface GitIdentity {
/** `member`: the user's own Linux account (S3); `host`: the shared host account. */
@@ -5249,13 +5280,14 @@ export interface GitIdentity {
* Every key the account holds: the default ED25519 and RSA first, then the
* named ones (`name`; `id` = its file name); absent from an older server.
*/
sshKeys?: {
id?: string;
type?: "ed25519" | "rsa";
name?: string | null;
publicKey: string;
fingerprint: string;
}[];
sshKeys?: GitSshKey[];
/**
* Commit signing (`gpg.format ssh` + `user.signingkey` + `commit.gpgsign`
* in `~/.gitconfig`): `on` with one of these keys (`key` = its id), `off`,
* or `foreign` — a signing key the user set themselves (`path`). Absent
* from an older server.
*/
signing?: { state: "on" | "off" | "foreign"; key: string | null; path: string | null };
/** The terminal switch (shared by both keys); null without a key. */
sshTerminal?: "on" | "off" | "foreign" | null;
/**