yatfa 1.0.79 → 1.0.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ # GitHub authentication and gh CLI wrapper setup
4
+ # Extracted from setup-agent.rb for modularity
5
+
6
+ def setup_github_auth!
7
+ rails_api_url = ENV["YATFA_API_URL"]
8
+ rails_api_key = ENV["YATFA_API_KEY"]
9
+
10
+ # Derive YATFA_API_URL from YATFA_URL if not set
11
+ unless rails_api_url && !rails_api_url.empty?
12
+ yatfa_url = ENV["YATFA_URL"]
13
+ if yatfa_url && !yatfa_url.empty?
14
+ rails_api_url = "#{yatfa_url}/api/v1"
15
+ ENV["YATFA_API_URL"] = rails_api_url
16
+ end
17
+ end
18
+
19
+ # Validate YATFA_API_URL format before proceeding
20
+ if rails_api_url && !rails_api_url.empty? && !rails_api_url.include?('/api/v1')
21
+ puts "⚠️ Warning: YATFA_API_URL must include /api/v1 path"
22
+ puts " Current: #{rails_api_url}"
23
+ puts " Expected: https://example.com/api/v1"
24
+ puts " Falling back to alternative authentication..."
25
+ rails_api_url = nil # Clear it to fall through to other methods
26
+ end
27
+
28
+ # New method: Get token from Rails backend (preferred)
29
+ if rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
30
+ puts "🔐 Configuring GitHub App authentication (via backend)..."
31
+
32
+ # Create helper script that calls backend API
33
+ helper_content = <<~RUBY
34
+ #!/usr/bin/env ruby
35
+ require 'json'
36
+ require 'net/http'
37
+ require 'uri'
38
+ require 'time'
39
+
40
+ def get_token_from_backend
41
+ rails_api_url = ENV['YATFA_API_URL']
42
+ rails_api_key = ENV['YATFA_API_KEY']
43
+
44
+ unless rails_api_url && rails_api_key
45
+ STDERR.puts "❌ Error: YATFA_API_URL or YATFA_API_KEY not set"
46
+ exit 1
47
+ end
48
+
49
+ # Build GitHub token endpoint URL
50
+ token_url = "#{rails_api_url.chomp('/')}/github/token"
51
+
52
+ uri = URI(token_url)
53
+ http = Net::HTTP.new(uri.host, uri.port)
54
+ http.use_ssl = uri.scheme == 'https'
55
+ http.open_timeout = 5
56
+ http.read_timeout = 10
57
+
58
+ request = Net::HTTP::Get.new(uri)
59
+ request['X-API-Key'] = rails_api_key
60
+ request['Accept'] = 'application/json'
61
+
62
+ response = http.request(request)
63
+
64
+ unless response.is_a?(Net::HTTPSuccess)
65
+ # Silent failure for git credential helper - let git try other auth methods
66
+ exit 0
67
+ end
68
+
69
+ data = JSON.parse(response.body)
70
+ token = data['token']
71
+
72
+ unless token
73
+ STDERR.puts "❌ Error: No token in backend response"
74
+ exit 1
75
+ end
76
+
77
+ data # Return full response including expires_at
78
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
79
+ STDERR.puts "❌ Error: Backend API timeout: \#{e.message}"
80
+ exit 1
81
+ rescue StandardError => e
82
+ STDERR.puts "❌ Error: \#{e.class} - \#{e.message}"
83
+ exit 1
84
+ end
85
+
86
+ def find_or_create_cached_token
87
+ cache_file = '/tmp/github-app-token-cache'
88
+ cached_token = read_cached_token(cache_file)
89
+ return cached_token if cached_token
90
+
91
+ token_data = get_token_from_backend
92
+ expires_at = token_data['expires_at'] ? Time.parse(token_data['expires_at']) : (Time.now + 3600 - 300)
93
+ token = token_data['token']
94
+
95
+ cache_data = {
96
+ token: token,
97
+ expires_at: expires_at.iso8601
98
+ }
99
+ File.write(cache_file, cache_data.to_json)
100
+ token
101
+ end
102
+
103
+ def read_cached_token(cache_file)
104
+ return nil unless File.exist?(cache_file)
105
+ cache = JSON.parse(File.read(cache_file))
106
+ return nil if cache['token'].nil? || cache['expires_at'].nil?
107
+ return cache['token'] if Time.parse(cache['expires_at']) > Time.now + 300
108
+ nil
109
+ rescue JSON::ParserError
110
+ nil
111
+ end
112
+
113
+ puts find_or_create_cached_token
114
+ RUBY
115
+
116
+ # Install helper via sudo to /usr/local/bin
117
+ helper_path = "/usr/local/bin/git-auth-helper"
118
+ File.write("/tmp/git-auth-helper", helper_content)
119
+ system("sudo mv /tmp/git-auth-helper #{helper_path}")
120
+ system("sudo chmod +x #{helper_path}")
121
+
122
+ # Configure git
123
+ system("git config --global credential.helper '!f() { test \"$1\" = get && echo \"protocol=https\" && echo \"host=github.com\" && echo \"username=x-access-token\" && echo \"password=$(#{helper_path})\"; }; f'")
124
+
125
+ # Configure gh CLI wrapper for auto-refresh and permission controls
126
+ if install_gh_wrapper!(real_gh_path: "/usr/bin/gh", with_token_refresh: true, token_helper_path: helper_path)
127
+ puts "✅ GitHub App authentication configured (via backend with auto-refresh + permission controls)"
128
+ end
129
+
130
+ elsif ENV["GH_TOKEN"] && !ENV["GH_TOKEN"].empty?
131
+ puts "🔑 Using GitHub token authentication (fallback method)"
132
+ real_gh_path = "/usr/bin/gh"
133
+
134
+ # Authenticate using the real gh binary
135
+ # On first run: real_gh_path exists, authenticate before wrapper installation
136
+ # On subsequent runs: real_gh_path.real exists from previous run
137
+ if File.exist?("#{real_gh_path}.real")
138
+ system("echo '#{ENV['GH_TOKEN']}' | #{real_gh_path}.real auth login --with-token 2>/dev/null")
139
+ elsif File.exist?(real_gh_path)
140
+ system("echo '#{ENV['GH_TOKEN']}' | #{real_gh_path} auth login --with-token 2>/dev/null")
141
+ end
142
+
143
+ # Install wrapper for permission controls even with GH_TOKEN
144
+ if install_gh_wrapper!(real_gh_path: real_gh_path)
145
+ puts "🔐 GitHub authentication configured (with permission controls)"
146
+ else
147
+ puts "🔐 GitHub authentication configured"
148
+ end
149
+ else
150
+ puts "⚠️ No YATFA_API_KEY or GH_TOKEN - GitHub operations will fail"
151
+ end
152
+ end
153
+
154
+ def install_gh_wrapper!(real_gh_path:, with_token_refresh: false, token_helper_path: nil)
155
+ unless File.exist?(real_gh_path)
156
+ puts "⚠️ Could not find 'gh' at #{real_gh_path}, skipping wrapper"
157
+ return false
158
+ end
159
+
160
+ # Move real gh to gh.real if not already done
161
+ unless File.exist?("#{real_gh_path}.real")
162
+ system("sudo mv #{real_gh_path} #{real_gh_path}.real")
163
+ end
164
+
165
+ wrapper_path = "/usr/local/bin/gh"
166
+
167
+ # Build token export line if auto-refresh is enabled
168
+ token_export = with_token_refresh ? "export GH_TOKEN=$(#{token_helper_path})\n\n" : ""
169
+
170
+ wrapper_content = <<~BASH
171
+ #!/bin/bash
172
+ # GitHub CLI wrapper with permission controls for agents
173
+ #{token_export}# Verify GH_TOKEN is set if using auto-refresh
174
+ if [ -n "$AGENT_TYPE" ] && [ "#{with_token_refresh}" = "true" ] && [ -z "$GH_TOKEN" ]; then
175
+ echo "❌ GitHub authentication failed: token helper returned empty value" >&2
176
+ echo " Check that GITHUB_APP_CLIENT_ID and GITHUB_APP_INSTALLATION_ID are set" >&2
177
+ echo " Run 'git-auth-helper' to test the token helper directly" >&2
178
+ exit 1
179
+ fi
180
+
181
+ # Check if caller is an agent (read AGENT_TYPE at runtime)
182
+ if [ -n "$AGENT_TYPE" ] && [[ "$AGENT_TYPE" =~ ^(worker|planner|reviewer|researcher)$ ]]; then
183
+ # Block comment-related commands for agents
184
+ if [[ "$1" == "comment" ]] || ([[ "$1" == "pr" ]] && [[ "$2" == "comment" ]]); then
185
+ echo "❌ You cannot use 'gh $*' commands as an agent." >&2
186
+ echo "" >&2
187
+ echo "Yatfa has its own comment system via the add_comment MCP tool." >&2
188
+ echo "Please use the add_comment tool instead of gh commands." >&2
189
+ echo "" >&2
190
+ echo "Example:" >&2
191
+ echo " add_comment(ticket_id: 123, content: \\"Your comment here\\", comment_type: \\"note\\")" >&2
192
+ exit 1
193
+ fi
194
+
195
+ # Block pr create for agents - they should use the create_pull_request MCP tool
196
+ if [[ "$1" == "pr" ]] && [[ "$2" == "create" ]]; then
197
+ echo "❌ You cannot use 'gh pr create' as an agent." >&2
198
+ echo "" >&2
199
+ echo "Yatfa has its own PR creation system via the create_pull_request MCP tool." >&2
200
+ echo "This automatically attaches PRs to tickets and adds metadata footers." >&2
201
+ echo "" >&2
202
+ echo "Example:" >&2
203
+ echo " create_pull_request(" >&2
204
+ echo " ticket_id: 123," >&2
205
+ echo " repository_id: 1," >&2
206
+ echo " title: \\"Fix authentication bug\\"," >&2
207
+ echo " body: \\"Description of changes\\"," >&2
208
+ echo " branch: \\"feature/123-fix-auth\\"" >&2
209
+ echo " )" >&2
210
+ exit 1
211
+ fi
212
+ fi
213
+
214
+ # Execute real gh binary with all arguments
215
+ exec #{real_gh_path}.real "$@"
216
+ BASH
217
+
218
+ File.write("/tmp/gh-wrapper", wrapper_content)
219
+ system("sudo mv /tmp/gh-wrapper #{wrapper_path}")
220
+ system("sudo chmod +x #{wrapper_path}")
221
+ true
222
+ end