From afc1f3a66587a526371985a537cf6b5e990eca50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:50:49 +0000 Subject: [PATCH 1/4] Initial plan From b2da6c891828fd87619f95e0893eca465430bbf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:54:02 +0000 Subject: [PATCH 2/4] Add SSH key fallback authentication for git push/pull - Implement createSSHCredentials helper that tries SSH agent first, then falls back to direct key files - Support common SSH key types: id_ed25519, id_rsa, id_ecdsa, id_dsa - Update both push and fetch credential callbacks to use new helper - Update user documentation with SSH authentication details and troubleshooting Co-authored-by: kristinaquinones <5562083+kristinaquinones@users.noreply.github.com> --- tibok/tibok/Services/LibGit2Service.swift | 59 +++++++++++++++++++-- tibok/user_docs/features/git-integration.md | 42 ++++++++++++++- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/tibok/tibok/Services/LibGit2Service.swift b/tibok/tibok/Services/LibGit2Service.swift index ff83296..fe9a79c 100644 --- a/tibok/tibok/Services/LibGit2Service.swift +++ b/tibok/tibok/Services/LibGit2Service.swift @@ -785,6 +785,58 @@ class LibGit2Service: ObservableObject { // MARK: - Remote Operations + /// Helper function to create SSH credentials with fallback + /// Tries SSH agent first, then falls back to reading SSH keys from ~/.ssh directory + private static func createSSHCredentials( + out: UnsafeMutablePointer?, + username: UnsafePointer? + ) -> Int32 { + // Try SSH agent first (current behavior) + let agentResult = git_credential_ssh_key_from_agent(out, username) + if agentResult == 0 { + return agentResult + } + + // SSH agent failed, try loading keys directly from ~/.ssh + let homeDir = FileManager.default.homeDirectoryForCurrentUser.path + let sshDir = "\(homeDir)/.ssh" + + // Common SSH key filenames in order of preference + let keyFiles = [ + "id_ed25519", // Ed25519 (modern, recommended) + "id_rsa", // RSA (most common) + "id_ecdsa", // ECDSA + "id_dsa" // DSA (legacy) + ] + + for keyFile in keyFiles { + let privateKeyPath = "\(sshDir)/\(keyFile)" + let publicKeyPath = "\(sshDir)/\(keyFile).pub" + + // Check if both private and public key files exist + if FileManager.default.fileExists(atPath: privateKeyPath) && + FileManager.default.fileExists(atPath: publicKeyPath) { + + // Try to create credential with this key pair + // Pass empty string for passphrase (will prompt if needed, or fail gracefully) + let result = git_credential_ssh_key_new( + out, + username, + publicKeyPath, + privateKeyPath, + "" // Empty passphrase - libgit2 will handle encrypted keys + ) + + if result == 0 { + return result + } + } + } + + // All methods failed + return GIT_PASSTHROUGH.rawValue + } + /// Push to remote func push(in repoURL: URL) -> (success: Bool, error: String?, alreadyUpToDate: Bool) { do { @@ -820,9 +872,9 @@ class LibGit2Service: ObservableObject { // Set up credentials callback for SSH opts.callbacks.credentials = { (out, url, username_from_url, allowed_types, payload) -> Int32 in - // Try SSH agent first + // Try SSH authentication (agent first, then direct key files) if allowed_types & GIT_CREDENTIAL_SSH_KEY.rawValue != 0 { - return git_credential_ssh_key_from_agent(out, username_from_url) + return LibGit2Service.createSSHCredentials(out: out, username: username_from_url) } return GIT_PASSTHROUGH.rawValue } @@ -881,8 +933,9 @@ class LibGit2Service: ObservableObject { // Set up credentials callback opts.callbacks.credentials = { (out, url, username_from_url, allowed_types, payload) -> Int32 in + // Try SSH authentication (agent first, then direct key files) if allowed_types & GIT_CREDENTIAL_SSH_KEY.rawValue != 0 { - return git_credential_ssh_key_from_agent(out, username_from_url) + return LibGit2Service.createSSHCredentials(out: out, username: username_from_url) } return GIT_PASSTHROUGH.rawValue } diff --git a/tibok/user_docs/features/git-integration.md b/tibok/user_docs/features/git-integration.md index 17ed578..53df06d 100644 --- a/tibok/user_docs/features/git-integration.md +++ b/tibok/user_docs/features/git-integration.md @@ -170,18 +170,56 @@ The Git menu provides access to all Git operations: ## How It Works -tibok uses your system's Git installation (`/usr/bin/git`) to execute commands. This means: +tibok uses libgit2 for native Git operations. This means: - **Authentication**: Uses your existing SSH keys and credentials - **Configuration**: Respects your `.gitconfig` settings - **Compatibility**: Works with any Git remote (GitHub, GitLab, Bitbucket, etc.) +### SSH Authentication + +For SSH remotes (git@github.com:user/repo.git), tibok supports multiple authentication methods: + +1. **SSH Agent** - First tries to use keys from ssh-agent +2. **Direct Keys** - Falls back to reading keys from `~/.ssh/` directory + +Supported key types (in order of preference): +- `~/.ssh/id_ed25519` (Ed25519 - recommended) +- `~/.ssh/id_rsa` (RSA - most common) +- `~/.ssh/id_ecdsa` (ECDSA) +- `~/.ssh/id_dsa` (DSA - legacy) + +**Note**: If your SSH key has a passphrase, you may need to add it to ssh-agent first: +```bash +ssh-add ~/.ssh/id_rsa +``` + If `git push` works in Terminal, it will work in tibok. ## Requirements - Git must be installed on your system (included with Xcode Command Line Tools) -- For push/pull: Git credentials configured (SSH keys or credential helper) +- For push/pull with SSH: SSH keys configured in `~/.ssh/` directory +- For push/pull with HTTPS: Git credential helper configured + +## Troubleshooting + +### Push/Pull Fails with Authentication Error + +**Error**: "Authentication failed. Check your SSH keys are added to ssh-agent." + +**Solutions**: +1. Ensure your SSH key exists in `~/.ssh/` directory with matching `.pub` file +2. For encrypted keys, add to ssh-agent: `ssh-add ~/.ssh/id_rsa` +3. Test SSH connection: `ssh -T git@github.com` +4. Verify remote URL uses SSH format: check with `git remote -v` in Terminal + +### Permission Denied (publickey) + +This means your SSH key is not authorized on the remote server: +1. Add your public key to GitHub/GitLab/Bitbucket account settings +2. Copy public key: `cat ~/.ssh/id_rsa.pub` +3. Add to your account under Settings → SSH Keys ## Tips From 1b4325a2a1682134ec3535b5e7922ea8ea96779b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:55:21 +0000 Subject: [PATCH 3/4] Fix C string conversion for SSH key paths Use withCString to properly convert Swift String paths to C strings for git_credential_ssh_key_new Co-authored-by: kristinaquinones <5562083+kristinaquinones@users.noreply.github.com> --- tibok/tibok/Services/LibGit2Service.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tibok/tibok/Services/LibGit2Service.swift b/tibok/tibok/Services/LibGit2Service.swift index fe9a79c..79dc7bd 100644 --- a/tibok/tibok/Services/LibGit2Service.swift +++ b/tibok/tibok/Services/LibGit2Service.swift @@ -819,13 +819,17 @@ class LibGit2Service: ObservableObject { // Try to create credential with this key pair // Pass empty string for passphrase (will prompt if needed, or fail gracefully) - let result = git_credential_ssh_key_new( - out, - username, - publicKeyPath, - privateKeyPath, - "" // Empty passphrase - libgit2 will handle encrypted keys - ) + let result = privateKeyPath.withCString { privateKey in + publicKeyPath.withCString { publicKey in + git_credential_ssh_key_new( + out, + username, + publicKey, + privateKey, + "" // Empty passphrase - libgit2 will handle encrypted keys + ) + } + } if result == 0 { return result From 43cc9b812d9458cf8d3790371f0533fc7b1ab4ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:58:19 +0000 Subject: [PATCH 4/4] Address code review feedback - Update error message to reflect fallback authentication - Add debug logging for SSH authentication attempts - Clarify that passphrase-protected keys require ssh-agent - Update documentation with clearer limitations and troubleshooting Co-authored-by: kristinaquinones <5562083+kristinaquinones@users.noreply.github.com> --- tibok/tibok/Services/LibGit2Service.swift | 32 ++++++++++++++++++--- tibok/user_docs/features/git-integration.md | 25 +++++++++------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/tibok/tibok/Services/LibGit2Service.swift b/tibok/tibok/Services/LibGit2Service.swift index 79dc7bd..c8c0c69 100644 --- a/tibok/tibok/Services/LibGit2Service.swift +++ b/tibok/tibok/Services/LibGit2Service.swift @@ -38,12 +38,12 @@ struct LibGit2Error: Error, LocalizedError { // Authentication errors if lowercased.contains("authentication") || lowercased.contains("credentials") { - return "Authentication failed. Check your SSH keys are added to ssh-agent." + return "Authentication failed. Ensure SSH keys exist in ~/.ssh/ directory (without passphrase) or are added to ssh-agent." } // Permission errors if lowercased.contains("permission denied") || lowercased.contains("publickey") { - return "Permission denied. Ensure your SSH key is configured correctly." + return "Permission denied. Ensure your SSH key is configured correctly and added to your Git hosting service." } // Network errors @@ -787,6 +787,7 @@ class LibGit2Service: ObservableObject { /// Helper function to create SSH credentials with fallback /// Tries SSH agent first, then falls back to reading SSH keys from ~/.ssh directory + /// Note: Direct key loading only works with unencrypted SSH keys private static func createSSHCredentials( out: UnsafeMutablePointer?, username: UnsafePointer? @@ -794,9 +795,16 @@ class LibGit2Service: ObservableObject { // Try SSH agent first (current behavior) let agentResult = git_credential_ssh_key_from_agent(out, username) if agentResult == 0 { + #if DEBUG + print("[LibGit2] SSH authentication: using key from ssh-agent") + #endif return agentResult } + #if DEBUG + print("[LibGit2] SSH agent auth failed, trying direct key files from ~/.ssh") + #endif + // SSH agent failed, try loading keys directly from ~/.ssh let homeDir = FileManager.default.homeDirectoryForCurrentUser.path let sshDir = "\(homeDir)/.ssh" @@ -817,8 +825,13 @@ class LibGit2Service: ObservableObject { if FileManager.default.fileExists(atPath: privateKeyPath) && FileManager.default.fileExists(atPath: publicKeyPath) { + #if DEBUG + print("[LibGit2] Found SSH key pair: \(keyFile)") + #endif + // Try to create credential with this key pair - // Pass empty string for passphrase (will prompt if needed, or fail gracefully) + // Note: Empty passphrase only works with unencrypted keys + // Passphrase-protected keys must be added to ssh-agent let result = privateKeyPath.withCString { privateKey in publicKeyPath.withCString { publicKey in git_credential_ssh_key_new( @@ -826,17 +839,28 @@ class LibGit2Service: ObservableObject { username, publicKey, privateKey, - "" // Empty passphrase - libgit2 will handle encrypted keys + "" // Empty passphrase - only works for unencrypted keys ) } } if result == 0 { + #if DEBUG + print("[LibGit2] Successfully authenticated with \(keyFile)") + #endif return result + } else { + #if DEBUG + print("[LibGit2] Failed to authenticate with \(keyFile) (may be encrypted)") + #endif } } } + #if DEBUG + print("[LibGit2] All SSH authentication methods failed") + #endif + // All methods failed return GIT_PASSTHROUGH.rawValue } diff --git a/tibok/user_docs/features/git-integration.md b/tibok/user_docs/features/git-integration.md index 53df06d..85dbfb7 100644 --- a/tibok/user_docs/features/git-integration.md +++ b/tibok/user_docs/features/git-integration.md @@ -180,8 +180,8 @@ tibok uses libgit2 for native Git operations. This means: For SSH remotes (git@github.com:user/repo.git), tibok supports multiple authentication methods: -1. **SSH Agent** - First tries to use keys from ssh-agent -2. **Direct Keys** - Falls back to reading keys from `~/.ssh/` directory +1. **SSH Agent** - First tries to use keys from ssh-agent (supports encrypted keys) +2. **Direct Keys** - Falls back to reading unencrypted keys from `~/.ssh/` directory Supported key types (in order of preference): - `~/.ssh/id_ed25519` (Ed25519 - recommended) @@ -189,10 +189,10 @@ Supported key types (in order of preference): - `~/.ssh/id_ecdsa` (ECDSA) - `~/.ssh/id_dsa` (DSA - legacy) -**Note**: If your SSH key has a passphrase, you may need to add it to ssh-agent first: -```bash -ssh-add ~/.ssh/id_rsa -``` +**Important**: +- **Passphrase-protected keys MUST be added to ssh-agent** for authentication to work +- Unencrypted keys (no passphrase) work automatically if present in `~/.ssh/` +- To add an encrypted key to ssh-agent: `ssh-add ~/.ssh/id_rsa` If `git push` works in Terminal, it will work in tibok. @@ -206,13 +206,16 @@ If `git push` works in Terminal, it will work in tibok. ### Push/Pull Fails with Authentication Error -**Error**: "Authentication failed. Check your SSH keys are added to ssh-agent." +**Error**: "Authentication failed. Ensure SSH keys exist in ~/.ssh/ directory (without passphrase) or are added to ssh-agent." **Solutions**: -1. Ensure your SSH key exists in `~/.ssh/` directory with matching `.pub` file -2. For encrypted keys, add to ssh-agent: `ssh-add ~/.ssh/id_rsa` -3. Test SSH connection: `ssh -T git@github.com` -4. Verify remote URL uses SSH format: check with `git remote -v` in Terminal +1. **For unencrypted keys**: Ensure your SSH key exists in `~/.ssh/` directory with matching `.pub` file +2. **For passphrase-protected keys**: Add to ssh-agent with `ssh-add ~/.ssh/id_rsa` +3. **Test SSH connection**: `ssh -T git@github.com` (should authenticate successfully) +4. **Verify remote URL**: Check it uses SSH format with `git remote -v` in Terminal +5. **Check key permissions**: Private keys should have 600 permissions (`chmod 600 ~/.ssh/id_rsa`) + +**Note**: Direct key loading (method 2) only works with unencrypted SSH keys. If your key has a passphrase, you must use ssh-agent (method 1). ### Permission Denied (publickey)