yatfa 1.0.78 → 1.0.80
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.
- package/bin/install-agent.sh +13 -9
- package/bin/run-yatfa-agent.rb +40 -4
- package/lib/yatfa_agent/agent.rb +8 -29
- package/package.json +2 -1
- package/setup/setup-agent.rb +1038 -0
- package/setup/setup-claw-agent.rb +494 -0
- package/setup/setup-github.rb +222 -0
|
@@ -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
|