tinker-agent 1.0.0
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/LICENSE +21 -0
- package/README.md +89 -0
- package/agents.rb +457 -0
- package/bin/agent-bridge-tmux +56 -0
- package/bin/install-agent.sh +95 -0
- package/package.json +20 -0
- package/run-tinker-agent.rb +255 -0
- package/setup-agent.rb +399 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Install dependencies
|
|
5
|
+
apt-get update && apt-get install -y \
|
|
6
|
+
git curl tmux sudo unzip wget
|
|
7
|
+
|
|
8
|
+
# Install Node.js (required for Claude CLI)
|
|
9
|
+
if ! command -v node &> /dev/null; then
|
|
10
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
|
11
|
+
apt-get install -y nodejs
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Install Claude CLI
|
|
15
|
+
npm install -g @anthropic-ai/claude-code
|
|
16
|
+
|
|
17
|
+
# Install GitHub CLI
|
|
18
|
+
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
19
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list
|
|
20
|
+
apt-get update && apt-get install gh -y
|
|
21
|
+
|
|
22
|
+
# Setup User
|
|
23
|
+
# We want to run as the same UID as the host user (passed via build arg)
|
|
24
|
+
# to ensure we can edit mounted files.
|
|
25
|
+
USER_ID=${USER_ID:-1000}
|
|
26
|
+
GROUP_ID=${GROUP_ID:-1000}
|
|
27
|
+
AGENT_USER="claude"
|
|
28
|
+
|
|
29
|
+
# 1. Handle Group
|
|
30
|
+
if getent group ${GROUP_ID} >/dev/null 2>&1; then
|
|
31
|
+
# Group exists (e.g. 'node'), use it
|
|
32
|
+
GROUP_NAME=$(getent group ${GROUP_ID} | cut -d: -f1)
|
|
33
|
+
else
|
|
34
|
+
# Create group
|
|
35
|
+
groupadd -g ${GROUP_ID} ${AGENT_USER}
|
|
36
|
+
GROUP_NAME=${AGENT_USER}
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# 2. Handle User
|
|
40
|
+
if getent passwd ${USER_ID} >/dev/null 2>&1; then
|
|
41
|
+
# User exists (e.g. 'node'), use it
|
|
42
|
+
AGENT_USER=$(getent passwd ${USER_ID} | cut -d: -f1)
|
|
43
|
+
# Ensure user is in the group (if different)
|
|
44
|
+
usermod -aG ${GROUP_NAME} ${AGENT_USER}
|
|
45
|
+
else
|
|
46
|
+
# Create user
|
|
47
|
+
useradd -u ${USER_ID} -g ${GROUP_NAME} -m -s /bin/bash ${AGENT_USER}
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# 3. Grant Sudo
|
|
51
|
+
echo "${AGENT_USER} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
52
|
+
|
|
53
|
+
# 4. Determine Home Directory
|
|
54
|
+
AGENT_HOME=$(getent passwd ${AGENT_USER} | cut -d: -f6)
|
|
55
|
+
|
|
56
|
+
# Create entrypoint
|
|
57
|
+
cat << EOF > /entrypoint.sh
|
|
58
|
+
#!/bin/bash
|
|
59
|
+
set -e
|
|
60
|
+
|
|
61
|
+
# Copy config files if they exist in /tmp (mounted volumes)
|
|
62
|
+
if [ -f "/tmp/cfg/claude.json" ]; then
|
|
63
|
+
cp /tmp/cfg/claude.json ${AGENT_HOME}/.claude.json || echo "⚠️ Failed to copy claude.json"
|
|
64
|
+
fi
|
|
65
|
+
if [ -d "/tmp/cfg/claude_dir" ]; then
|
|
66
|
+
rm -rf ${AGENT_HOME}/.claude
|
|
67
|
+
cp -r /tmp/cfg/claude_dir ${AGENT_HOME}/.claude || echo "⚠️ Failed to copy claude_dir"
|
|
68
|
+
fi
|
|
69
|
+
if [ -f "/tmp/github-app-privkey.pem" ]; then
|
|
70
|
+
cp /tmp/github-app-privkey.pem ${AGENT_HOME}/.github-app-privkey.pem || echo "⚠️ Failed to copy github key"
|
|
71
|
+
chmod 600 ${AGENT_HOME}/.github-app-privkey.pem 2>/dev/null || true
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Fix permissions
|
|
75
|
+
sudo chown -R ${AGENT_USER}:${GROUP_NAME} ${AGENT_HOME} || echo "⚠️ Failed to chown home"
|
|
76
|
+
|
|
77
|
+
# Fix permissions of the current directory (project root)
|
|
78
|
+
# This ensures the agent can write CLAUDE.md and .mcp.json
|
|
79
|
+
sudo chown ${AGENT_USER}:${GROUP_NAME} $(pwd) || echo "⚠️ Failed to chown project root"
|
|
80
|
+
|
|
81
|
+
# Set GitHub App Key Path if not set
|
|
82
|
+
if [ -z "\$GITHUB_APP_PRIVATE_KEY_PATH" ]; then
|
|
83
|
+
export GITHUB_APP_PRIVATE_KEY_PATH="${AGENT_HOME}/.github-app-privkey.pem"
|
|
84
|
+
else
|
|
85
|
+
# If it was set to /home/claude/... but we are /home/node, fix it
|
|
86
|
+
if [[ "\$GITHUB_APP_PRIVATE_KEY_PATH" == *"/home/claude/"* ]] && [ "${AGENT_HOME}" != "/home/claude" ]; then
|
|
87
|
+
export GITHUB_APP_PRIVATE_KEY_PATH="${AGENT_HOME}/.github-app-privkey.pem"
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Execute command as agent user
|
|
92
|
+
exec sudo -E -u ${AGENT_USER} env "HOME=${AGENT_HOME}" "\$@"
|
|
93
|
+
EOF
|
|
94
|
+
|
|
95
|
+
chmod +x /entrypoint.sh
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tinker-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tinker Agent Runner",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tinker-agent": "./run-tinker-agent.rb"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"run-tinker-agent.rb",
|
|
10
|
+
"agents.rb",
|
|
11
|
+
"setup-agent.rb",
|
|
12
|
+
"bin/agent-bridge-tmux",
|
|
13
|
+
"bin/install-agent.sh"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
},
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC"
|
|
20
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Tinker Agent Runner
|
|
5
|
+
# Usage: npx tinker-agent [worker|planner|reviewer|orchestrator|researcher]
|
|
6
|
+
# npx tinker-agent attach [agent-type]
|
|
7
|
+
#
|
|
8
|
+
# Requirements:
|
|
9
|
+
# - Docker
|
|
10
|
+
# - Ruby
|
|
11
|
+
# - Dockerfile.sandbox in project root
|
|
12
|
+
# - tinker.env.json in project root (gitignored)
|
|
13
|
+
|
|
14
|
+
require "json"
|
|
15
|
+
|
|
16
|
+
# Load agent configs
|
|
17
|
+
require_relative "agents"
|
|
18
|
+
|
|
19
|
+
IMAGE_NAME = "tinker-sandbox"
|
|
20
|
+
|
|
21
|
+
AGENT_TYPES = AGENT_CONFIGS.keys.freeze
|
|
22
|
+
|
|
23
|
+
def load_config
|
|
24
|
+
config_file = File.join(Dir.pwd, "tinker.env.json")
|
|
25
|
+
|
|
26
|
+
unless File.exist?(config_file)
|
|
27
|
+
puts "❌ Error: tinker.env.json not found in current directory"
|
|
28
|
+
puts ""
|
|
29
|
+
puts "Create it:"
|
|
30
|
+
puts " curl -fsSL https://raw.githubusercontent.com/RoM4iK/tinker-public/main/tinker.env.example.json -o tinker.env.json"
|
|
31
|
+
puts " # Edit with your project config"
|
|
32
|
+
puts " echo 'tinker.env.json' >> .gitignore"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
JSON.parse(File.read(config_file))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check_dockerfile!
|
|
40
|
+
unless File.exist?("Dockerfile.sandbox")
|
|
41
|
+
puts "❌ Error: Dockerfile.sandbox not found"
|
|
42
|
+
puts ""
|
|
43
|
+
puts "Please create Dockerfile.sandbox by copying your existing Dockerfile"
|
|
44
|
+
puts "and adding the required agent dependencies."
|
|
45
|
+
puts ""
|
|
46
|
+
puts "See https://github.com/RoM4iK/tinker-public/blob/main/README.md for instructions."
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_docker_image
|
|
52
|
+
check_dockerfile!
|
|
53
|
+
|
|
54
|
+
user_id = `id -u`.strip
|
|
55
|
+
group_id = `id -g`.strip
|
|
56
|
+
|
|
57
|
+
puts "🏗️ Building Docker image..."
|
|
58
|
+
|
|
59
|
+
success = system(
|
|
60
|
+
"docker", "build",
|
|
61
|
+
"--build-arg", "USER_ID=#{user_id}",
|
|
62
|
+
"--build-arg", "GROUP_ID=#{group_id}",
|
|
63
|
+
"-t", IMAGE_NAME,
|
|
64
|
+
"-f", "Dockerfile.sandbox",
|
|
65
|
+
"."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
unless success
|
|
69
|
+
puts "❌ Failed to build Docker image"
|
|
70
|
+
exit 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts "✅ Docker image built"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def run_agent(agent_type, config)
|
|
77
|
+
unless AGENT_TYPES.include?(agent_type)
|
|
78
|
+
puts "❌ Unknown agent type: #{agent_type}"
|
|
79
|
+
puts " Available: #{AGENT_TYPES.join(', ')}"
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
agent_def = AGENT_CONFIGS[agent_type]
|
|
84
|
+
agent_config = config.dig("agents", agent_type) || {}
|
|
85
|
+
container_name = agent_config["container_name"] || agent_def[:name]
|
|
86
|
+
|
|
87
|
+
puts "🚀 Starting #{agent_type} agent..."
|
|
88
|
+
|
|
89
|
+
# Stop existing container if running
|
|
90
|
+
system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
|
|
91
|
+
|
|
92
|
+
# Write banner to a persistent temp file (not auto-deleted)
|
|
93
|
+
banner_path = "/tmp/tinker-agent-banner-#{agent_type}.txt"
|
|
94
|
+
File.write(banner_path, agent_def[:banner])
|
|
95
|
+
|
|
96
|
+
docker_cmd = [
|
|
97
|
+
"docker", "run", "-d",
|
|
98
|
+
"--name", container_name,
|
|
99
|
+
"--network=host",
|
|
100
|
+
# Mount Claude config
|
|
101
|
+
"-v", "#{ENV['HOME']}/.claude.json:/tmp/cfg/claude.json:ro",
|
|
102
|
+
"-v", "#{ENV['HOME']}/.claude:/tmp/cfg/claude_dir:ro",
|
|
103
|
+
# Mount agent banner for CLAUDE.md
|
|
104
|
+
"-v", "#{banner_path}:/tmp/agent-banner.txt:ro",
|
|
105
|
+
# Skills are downloaded inside container (see entrypoint)
|
|
106
|
+
"-e", "TINKER_VERSION=main",
|
|
107
|
+
# Pass config as env vars
|
|
108
|
+
"-e", "AGENT_TYPE=#{agent_type}",
|
|
109
|
+
"-e", "PROJECT_ID=#{config['project_id']}",
|
|
110
|
+
"-e", "RAILS_WS_URL=#{config['rails_ws_url']}",
|
|
111
|
+
"-e", "RAILS_API_URL=#{config['rails_api_url']}",
|
|
112
|
+
"-e", "RAILS_API_KEY=#{agent_config['mcp_api_key']}"
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
# Add Anthropic config
|
|
116
|
+
if (anthropic = config["anthropic"])
|
|
117
|
+
docker_cmd += ["-e", "ANTHROPIC_BASE_URL=#{anthropic['base_url']}"] if anthropic["base_url"]
|
|
118
|
+
docker_cmd += ["-e", "ANTHROPIC_MODEL=#{anthropic['model']}"] if anthropic["model"]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Add GitHub auth
|
|
122
|
+
github = config["github"] || {}
|
|
123
|
+
if github["method"] == "app"
|
|
124
|
+
docker_cmd += [
|
|
125
|
+
"-e", "GITHUB_APP_CLIENT_ID=#{github['app_client_id']}",
|
|
126
|
+
"-e", "GITHUB_APP_INSTALLATION_ID=#{github['app_installation_id']}",
|
|
127
|
+
# Path is set dynamically in entrypoint.sh based on user home
|
|
128
|
+
"-v", "#{github['app_private_key_path']}:/tmp/github-app-privkey.pem:ro"
|
|
129
|
+
]
|
|
130
|
+
puts "🔐 Using GitHub App authentication"
|
|
131
|
+
elsif github["token"]
|
|
132
|
+
docker_cmd += ["-e", "GH_TOKEN=#{github['token']}"]
|
|
133
|
+
puts "🔑 Using GitHub token authentication"
|
|
134
|
+
else
|
|
135
|
+
puts "⚠️ Warning: No GitHub authentication configured"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Add git config
|
|
139
|
+
if (git_config = config["git"])
|
|
140
|
+
docker_cmd += ["-e", "GIT_USER_NAME=#{git_config['user_name']}"] if git_config["user_name"]
|
|
141
|
+
docker_cmd += ["-e", "GIT_USER_EMAIL=#{git_config['user_email']}"] if git_config["user_email"]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check for local setup-agent.rb (for development)
|
|
145
|
+
local_setup_script = File.join(File.dirname(__FILE__), "setup-agent.rb")
|
|
146
|
+
|
|
147
|
+
# Check for local agent-bridge binaries (for development)
|
|
148
|
+
local_bridge = File.join(Dir.pwd, "bin", "agent-bridge")
|
|
149
|
+
local_tmux = File.join(File.dirname(__FILE__), "bin", "agent-bridge-tmux")
|
|
150
|
+
|
|
151
|
+
mounts = []
|
|
152
|
+
if File.exist?(local_setup_script)
|
|
153
|
+
puts "🔧 Using local setup-agent.rb for development"
|
|
154
|
+
mounts += ["-v", "#{File.expand_path(local_setup_script)}:/tmp/setup-agent.rb:ro"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if File.exist?(local_bridge)
|
|
158
|
+
puts "🔧 Using local agent-bridge binary"
|
|
159
|
+
mounts += ["-v", "#{local_bridge}:/tmp/agent-bridge:ro"]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if File.exist?(local_tmux)
|
|
163
|
+
mounts += ["-v", "#{File.expand_path(local_tmux)}:/tmp/agent-bridge-tmux:ro"]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
docker_cmd += mounts
|
|
167
|
+
|
|
168
|
+
if File.exist?(local_setup_script)
|
|
169
|
+
docker_cmd += [IMAGE_NAME, "ruby", "/tmp/setup-agent.rb"]
|
|
170
|
+
else
|
|
171
|
+
docker_cmd += [IMAGE_NAME]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
success = system(*docker_cmd)
|
|
175
|
+
|
|
176
|
+
if success
|
|
177
|
+
puts "✅ Agent started in background"
|
|
178
|
+
puts ""
|
|
179
|
+
puts " Attach: npx tinker-agent attach #{agent_type}"
|
|
180
|
+
puts " Logs: docker logs -f #{container_name}"
|
|
181
|
+
puts " Stop: docker stop #{container_name}"
|
|
182
|
+
else
|
|
183
|
+
puts "❌ Failed to start agent"
|
|
184
|
+
exit 1
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def attach_to_agent(agent_type, config)
|
|
189
|
+
unless AGENT_TYPES.include?(agent_type)
|
|
190
|
+
puts "❌ Unknown agent type: #{agent_type}"
|
|
191
|
+
exit 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
agent_config = config.dig("agents", agent_type) || {}
|
|
195
|
+
container_name = agent_config["container_name"] || "tinker-#{agent_type}"
|
|
196
|
+
|
|
197
|
+
running = `docker ps --filter name=^#{container_name}$ --format '{{.Names}}'`.strip
|
|
198
|
+
|
|
199
|
+
if running.empty?
|
|
200
|
+
puts "❌ #{agent_type} agent is not running"
|
|
201
|
+
puts " Start with: npx tinker-agent #{agent_type}"
|
|
202
|
+
exit 1
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
puts "📎 Attaching to #{agent_type} agent..."
|
|
206
|
+
|
|
207
|
+
# Determine the user to attach as
|
|
208
|
+
# We assume the agent is running as the user with the same UID as the host user
|
|
209
|
+
# (since that's how we build the image)
|
|
210
|
+
uid = Process.uid
|
|
211
|
+
user = `docker exec #{container_name} getent passwd #{uid} | cut -d: -f1`.strip
|
|
212
|
+
|
|
213
|
+
if user.empty?
|
|
214
|
+
# Fallback to 'claude' if detection fails (e.g. different UID mapping)
|
|
215
|
+
puts "⚠️ Could not detect agent user for UID #{uid}, defaulting to 'claude'"
|
|
216
|
+
user = "claude"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
puts " User: #{user} (UID: #{uid})"
|
|
220
|
+
|
|
221
|
+
# Attach to agent session which has the status bar
|
|
222
|
+
# Must run as agent user since tmux server runs under that user
|
|
223
|
+
exec("docker", "exec", "-it", "-u", user, container_name, "tmux", "attach", "-t", "agent")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def show_usage
|
|
227
|
+
puts "Tinker Agent Runner"
|
|
228
|
+
puts ""
|
|
229
|
+
puts "Usage: npx tinker-agent [worker|planner|reviewer|orchestrator|researcher]"
|
|
230
|
+
puts " npx tinker-agent attach [agent-type]"
|
|
231
|
+
puts ""
|
|
232
|
+
puts "Setup:"
|
|
233
|
+
puts " 1. Create Dockerfile.sandbox (see https://github.com/RoM4iK/tinker-public/blob/main/README.md)"
|
|
234
|
+
puts " 2. curl -fsSL https://raw.githubusercontent.com/RoM4iK/tinker-public/main/tinker.env.example.json -o tinker.env.json"
|
|
235
|
+
puts " 3. Edit tinker.env.json with your config"
|
|
236
|
+
puts " 4. echo 'tinker.env.json' >> .gitignore"
|
|
237
|
+
puts " 5. npx tinker-agent worker"
|
|
238
|
+
exit 1
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Main
|
|
242
|
+
show_usage if ARGV.empty?
|
|
243
|
+
|
|
244
|
+
command = ARGV[0].downcase
|
|
245
|
+
|
|
246
|
+
if command == "attach"
|
|
247
|
+
agent_type = ARGV[1]&.downcase
|
|
248
|
+
abort "Usage: npx tinker-agent attach [agent-type]" unless agent_type
|
|
249
|
+
config = load_config
|
|
250
|
+
attach_to_agent(agent_type, config)
|
|
251
|
+
else
|
|
252
|
+
config = load_config
|
|
253
|
+
build_docker_image
|
|
254
|
+
run_agent(command, config)
|
|
255
|
+
end
|