actionagent 1.7.2 → 1.8.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.
- checksums.yaml +4 -4
- data/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +59 -54
- data/app/controllers/action_agent/api/agents_controller.rb +19 -3
- data/app/controllers/action_agent/api/code_sessions_controller.rb +156 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +27 -130
- data/app/controllers/action_agent/api/github_connections_controller.rb +147 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +38 -5
- data/app/controllers/action_agent/api/mcp_servers_controller.rb +10 -1
- data/app/controllers/action_agent/api/provider_keys_controller.rb +54 -3
- data/app/controllers/action_agent/api/provider_models_controller.rb +10 -6
- data/app/controllers/action_agent/api/sandboxes_controller.rb +97 -6
- data/app/controllers/concerns/action_agent/api/evaluation_run_starting.rb +93 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +507 -0
- data/app/controllers/concerns/action_agent/api/run_sandbox.rb +65 -0
- data/app/jobs/action_agent/code_session_job.rb +166 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +80 -11
- data/app/jobs/action_agent/sandbox_provision_job.rb +122 -14
- data/app/jobs/action_agent/sandbox_run_job.rb +10 -3
- data/app/models/action_agent/agent.rb +16 -6
- data/app/models/action_agent/agent_run.rb +20 -1
- data/app/models/action_agent/code_session.rb +141 -0
- data/app/models/action_agent/evaluation_run.rb +23 -1
- data/app/models/action_agent/github_connection.rb +75 -0
- data/app/models/action_agent/provider_key.rb +142 -10
- data/app/models/action_agent/sandbox_session.rb +193 -17
- data/app/serializers/action_agent/evaluation_serializer.rb +118 -0
- data/app/serializers/action_agent/telemetry_trace_serializer.rb +10 -2
- data/app/services/action_agent/agent_execution_service.rb +4 -2
- data/app/services/action_agent/agent_tool_roster.rb +20 -9
- data/app/services/action_agent/claude_code_auth.rb +86 -0
- data/app/services/action_agent/dashboard_assistant_service.rb +47 -5
- data/app/services/action_agent/evaluation_tool_resolver.rb +18 -0
- data/app/services/action_agent/github_client.rb +111 -0
- data/app/services/action_agent/local_sandbox_backend.rb +1689 -0
- data/app/services/action_agent/local_sandbox_databases.rb +257 -0
- data/app/services/action_agent/mcp_client.rb +5 -1
- data/app/services/action_agent/mcp_tool_dispatcher.rb +137 -17
- data/app/services/action_agent/mock_sandbox_backend.rb +39 -0
- data/app/services/action_agent/ollama_host_probe.rb +75 -0
- data/app/services/action_agent/payload_bounds.rb +36 -0
- data/app/services/action_agent/sandbox_manifest.rb +67 -0
- data/app/services/action_agent/sandbox_orchestrator.rb +69 -14
- data/app/services/action_agent/scenario_evaluation_runner.rb +50 -4
- data/app/services/action_agent/secret_scrubber.rb +37 -0
- data/app/services/action_agent/tool_discovery.rb +19 -5
- data/config/routes.rb +22 -3
- data/lib/action_agent/engine.rb +1 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +147 -3
- data/lib/generators/action_agent/install_generator.rb +30 -3
- data/lib/generators/action_agent/templates/action_agent.rb.erb +44 -0
- data/lib/generators/action_agent/templates/add_provider_key_api_key.rb.erb +25 -0
- data/lib/generators/action_agent/templates/create_active_agent_code_sessions.rb.erb +59 -0
- data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +2 -0
- data/lib/generators/action_agent/templates/create_active_agent_github_connections.rb.erb +61 -0
- data/lib/tasks/claude_code.rake +16 -0
- data/lib/tasks/sandbox.rake +26 -0
- metadata +23 -1
|
@@ -31,7 +31,7 @@ module ActionAgent
|
|
|
31
31
|
PROCESSING_DISCLOSURE = "The selected provider receives your current message, bounded conversation history, and authorized report excerpts requested through assistant tools."
|
|
32
32
|
LIMITATIONS = [
|
|
33
33
|
"Reports describe recorded behavior; no repository branch or current main has been verified.",
|
|
34
|
-
"GitHub
|
|
34
|
+
"This assistant cannot connect GitHub, start checkout sandboxes, or run Claude Code sessions: do that in Settings → Integrations. COI execution is not available.",
|
|
35
35
|
"Agent proposals are drafts only. Review them in the builder before saving or running."
|
|
36
36
|
].freeze
|
|
37
37
|
INSTRUCTIONS = <<~TEXT.freeze
|
|
@@ -55,9 +55,12 @@ module ActionAgent
|
|
|
55
55
|
agents. These groups do not provide repository editing, arbitrary browser
|
|
56
56
|
automation, or private database access. Instructions alone do not add tools
|
|
57
57
|
or data access. Explain missing capabilities when proposing an agent.
|
|
58
|
-
GitHub
|
|
59
|
-
evaluations,
|
|
60
|
-
|
|
58
|
+
You cannot connect GitHub, start checkout sandboxes, run Claude Code
|
|
59
|
+
sessions, run evaluations, or publish PRs. The developer connects GitHub
|
|
60
|
+
and Claude Code, starts checkout sandboxes, and runs Claude Code sessions
|
|
61
|
+
in Settings → Integrations; point them there instead of claiming a sandbox
|
|
62
|
+
or session started. COI execution is not available in this version.
|
|
63
|
+
Explain those limitations without suggesting fake authentication links.
|
|
61
64
|
TEXT
|
|
62
65
|
TOOL_DEFINITIONS = [
|
|
63
66
|
{
|
|
@@ -107,7 +110,7 @@ module ActionAgent
|
|
|
107
110
|
providers: DEFAULT_MODELS.map { |id, model| { id: id, configured: provider_configured?(id), default_model: model } },
|
|
108
111
|
defaults: { provider: nil, model: nil },
|
|
109
112
|
processing: { consent_required: true, disclosure: PROCESSING_DISCLOSURE },
|
|
110
|
-
connections:
|
|
113
|
+
connections: connections,
|
|
111
114
|
limits: { message_characters: MAX_MESSAGE_CHARACTERS, history_messages: MAX_HISTORY_MESSAGES, history_characters: MAX_HISTORY_CHARACTERS },
|
|
112
115
|
limitations: LIMITATIONS
|
|
113
116
|
}
|
|
@@ -191,6 +194,45 @@ module ActionAgent
|
|
|
191
194
|
|
|
192
195
|
private
|
|
193
196
|
|
|
197
|
+
# What the owner has set up in Settings → Integrations, reported by the
|
|
198
|
+
# assistant's configuration endpoint (booleans only, never a token). The
|
|
199
|
+
# model is not told: it starts nothing, and sandboxes and Claude Code
|
|
200
|
+
# sessions are started from that view (see LIMITATIONS).
|
|
201
|
+
#
|
|
202
|
+
# Claude Code is connected by ClaudeCodeAuth's rule, as the sandbox
|
|
203
|
+
# listing reports it: an API key the owner connected (auth "api_key"), or
|
|
204
|
+
# this machine's own login (auth "local_login", with its status).
|
|
205
|
+
def connections
|
|
206
|
+
claude_code = ClaudeCodeAuth.status(ProviderKey.for_owner(@owner))
|
|
207
|
+
{
|
|
208
|
+
github: { supported: true, connected: GithubConnection.for_owner(@owner).exists? },
|
|
209
|
+
claude_code: {
|
|
210
|
+
supported: code_sessions_supported?,
|
|
211
|
+
connected: claude_code[:connected],
|
|
212
|
+
auth: claude_code[:mode],
|
|
213
|
+
login: claude_code[:login]
|
|
214
|
+
}.compact,
|
|
215
|
+
coi: { supported: false }
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Claude Code needs a sandbox backend that runs sessions, which :mock and
|
|
220
|
+
# :local do and a host's may not. A backend that cannot even be loaded (a
|
|
221
|
+
# misspelled class in ActionAgent.sandbox_backends, or a class file that
|
|
222
|
+
# requires an SDK the host doesn't bundle) runs none, and must not take
|
|
223
|
+
# the assistant's configuration down with it. The SDK case raises
|
|
224
|
+
# LoadError, a ScriptError rather than a StandardError.
|
|
225
|
+
#
|
|
226
|
+
# Nor can one whose Claude Code authentication does not work there
|
|
227
|
+
# (ActionAgent.claude_code_auth = :local_login needs :local).
|
|
228
|
+
def code_sessions_supported?
|
|
229
|
+
orchestrator = SandboxOrchestrator.new
|
|
230
|
+
orchestrator.supports?(:code_session) && ClaudeCodeAuth.backend_refusal(orchestrator).nil?
|
|
231
|
+
rescue StandardError, LoadError => e
|
|
232
|
+
Rails.logger.warn("[ActionAgent] sandbox backend unavailable: #{e.message}")
|
|
233
|
+
false
|
|
234
|
+
end
|
|
235
|
+
|
|
194
236
|
def require_processing_consent!
|
|
195
237
|
return if @allow_provider_processing == true
|
|
196
238
|
|
|
@@ -80,6 +80,24 @@ module ActionAgent
|
|
|
80
80
|
configured_keys.to_a
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# The tools the agent's entry for +key+ allows, or nil when that entry
|
|
84
|
+
# names none — which offers every tool the server serves. This is the rule
|
|
85
|
+
# AgentToolRoster#allowed_tools reads for the Tools tab, which saves
|
|
86
|
+
# {key, name, tools: [...]} when some are switched off and tools: [] when
|
|
87
|
+
# all are: a later entry for the same key wins, as it does there.
|
|
88
|
+
#
|
|
89
|
+
# @param key [String] a server key
|
|
90
|
+
# @return [Array<String>, nil]
|
|
91
|
+
def allowed_tools_for(key)
|
|
92
|
+
entry = configured_entries.select { |candidate| normalize(entry_key(candidate)) == normalize(key) }.last
|
|
93
|
+
return nil unless entry.respond_to?(:key?)
|
|
94
|
+
|
|
95
|
+
names = entry["tools"] || entry[:tools]
|
|
96
|
+
return nil if names.nil?
|
|
97
|
+
|
|
98
|
+
Array(names).filter_map { |tool| (tool.respond_to?(:key?) ? tool["name"] || tool[:name] : tool).to_s.strip.presence }
|
|
99
|
+
end
|
|
100
|
+
|
|
83
101
|
private
|
|
84
102
|
|
|
85
103
|
# The catalog's name when it has one; otherwise the name the agent's
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# The few GitHub calls the dashboard makes: the OAuth code exchange, the
|
|
5
|
+
# authenticated user, and the repositories that user can reach. Plain
|
|
6
|
+
# Net::HTTP, like the provider model lookups, so the engine carries no
|
|
7
|
+
# GitHub SDK.
|
|
8
|
+
class GithubClient
|
|
9
|
+
API = "https://api.github.com"
|
|
10
|
+
TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
11
|
+
AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
|
|
12
|
+
TIMEOUT_SECONDS = 8
|
|
13
|
+
# 100 per page is GitHub's maximum; five pages bounds a picker at 500.
|
|
14
|
+
PER_PAGE = 100
|
|
15
|
+
MAX_PAGES = 5
|
|
16
|
+
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
# The token was revoked or expired: the owner has to connect again.
|
|
19
|
+
class Unauthorized < Error; end
|
|
20
|
+
|
|
21
|
+
def self.authorize_url(redirect_uri:, state:)
|
|
22
|
+
query = {
|
|
23
|
+
client_id: ActionAgent.github_client_id,
|
|
24
|
+
redirect_uri: redirect_uri,
|
|
25
|
+
scope: ActionAgent.github_oauth_scopes,
|
|
26
|
+
state: state,
|
|
27
|
+
allow_signup: "false"
|
|
28
|
+
}.to_query
|
|
29
|
+
|
|
30
|
+
"#{AUTHORIZE_URL}?#{query}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Trades an OAuth code for { access_token:, scope: }.
|
|
34
|
+
def self.exchange_code(code:, redirect_uri:)
|
|
35
|
+
uri = URI.parse(TOKEN_URL)
|
|
36
|
+
request = Net::HTTP::Post.new(uri, "Accept" => "application/json")
|
|
37
|
+
request.set_form_data(
|
|
38
|
+
client_id: ActionAgent.github_client_id,
|
|
39
|
+
client_secret: ActionAgent.github_client_secret,
|
|
40
|
+
code: code,
|
|
41
|
+
redirect_uri: redirect_uri
|
|
42
|
+
)
|
|
43
|
+
data = perform(uri, request)
|
|
44
|
+
raise Error, data["error_description"].presence || data["error"] if data["error"].present?
|
|
45
|
+
raise Error, "GitHub returned no access token" if data["access_token"].blank?
|
|
46
|
+
|
|
47
|
+
{ access_token: data["access_token"], scope: data["scope"].to_s }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.perform(uri, request)
|
|
51
|
+
response = Net::HTTP.start(
|
|
52
|
+
uri.host, uri.port,
|
|
53
|
+
use_ssl: true, open_timeout: TIMEOUT_SECONDS, read_timeout: TIMEOUT_SECONDS
|
|
54
|
+
) { |http| http.request(request) }
|
|
55
|
+
|
|
56
|
+
raise Unauthorized, "GitHub rejected the token" if response.code.to_i == 401
|
|
57
|
+
raise Error, "GitHub answered #{response.code}" unless response.code.to_i.between?(200, 299)
|
|
58
|
+
|
|
59
|
+
JSON.parse(response.body.presence || "{}")
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
raise Error, "GitHub answered with something other than JSON"
|
|
62
|
+
rescue Timeout::Error, SocketError, SystemCallError, OpenSSL::SSL::SSLError => e
|
|
63
|
+
raise Error, "GitHub is unreachable (#{e.class.name})"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(access_token)
|
|
67
|
+
@access_token = access_token
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def user
|
|
71
|
+
get("/user")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Every repository the token reaches (owned, collaborated on, or through
|
|
75
|
+
# an organization), most recently pushed first, reduced to the fields the
|
|
76
|
+
# dashboard stores.
|
|
77
|
+
def repositories
|
|
78
|
+
(1..MAX_PAGES).each_with_object([]) do |page, all|
|
|
79
|
+
batch = get("/user/repos", per_page: PER_PAGE, page: page, sort: "pushed",
|
|
80
|
+
affiliation: "owner,collaborator,organization_member")
|
|
81
|
+
all.concat(Array(batch).map { |repo| self.class.slice_repository(repo) })
|
|
82
|
+
break all if Array(batch).size < PER_PAGE
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.slice_repository(repo)
|
|
87
|
+
{
|
|
88
|
+
"id" => repo["id"],
|
|
89
|
+
"full_name" => repo["full_name"],
|
|
90
|
+
"private" => repo["private"] == true,
|
|
91
|
+
"default_branch" => repo["default_branch"].presence || "main",
|
|
92
|
+
"description" => repo["description"],
|
|
93
|
+
"html_url" => repo["html_url"],
|
|
94
|
+
"pushed_at" => repo["pushed_at"]
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def get(path, params = {})
|
|
101
|
+
uri = URI.parse("#{API}#{path}")
|
|
102
|
+
uri.query = params.to_query if params.any?
|
|
103
|
+
request = Net::HTTP::Get.new(uri,
|
|
104
|
+
"Accept" => "application/vnd.github+json",
|
|
105
|
+
"Authorization" => "Bearer #{@access_token}",
|
|
106
|
+
"X-GitHub-Api-Version" => "2022-11-28",
|
|
107
|
+
"User-Agent" => "activeagent-dashboard")
|
|
108
|
+
self.class.perform(uri, request)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|