yatfa 1.0.95 → 1.0.96
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 +1 -1
- package/lib/yatfa_agent/config.rb +37 -11
- package/lib/yatfa_agent/docker.rb +64 -12
- package/lib/yatfa_agent/setup.rb +4 -6
- package/lib/yatfa_agent/updater.rb +17 -5
- package/package.json +2 -2
- package/setup/setup-agent.rb +26 -15
package/bin/install-agent.sh
CHANGED
|
@@ -101,7 +101,7 @@ YATFA_VERSION=${YATFA_VERSION:-main}
|
|
|
101
101
|
cat << 'EOF' > /usr/local/bin/setup-agent
|
|
102
102
|
#!/bin/bash
|
|
103
103
|
YATFA_VERSION=${YATFA_VERSION:-main}
|
|
104
|
-
BASE_URL="https://
|
|
104
|
+
BASE_URL="https://pub-06ce56b9a9c143118036f593f07ac1d8.r2.dev/${YATFA_VERSION}"
|
|
105
105
|
# Use local setup/ dir if mounted (dev mode), otherwise download all from remote
|
|
106
106
|
if [ -f "/tmp/setup/setup-agent.rb" ]; then
|
|
107
107
|
echo "🔧 Using local setup/ (dev mode)"
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "digest"
|
|
4
5
|
require_relative "http"
|
|
5
6
|
|
|
6
7
|
module YatfaAgent
|
|
7
8
|
module Config
|
|
9
|
+
# Image name returned when the project's Dockerfile.sandbox is not present
|
|
10
|
+
# (e.g. image_name queried before the file is materialized, or offline).
|
|
11
|
+
DEFAULT_IMAGE_NAME = "yatfa-sandbox"
|
|
12
|
+
DOCKERFILE_SANDBOX = "Dockerfile.sandbox"
|
|
8
13
|
# Walk up the directory tree from +start_dir+ (default: Dir.pwd)
|
|
9
14
|
# looking for yatfa.env.rb. Returns the directory containing the
|
|
10
15
|
# config file, or nil if not found.
|
|
@@ -104,22 +109,27 @@ module YatfaAgent
|
|
|
104
109
|
"#{api_base.sub(/^https:\/\//, "wss://").sub(/^http:\/\//, "ws://")}/cable"
|
|
105
110
|
end
|
|
106
111
|
|
|
112
|
+
# Extract the first available agent MCP API key from the config.
|
|
113
|
+
# Returns nil when no key is configured.
|
|
114
|
+
def self.agent_api_key(config)
|
|
115
|
+
agents = config["agents"]
|
|
116
|
+
return nil unless agents.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
agents.each_value do |agent_config|
|
|
119
|
+
return agent_config["mcp_api_key"] if agent_config.is_a?(Hash) && agent_config["mcp_api_key"]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
107
125
|
def self.fetch_repositories!(config)
|
|
108
126
|
# Support both old and new config keys
|
|
109
127
|
api_base = api_url(config)
|
|
110
128
|
|
|
111
129
|
return unless api_base
|
|
112
130
|
|
|
113
|
-
# Use
|
|
114
|
-
mcp_api_key =
|
|
115
|
-
if config["agents"].is_a?(Hash)
|
|
116
|
-
config["agents"].each do |_, agent_config|
|
|
117
|
-
if agent_config.is_a?(Hash) && agent_config["mcp_api_key"]
|
|
118
|
-
mcp_api_key = agent_config["mcp_api_key"]
|
|
119
|
-
break
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
131
|
+
# Use the first available agent's MCP API key
|
|
132
|
+
mcp_api_key = agent_api_key(config)
|
|
123
133
|
|
|
124
134
|
return unless mcp_api_key
|
|
125
135
|
|
|
@@ -146,6 +156,10 @@ module YatfaAgent
|
|
|
146
156
|
project_id = agent_info["project_id"]
|
|
147
157
|
project_name = agent_info["name"] rescue "unknown"
|
|
148
158
|
|
|
159
|
+
# Expose the resolved project id so downstream steps can scope per-project
|
|
160
|
+
# resources (image tag, dockerfile_sandbox fetch) without another round-trip.
|
|
161
|
+
config["project_id"] = project_id if project_id
|
|
162
|
+
|
|
149
163
|
puts "🔑 Authenticated as #{agent_info["name"]} (project #{project_id})"
|
|
150
164
|
|
|
151
165
|
# Then fetch repositories from Rails API (scoped to our project)
|
|
@@ -176,8 +190,20 @@ module YatfaAgent
|
|
|
176
190
|
end
|
|
177
191
|
end
|
|
178
192
|
|
|
193
|
+
# Per-project Docker image tag so concurrent projects on one host don't
|
|
194
|
+
# clobber the shared "yatfa-sandbox" tag.
|
|
195
|
+
#
|
|
196
|
+
# The tag is derived from a digest of the project's Dockerfile.sandbox
|
|
197
|
+
# content: projects with an identical dockerfile share one image (no
|
|
198
|
+
# redundant rebuilds), while projects with different dockerfiles build to
|
|
199
|
+
# distinct tags and never overwrite each other. Falls back to
|
|
200
|
+
# +DEFAULT_IMAGE_NAME+ when no Dockerfile.sandbox is present (e.g. the tag
|
|
201
|
+
# is queried before the file is materialized, or while running offline).
|
|
179
202
|
def self.image_name(config)
|
|
180
|
-
|
|
203
|
+
content = File.exist?(DOCKERFILE_SANDBOX) ? File.read(DOCKERFILE_SANDBOX) : nil
|
|
204
|
+
return DEFAULT_IMAGE_NAME if content.nil? || content.empty?
|
|
205
|
+
|
|
206
|
+
"yatfa-sandbox-#{Digest::SHA256.hexdigest(content)[0, 16]}"
|
|
181
207
|
end
|
|
182
208
|
end
|
|
183
209
|
end
|
|
@@ -4,22 +4,74 @@ require "fileutils"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "time"
|
|
6
6
|
require_relative "http"
|
|
7
|
+
require_relative "config"
|
|
7
8
|
|
|
8
9
|
module YatfaAgent
|
|
9
10
|
module Docker
|
|
10
11
|
STAMP_FILE = ".yatfa-build-stamp"
|
|
11
|
-
VERSION_URL = "https://
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
VERSION_URL = "https://pub-06ce56b9a9c143118036f593f07ac1d8.r2.dev/main/version.json"
|
|
13
|
+
DOCKERFILE_NAME = "Dockerfile.sandbox"
|
|
14
|
+
|
|
15
|
+
# Ensure a Dockerfile.sandbox is available for the build.
|
|
16
|
+
#
|
|
17
|
+
# A local ./Dockerfile.sandbox always wins (override semantics — we never
|
|
18
|
+
# silently overwrite a file the user placed). When no local file is present,
|
|
19
|
+
# we try to materialize one from the project's active
|
|
20
|
+
# `dockerfile_sandbox/default` ProjectConfig via the API, using the agent's
|
|
21
|
+
# existing API key. If neither is available, we exit with a clear, actionable
|
|
22
|
+
# error instead of a stack trace.
|
|
23
|
+
#
|
|
24
|
+
# @param config [Hash] the loaded yatfa.env.rb config (carries yatfa_url,
|
|
25
|
+
# project_id, and the agent mcp_api_key needed to scope the request)
|
|
26
|
+
def self.check_dockerfile!(config = {})
|
|
27
|
+
return if File.exist?(DOCKERFILE_NAME)
|
|
28
|
+
|
|
29
|
+
fetched = fetch_remote_dockerfile(config)
|
|
30
|
+
if fetched
|
|
31
|
+
File.write(DOCKERFILE_NAME, fetched)
|
|
32
|
+
puts "📥 Wrote Dockerfile.sandbox from project config (dockerfile_sandbox/default)"
|
|
33
|
+
return
|
|
22
34
|
end
|
|
35
|
+
|
|
36
|
+
puts "❌ Error: Dockerfile.sandbox not found"
|
|
37
|
+
puts ""
|
|
38
|
+
puts "No local Dockerfile.sandbox and no active 'dockerfile_sandbox' project config"
|
|
39
|
+
puts "was found. Provide one of:"
|
|
40
|
+
puts " • a local ./Dockerfile.sandbox, OR"
|
|
41
|
+
puts " • a 'dockerfile_sandbox' config in your project (Project Configurations UI / API)"
|
|
42
|
+
puts ""
|
|
43
|
+
puts "See https://github.com/RoM4iK/yatfa-public/blob/main/README.md for instructions."
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetch the project's active dockerfile_sandbox/default config content.
|
|
48
|
+
# Returns the dockerfile content [String], or nil when the project cannot
|
|
49
|
+
# be identified, the API is unreachable, or no such config is stored.
|
|
50
|
+
# Never raises — a missing remote config is a normal "fall back to error" case.
|
|
51
|
+
def self.fetch_remote_dockerfile(config)
|
|
52
|
+
api_base = Config.api_url(config)
|
|
53
|
+
project_id = config["project_id"]
|
|
54
|
+
api_key = Config.agent_api_key(config)
|
|
55
|
+
|
|
56
|
+
return nil if api_base.nil? || api_base.to_s.empty?
|
|
57
|
+
return nil if project_id.nil? || project_id.to_s.empty?
|
|
58
|
+
return nil if api_key.nil? || api_key.to_s.empty?
|
|
59
|
+
|
|
60
|
+
url = "#{api_base}/projects/#{project_id}/configs/dockerfile_sandbox/default"
|
|
61
|
+
auth_headers = { "X-API-Key" => api_key, "Accept" => "application/json" }
|
|
62
|
+
|
|
63
|
+
data = YatfaAgent::HTTP.get(url, headers: auth_headers)
|
|
64
|
+
return nil unless data.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
content = data["content"]
|
|
67
|
+
content.nil? || content.to_s.empty? ? nil : content.to_s
|
|
68
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
69
|
+
# 404 just means no dockerfile_sandbox config is stored — not an error.
|
|
70
|
+
warn "⚠️ No dockerfile_sandbox config fetched (HTTP #{e.status_code})" unless e.status_code == 404
|
|
71
|
+
nil
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
warn "⚠️ Failed to fetch dockerfile_sandbox config (#{e.class}: #{e.message})"
|
|
74
|
+
nil
|
|
23
75
|
end
|
|
24
76
|
|
|
25
77
|
def self.fetch_remote_version
|
|
@@ -48,7 +100,7 @@ module YatfaAgent
|
|
|
48
100
|
end
|
|
49
101
|
|
|
50
102
|
def self.build_image(config)
|
|
51
|
-
check_dockerfile!
|
|
103
|
+
check_dockerfile!(config)
|
|
52
104
|
|
|
53
105
|
user_id = `id -u`.strip
|
|
54
106
|
group_id = `id -g`.strip
|
package/lib/yatfa_agent/setup.rb
CHANGED
|
@@ -524,12 +524,12 @@ module YatfaAgent
|
|
|
524
524
|
ARG GROUP_ID=1000
|
|
525
525
|
|
|
526
526
|
# Cache busting: only rebuilds when install-agent.sh infrastructure changes
|
|
527
|
-
ADD https://
|
|
527
|
+
ADD https://pub-06ce56b9a9c143118036f593f07ac1d8.r2.dev/${YATFA_VERSION}/version.json /tmp/yatfa_version.json
|
|
528
528
|
|
|
529
529
|
# Install YATFA agent infrastructure
|
|
530
530
|
# This layer is cached unless install-agent.sh changes (detected via version.json above)
|
|
531
531
|
# setup-agent.rb, skills, and hooks download fresh on every container startup
|
|
532
|
-
RUN curl -fsSL https://
|
|
532
|
+
RUN curl -fsSL https://pub-06ce56b9a9c143118036f593f07ac1d8.r2.dev/${YATFA_VERSION}/bin/install-agent.sh | bash
|
|
533
533
|
|
|
534
534
|
# Ensure /workspace is owned by agent user and writable
|
|
535
535
|
# The install-agent.sh script creates a user with USER_ID, so we use that ID directly
|
|
@@ -561,10 +561,8 @@ module YatfaAgent
|
|
|
561
561
|
puts " 1. Ensure 'yatfa.env.rb' is in .gitignore:"
|
|
562
562
|
puts " echo 'yatfa.env.rb' >> .gitignore"
|
|
563
563
|
puts ""
|
|
564
|
-
puts " 2.
|
|
565
|
-
puts "
|
|
566
|
-
puts ""
|
|
567
|
-
puts " 3. Start an agent:"
|
|
564
|
+
puts " 2. Start an agent (the sandbox image builds automatically on first run,"
|
|
565
|
+
puts " tagged from your Dockerfile.sandbox):"
|
|
568
566
|
if @config[:agents].key?(:worker)
|
|
569
567
|
puts " npx yatfa worker"
|
|
570
568
|
elsif @config[:agents].key?(:planner)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "time"
|
|
5
|
+
require_relative "config"
|
|
6
|
+
require_relative "docker"
|
|
5
7
|
|
|
6
8
|
module YatfaAgent
|
|
7
9
|
module Updater
|
|
@@ -130,17 +132,23 @@ module YatfaAgent
|
|
|
130
132
|
# Both Config.load and Docker.build_image call exit(1) on failure,
|
|
131
133
|
# which SystemExit catches here (not caught by rescue StandardError).
|
|
132
134
|
puts " Building fresh image..."
|
|
135
|
+
fresh_image = nil
|
|
133
136
|
begin
|
|
134
137
|
project_root = find_project_root_for_container(config)
|
|
135
138
|
if project_root && Dir.exist?(project_root)
|
|
136
139
|
Dir.chdir(project_root) do
|
|
137
140
|
docker_config = load_config_for_container(project_root)
|
|
138
141
|
Docker.build_image(docker_config)
|
|
142
|
+
# Resolve the tag while still inside the project root so the
|
|
143
|
+
# freshly built image (not the stale one baked into the old
|
|
144
|
+
# container) is used to recreate it.
|
|
145
|
+
fresh_image = Config.image_name(docker_config)
|
|
139
146
|
end
|
|
140
147
|
else
|
|
141
148
|
puts " ⚠️ Could not find project root, attempting build in cwd"
|
|
142
149
|
docker_config = Config.load
|
|
143
150
|
Docker.build_image(docker_config)
|
|
151
|
+
fresh_image = Config.image_name(docker_config)
|
|
144
152
|
end
|
|
145
153
|
rescue SystemExit => e
|
|
146
154
|
puts " ❌ Build failed (exit #{e.status}), keeping old container running"
|
|
@@ -154,7 +162,7 @@ module YatfaAgent
|
|
|
154
162
|
|
|
155
163
|
# 4. Recreate container with preserved config
|
|
156
164
|
puts " Starting new container..."
|
|
157
|
-
new_cmd = build_recreate_command(name, config, new_version)
|
|
165
|
+
new_cmd = build_recreate_command(name, config, new_version, image: fresh_image)
|
|
158
166
|
success = system(*new_cmd)
|
|
159
167
|
|
|
160
168
|
if success
|
|
@@ -262,8 +270,10 @@ module YatfaAgent
|
|
|
262
270
|
# @param name [String] container name
|
|
263
271
|
# @param config [Hash] container inspect data
|
|
264
272
|
# @param new_version [String] target infrastructure version number
|
|
273
|
+
# @param image [String, nil] freshly built image tag to run (takes
|
|
274
|
+
# precedence over the stale image baked into the old container)
|
|
265
275
|
# @return [Array<String>] docker command arguments
|
|
266
|
-
def self.build_recreate_command(name, config, new_version)
|
|
276
|
+
def self.build_recreate_command(name, config, new_version, image: nil)
|
|
267
277
|
cmd = ["docker", "run", "-d", "--name", name]
|
|
268
278
|
|
|
269
279
|
# Preserve restart policy
|
|
@@ -319,9 +329,11 @@ module YatfaAgent
|
|
|
319
329
|
entrypoint_extra = entrypoint[1..]
|
|
320
330
|
end
|
|
321
331
|
|
|
322
|
-
# Use the
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
# Use the freshly built image when provided (so an update-cycle rebuild
|
|
333
|
+
# actually takes effect); otherwise preserve the container's existing
|
|
334
|
+
# image, falling back to the default tag.
|
|
335
|
+
resolved_image = image || config.dig("Config", "Image") || Config::DEFAULT_IMAGE_NAME
|
|
336
|
+
cmd += [resolved_image]
|
|
325
337
|
|
|
326
338
|
# Use the original CMD (prepended with any extra entrypoint args)
|
|
327
339
|
original_cmd = config.dig("Config", "Cmd")
|
package/package.json
CHANGED
package/setup/setup-agent.rb
CHANGED
|
@@ -45,7 +45,7 @@ require_relative "skills"
|
|
|
45
45
|
|
|
46
46
|
SCRIPT_DIR = File.dirname(File.expand_path(__FILE__))
|
|
47
47
|
YATFA_VERSION = ENV["YATFA_VERSION"] || "main"
|
|
48
|
-
YATFA_SCRIPTS_BASE_URL = "https://
|
|
48
|
+
YATFA_SCRIPTS_BASE_URL = "https://pub-06ce56b9a9c143118036f593f07ac1d8.r2.dev"
|
|
49
49
|
YATFA_RAW_URL = "#{YATFA_SCRIPTS_BASE_URL}/#{YATFA_VERSION}"
|
|
50
50
|
|
|
51
51
|
# Valid agent types
|
|
@@ -244,26 +244,37 @@ def setup_claude_config!
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
def setup_git_config!
|
|
247
|
-
# Configure identity if provided
|
|
247
|
+
# Configure identity if provided.
|
|
248
|
+
# Each `git config` return value is captured and reported honestly — a failed
|
|
249
|
+
# config (read-only $HOME, full disk, restricted FS) is NOT fatal, but must
|
|
250
|
+
# NOT be hidden behind an unconditional ✅. Mirrors the repositories.rb:75-77
|
|
251
|
+
# precedent (YATFA-2842). Non-fatal: setup continues so a transient/recoverable
|
|
252
|
+
# failure does not restart-loop the container.
|
|
248
253
|
if ENV["GIT_USER_NAME"] && !ENV["GIT_USER_NAME"].empty?
|
|
249
|
-
system("git", "config", "--global", "user.name", ENV["GIT_USER_NAME"])
|
|
250
|
-
puts "✅ Git user.name configured"
|
|
254
|
+
name_success = system("git", "config", "--global", "user.name", ENV["GIT_USER_NAME"])
|
|
255
|
+
puts name_success ? "✅ Git user.name configured" :
|
|
256
|
+
"⚠️ Failed to set git user.name (later commits may fail authoring)"
|
|
251
257
|
end
|
|
252
258
|
|
|
253
259
|
if ENV["GIT_USER_EMAIL"] && !ENV["GIT_USER_EMAIL"].empty?
|
|
254
|
-
system("git", "config", "--global", "user.email", ENV["GIT_USER_EMAIL"])
|
|
255
|
-
puts "✅ Git user.email configured"
|
|
260
|
+
email_success = system("git", "config", "--global", "user.email", ENV["GIT_USER_EMAIL"])
|
|
261
|
+
puts email_success ? "✅ Git user.email configured" :
|
|
262
|
+
"⚠️ Failed to set git user.email (later commits may fail authoring)"
|
|
256
263
|
end
|
|
257
264
|
|
|
258
|
-
# Force HTTPS instead of SSH to ensure our token auth works
|
|
259
|
-
# This fixes "Permission denied (publickey)" when the repo uses git@github.com remote
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
# Force HTTPS instead of SSH to ensure our token auth works.
|
|
266
|
+
# This fixes "Permission denied (publickey)" when the repo uses git@github.com remote.
|
|
267
|
+
# If this silently fails, SSH-remote repos are NOT rewritten to HTTPS, so the
|
|
268
|
+
# HTTPS App-token auth path never engages → high impact, must be surfaced.
|
|
269
|
+
https_success = system("git config --global url.\"https://github.com/\".insteadOf \"git@github.com:\"")
|
|
270
|
+
puts https_success ? "✅ Git configured to force HTTPS for GitHub" :
|
|
271
|
+
"⚠️ Failed to force HTTPS for GitHub (SSH-remote clones may fail with \"Permission denied (publickey)\")"
|
|
272
|
+
|
|
273
|
+
# Avoid "dubious ownership" errors in containers.
|
|
274
|
+
# Note: Individual repository paths are added in clone_repositories! after cloning.
|
|
275
|
+
safe_success = system("git", "config", "--global", "--add", "safe.directory", Dir.pwd)
|
|
276
|
+
puts safe_success ? "✅ Git configured to trust #{Dir.pwd}" :
|
|
277
|
+
"⚠️ Failed to set git safe.directory for #{Dir.pwd} (may see 'dubious ownership' errors)"
|
|
267
278
|
end
|
|
268
279
|
|
|
269
280
|
|