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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/action_agent.css +1 -1
  3. data/app/assets/builds/action_agent.js +59 -54
  4. data/app/controllers/action_agent/api/agents_controller.rb +19 -3
  5. data/app/controllers/action_agent/api/code_sessions_controller.rb +156 -0
  6. data/app/controllers/action_agent/api/evaluations_controller.rb +27 -130
  7. data/app/controllers/action_agent/api/github_connections_controller.rb +147 -0
  8. data/app/controllers/action_agent/api/mcp_controller.rb +38 -5
  9. data/app/controllers/action_agent/api/mcp_servers_controller.rb +10 -1
  10. data/app/controllers/action_agent/api/provider_keys_controller.rb +54 -3
  11. data/app/controllers/action_agent/api/provider_models_controller.rb +10 -6
  12. data/app/controllers/action_agent/api/sandboxes_controller.rb +97 -6
  13. data/app/controllers/concerns/action_agent/api/evaluation_run_starting.rb +93 -0
  14. data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +507 -0
  15. data/app/controllers/concerns/action_agent/api/run_sandbox.rb +65 -0
  16. data/app/jobs/action_agent/code_session_job.rb +166 -0
  17. data/app/jobs/action_agent/sandbox_cleanup_job.rb +80 -11
  18. data/app/jobs/action_agent/sandbox_provision_job.rb +122 -14
  19. data/app/jobs/action_agent/sandbox_run_job.rb +10 -3
  20. data/app/models/action_agent/agent.rb +16 -6
  21. data/app/models/action_agent/agent_run.rb +20 -1
  22. data/app/models/action_agent/code_session.rb +141 -0
  23. data/app/models/action_agent/evaluation_run.rb +23 -1
  24. data/app/models/action_agent/github_connection.rb +75 -0
  25. data/app/models/action_agent/provider_key.rb +142 -10
  26. data/app/models/action_agent/sandbox_session.rb +193 -17
  27. data/app/serializers/action_agent/evaluation_serializer.rb +118 -0
  28. data/app/serializers/action_agent/telemetry_trace_serializer.rb +10 -2
  29. data/app/services/action_agent/agent_execution_service.rb +4 -2
  30. data/app/services/action_agent/agent_tool_roster.rb +20 -9
  31. data/app/services/action_agent/claude_code_auth.rb +86 -0
  32. data/app/services/action_agent/dashboard_assistant_service.rb +47 -5
  33. data/app/services/action_agent/evaluation_tool_resolver.rb +18 -0
  34. data/app/services/action_agent/github_client.rb +111 -0
  35. data/app/services/action_agent/local_sandbox_backend.rb +1689 -0
  36. data/app/services/action_agent/local_sandbox_databases.rb +257 -0
  37. data/app/services/action_agent/mcp_client.rb +5 -1
  38. data/app/services/action_agent/mcp_tool_dispatcher.rb +137 -17
  39. data/app/services/action_agent/mock_sandbox_backend.rb +39 -0
  40. data/app/services/action_agent/ollama_host_probe.rb +75 -0
  41. data/app/services/action_agent/payload_bounds.rb +36 -0
  42. data/app/services/action_agent/sandbox_manifest.rb +67 -0
  43. data/app/services/action_agent/sandbox_orchestrator.rb +69 -14
  44. data/app/services/action_agent/scenario_evaluation_runner.rb +50 -4
  45. data/app/services/action_agent/secret_scrubber.rb +37 -0
  46. data/app/services/action_agent/tool_discovery.rb +19 -5
  47. data/config/routes.rb +22 -3
  48. data/lib/action_agent/engine.rb +1 -0
  49. data/lib/action_agent/version.rb +1 -1
  50. data/lib/action_agent.rb +147 -3
  51. data/lib/generators/action_agent/install_generator.rb +30 -3
  52. data/lib/generators/action_agent/templates/action_agent.rb.erb +44 -0
  53. data/lib/generators/action_agent/templates/add_provider_key_api_key.rb.erb +25 -0
  54. data/lib/generators/action_agent/templates/create_active_agent_code_sessions.rb.erb +59 -0
  55. data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +2 -0
  56. data/lib/generators/action_agent/templates/create_active_agent_github_connections.rb.erb +61 -0
  57. data/lib/tasks/claude_code.rake +16 -0
  58. data/lib/tasks/sandbox.rake +26 -0
  59. 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 connections, COI execution, and native Claude Code session connections are not implemented here.",
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 auth, repo checkout, COI, native Claude Code sessions, running
59
- evaluations, and publishing PRs are not available in this version. Explain
60
- those limitations without suggesting fake authentication links.
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: %i[github coi claude_code].index_with { { supported: false } },
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