yatfa 1.0.95 → 1.0.97

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.
@@ -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://objectstore.fra1.civo.com/yatfa/${YATFA_VERSION}"
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 to first available agent's MCP API key
114
- mcp_api_key = nil
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
- "yatfa-sandbox"
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://objectstore.fra1.civo.com/yatfa/main/version.json"
12
-
13
- def self.check_dockerfile!
14
- unless File.exist?("Dockerfile.sandbox")
15
- puts "❌ Error: Dockerfile.sandbox not found"
16
- puts ""
17
- puts "Please create Dockerfile.sandbox by copying your existing Dockerfile"
18
- puts "and adding the required agent dependencies."
19
- puts ""
20
- puts "See https://github.com/RoM4iK/yatfa-public/blob/main/README.md for instructions."
21
- exit 1
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
@@ -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://objectstore.fra1.civo.com/yatfa/${YATFA_VERSION}/version.json /tmp/yatfa_version.json
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://objectstore.fra1.civo.com/yatfa/${YATFA_VERSION}/bin/install-agent.sh | bash
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. Build the sandbox Docker image:"
565
- puts " docker build -f Dockerfile.sandbox -t yatfa-sandbox ."
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 same image
323
- image = config.dig("Config", "Image")
324
- cmd += [image || "yatfa-sandbox"]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yatfa",
3
- "version": "1.0.95",
3
+ "version": "1.0.97",
4
4
  "description": "YATFA - Yet Another Tool For Agents",
5
5
  "bin": "./bin/run-yatfa-agent.rb",
6
6
  "files": [
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # Handles:
7
7
  # - Architecture detection and multi-strategy bridge download
8
- # (local override → Civo S3 presigned URL GitHub fallback)
8
+ # (local override → mounted binary public bucket download)
9
9
  # - tmux binary download and INSIDE_TMUX=1 patching
10
10
  # - Generic file download utility (download_file)
11
11
  # - Helper script installation to /usr/local/bin
@@ -58,22 +58,12 @@ def download_agent_bridge!
58
58
  return download_agent_bridge_tmux!(target_dir)
59
59
  end
60
60
 
61
- # 3. Try Civo bucket download (production mode)
62
- if ENV["CIVO_ACCESS_KEY_ID"] && ENV["CIVO_SECRET_KEY"]
63
- puts "📥 Downloading agent-bridge from Civo bucket..."
64
- if download_from_civo!(arch, target_dir)
65
- return download_agent_bridge_tmux!(target_dir)
66
- else
67
- puts "⚠️ Civo download failed, falling back to GitHub..."
68
- end
69
- end
70
-
71
- # 4. Fallback to GitHub raw URLs (legacy)
61
+ # 3. Download from the public bucket (production mode)
72
62
  bridge_url = "#{YATFA_RAW_URL}/bin/agent-bridge-linux-#{arch}"
73
- puts "📥 Downloading agent-bridge for linux-#{arch} from GitHub..."
63
+ puts "📥 Downloading agent-bridge for linux-#{arch} from bucket..."
74
64
  system("sudo", "curl", "-fsSL", bridge_url, "-o", "#{target_dir}/agent-bridge")
75
65
  unless $?.success?
76
- puts "❌ Failed to download agent-bridge from GitHub"
66
+ puts "❌ Failed to download agent-bridge from #{bridge_url}"
77
67
  exit 1
78
68
  end
79
69
  system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge")
@@ -82,63 +72,6 @@ def download_agent_bridge!
82
72
  download_agent_bridge_tmux!(target_dir)
83
73
  end
84
74
 
85
- def download_from_civo!(arch, target_dir)
86
- require "aws-sdk-s3"
87
-
88
- region = ENV["CIVO_REGION"] || "LON1"
89
- endpoint = ENV["CIVO_ENDPOINT"] || "https://objectstore.fra1.civo.com"
90
- bucket = ENV["YATFA_BINARIES_BUCKET"] || "yatfa"
91
- version = ENV["AGENT_BRIDGE_VERSION"] || "latest"
92
-
93
- binary_name = "agent-bridge-linux-#{arch}"
94
- key = "agent-bridge/#{version}/#{binary_name}"
95
-
96
- puts " Region: #{region}"
97
- puts " Bucket: #{bucket}"
98
- puts " Version: #{version}"
99
- puts " Binary: #{binary_name}"
100
-
101
- begin
102
- # Configure Civo S3-compatible client
103
- Aws.config.update({
104
- region: region,
105
- endpoint: endpoint,
106
- access_key_id: ENV["CIVO_ACCESS_KEY_ID"],
107
- secret_access_key: ENV["CIVO_SECRET_KEY"]
108
- })
109
-
110
- s3 = Aws::S3::Resource.new
111
- obj = s3.bucket(bucket).object(key)
112
-
113
- # Generate presigned URL (valid for 1 hour)
114
- url = obj.presigned_url(:get, expires_in: 3600)
115
-
116
- puts "✅ Generated presigned URL"
117
-
118
- # Download via curl (array form avoids shell interpolation of URL)
119
- system("sudo", "curl", "-fsSL", url, "-o", "#{target_dir}/agent-bridge")
120
-
121
- unless $?.success?
122
- puts "❌ Failed to download agent-bridge from Civo"
123
- return false
124
- end
125
-
126
- system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge")
127
- puts "✅ Downloaded agent-bridge from Civo"
128
- return true
129
-
130
- rescue LoadError
131
- puts "⚠️ aws-sdk-s3 gem not installed"
132
- return false
133
- rescue Aws::S3::Errors::NoSuchKey
134
- puts "❌ Version #{version} not found in bucket"
135
- return false
136
- rescue => e
137
- puts "❌ Civo download failed: #{e.message}"
138
- return false
139
- end
140
- end
141
-
142
75
  def download_agent_bridge_tmux!(target_dir)
143
76
  bridge_tmux_url = "#{YATFA_RAW_URL}/bin/agent-bridge-tmux"
144
77
 
@@ -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://objectstore.fra1.civo.com/yatfa"
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
- system("git config --global url.\"https://github.com/\".insteadOf \"git@github.com:\"")
261
- puts "✅ Git configured to force HTTPS for GitHub"
262
-
263
- # Avoid "dubious ownership" errors in containers
264
- # Note: Individual repository paths are added in clone_repositories! after cloning
265
- system("git", "config", "--global", "--add", "safe.directory", Dir.pwd)
266
- puts " Git configured to trust #{Dir.pwd}"
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
 
package/setup/skills.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  # Extracted from setup-agent.rb for modularity (Round 2)
5
5
  #
6
6
  # Handles:
7
- # - Installing built-in skills from the SKILLS env var (Civo-hosted SKILL.md files)
7
+ # - Installing built-in skills from the SKILLS env var (R2-hosted SKILL.md files)
8
8
  # - Fetching community skills list from the backend whoami API
9
9
  # - Installing community skills from external GitHub repos
10
10
  #