actionagent 1.7.2 → 1.8.1

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 (69) 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 +168 -0
  6. data/app/controllers/action_agent/api/evaluations_controller.rb +74 -131
  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 +105 -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 +509 -0
  15. data/app/controllers/concerns/action_agent/api/run_sandbox.rb +65 -0
  16. data/app/jobs/action_agent/code_session_job.rb +171 -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 +49 -6
  21. data/app/models/action_agent/agent_run.rb +20 -1
  22. data/app/models/action_agent/code_session.rb +145 -0
  23. data/app/models/action_agent/evaluation.rb +64 -0
  24. data/app/models/action_agent/evaluation_run.rb +166 -54
  25. data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
  26. data/app/models/action_agent/github_connection.rb +75 -0
  27. data/app/models/action_agent/model_pricing.rb +214 -34
  28. data/app/models/action_agent/provider_key.rb +148 -10
  29. data/app/models/action_agent/sandbox_session.rb +194 -17
  30. data/app/serializers/action_agent/evaluation_serializer.rb +149 -0
  31. data/app/serializers/action_agent/telemetry_trace_serializer.rb +10 -2
  32. data/app/services/action_agent/agent_execution_service.rb +4 -2
  33. data/app/services/action_agent/agent_scorecard.rb +50 -21
  34. data/app/services/action_agent/agent_tool_roster.rb +20 -9
  35. data/app/services/action_agent/claude_code_auth.rb +86 -0
  36. data/app/services/action_agent/codex_session_events.rb +37 -0
  37. data/app/services/action_agent/dashboard_assistant_service.rb +47 -5
  38. data/app/services/action_agent/evaluation_report_import.rb +84 -5
  39. data/app/services/action_agent/evaluation_run_cost.rb +609 -0
  40. data/app/services/action_agent/evaluation_runner_service.rb +25 -5
  41. data/app/services/action_agent/evaluation_standing.rb +145 -0
  42. data/app/services/action_agent/evaluation_tool_resolver.rb +18 -0
  43. data/app/services/action_agent/github_client.rb +111 -0
  44. data/app/services/action_agent/local_sandbox_backend.rb +1715 -0
  45. data/app/services/action_agent/local_sandbox_databases.rb +257 -0
  46. data/app/services/action_agent/mcp_client.rb +5 -1
  47. data/app/services/action_agent/mcp_tool_dispatcher.rb +137 -17
  48. data/app/services/action_agent/mock_sandbox_backend.rb +39 -0
  49. data/app/services/action_agent/ollama_host_probe.rb +75 -0
  50. data/app/services/action_agent/payload_bounds.rb +36 -0
  51. data/app/services/action_agent/sandbox_manifest.rb +67 -0
  52. data/app/services/action_agent/sandbox_orchestrator.rb +82 -14
  53. data/app/services/action_agent/scenario_evaluation_runner.rb +52 -5
  54. data/app/services/action_agent/secret_scrubber.rb +37 -0
  55. data/app/services/action_agent/tool_discovery.rb +19 -5
  56. data/config/routes.rb +23 -4
  57. data/lib/action_agent/engine.rb +1 -0
  58. data/lib/action_agent/version.rb +1 -1
  59. data/lib/action_agent.rb +153 -3
  60. data/lib/generators/action_agent/install_generator.rb +35 -5
  61. data/lib/generators/action_agent/templates/action_agent.rb.erb +47 -0
  62. data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -0
  63. data/lib/generators/action_agent/templates/add_provider_key_api_key.rb.erb +25 -0
  64. data/lib/generators/action_agent/templates/create_active_agent_code_sessions.rb.erb +59 -0
  65. data/lib/generators/action_agent/templates/create_active_agent_dashboard_tables.rb.erb +2 -0
  66. data/lib/generators/action_agent/templates/create_active_agent_github_connections.rb.erb +61 -0
  67. data/lib/tasks/claude_code.rake +16 -0
  68. data/lib/tasks/sandbox.rake +26 -0
  69. metadata +28 -2
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # The runtime manifest: how a booted checkout tells the sandbox backend
5
+ # where its MCP facade answers and which bearer token opens it.
6
+ #
7
+ # { "mcp_path": "/activeagents/mcp", "mcp_token": "aa_..." }
8
+ #
9
+ # The checked-out app writes it with `bin/rails action_agent:sandbox:manifest`
10
+ # (it mounts this engine, so the task ships with it), and a backend reads it
11
+ # back with .parse. Both halves live here so they cannot drift.
12
+ module SandboxManifest
13
+ # Where the manifest task writes, when set; stdout otherwise.
14
+ PATH_ENV = "ACTION_AGENT_SANDBOX_MANIFEST"
15
+ # The dashboard API key the manifest hands out, created once per checkout.
16
+ KEY_NAME = "Checkout sandbox runtime"
17
+
18
+ class Error < StandardError; end
19
+
20
+ module_function
21
+
22
+ # Builds the manifest inside the booted app: the engine's MCP path under
23
+ # wherever the app mounts it, and an API key for the MCP facade.
24
+ #
25
+ # @param routes [ActionDispatch::Routing::RouteSet] the app's routes
26
+ # @return [Hash{String => String}]
27
+ def generate(routes: Rails.application.routes)
28
+ # Rails 8 loads routes lazily in development and test; the mount is
29
+ # invisible until they are.
30
+ Rails.application.reload_routes_unless_loaded if Rails.application.respond_to?(:reload_routes_unless_loaded)
31
+
32
+ mount = ActiveAgent::Telemetry::Configuration.new.mount_path_in(routes)
33
+ raise Error, "ActionAgent::Engine is not mounted in this app's routes" if mount.nil?
34
+
35
+ { "mcp_path" => "#{mount}/mcp", "mcp_token" => api_key.token }
36
+ end
37
+
38
+ # The checkout's own dashboard API key for the facade. Reused across
39
+ # runs so a re-run manifest does not mint a key per boot. A host that
40
+ # owns keys by account or user gets a key with no owner, which reaches
41
+ # no agents over MCP — the manifest says so rather than failing.
42
+ def api_key
43
+ ActionAgent::ApiKey.find_or_create_by!(name: KEY_NAME)
44
+ end
45
+
46
+ # Reads a manifest a checkout wrote.
47
+ #
48
+ # @param json [String]
49
+ # @return [Hash{String => String}] with "mcp_path" and "mcp_token"
50
+ def parse(json)
51
+ data = JSON.parse(json.to_s)
52
+ raise Error, "the manifest is not a JSON object" unless data.is_a?(Hash)
53
+
54
+ path = data["mcp_path"]
55
+ unless path.is_a?(String) && path.start_with?("/")
56
+ raise Error, "the manifest names no mcp_path (expected a path such as /activeagents/mcp)"
57
+ end
58
+
59
+ token = data["mcp_token"]
60
+ raise Error, "the manifest's mcp_token is not a string" unless token.nil? || token.is_a?(String)
61
+
62
+ { "mcp_path" => path, "mcp_token" => token }
63
+ rescue JSON::ParserError => e
64
+ raise Error, "the manifest is not JSON (#{e.message.truncate(120)})"
65
+ end
66
+ end
67
+ end
@@ -3,16 +3,14 @@
3
3
  module ActionAgent
4
4
  # SandboxOrchestrator
5
5
  #
6
- # Unified interface for managing agent sandbox sessions.
7
- # Supports multiple backends for cloud-agnostic deployment:
8
- #
9
- # - incus: Self-hosted Incus containers (any Linux host)
10
- # - cloud_run: Google Cloud Run Jobs (serverless)
11
- # - kubernetes: Kubernetes pods (GKE, EKS, self-hosted k8s)
6
+ # Unified interface for managing agent sandbox sessions. The engine ships
7
+ # two backends — :mock (in-memory, runs nothing) and :local (checkouts as
8
+ # child processes of the dashboard) — and a host registers the rest
9
+ # (Incus, Cloud Run, Kubernetes) in ActionAgent.sandbox_backends.
12
10
  #
13
11
  # Configuration:
14
- # Set SANDBOX_BACKEND environment variable to choose backend.
15
- # Default: "incus" for simplicity
12
+ # ActionAgent.sandbox_service, or the SANDBOX_BACKEND environment
13
+ # variable, which wins. Default: :mock.
16
14
  #
17
15
  # Usage:
18
16
  # orchestrator = SandboxOrchestrator.new
@@ -21,14 +19,21 @@ module ActionAgent
21
19
  # orchestrator.terminate(container_id)
22
20
  #
23
21
  class SandboxOrchestrator
24
- # The engine ships only the in-memory backend. Anything that talks to
25
- # real infrastructure (Incus, Kubernetes, Cloud Run) is registered by
26
- # the app that operates it, so the engine carries none of those SDKs:
22
+ # Anything that talks to real infrastructure (Incus, Kubernetes, Cloud
23
+ # Run) is registered by the app that operates it, so the engine carries
24
+ # none of those SDKs:
27
25
  #
28
26
  # ActionAgent.sandbox_backends = {
29
27
  # "cloud_run" => "CloudRunService"
30
28
  # }
31
- BUILT_IN_BACKENDS = { "mock" => "ActionAgent::MockSandboxBackend" }.freeze
29
+ #
30
+ # The engine also ships :local, which boots app_runtime checkouts as
31
+ # child processes of the dashboard itself (see LocalSandboxBackend and
32
+ # ActionAgent.local_sandboxes_enabled?).
33
+ BUILT_IN_BACKENDS = {
34
+ "mock" => "ActionAgent::MockSandboxBackend",
35
+ "local" => "ActionAgent::LocalSandboxBackend"
36
+ }.freeze
32
37
 
33
38
  # Backends disagree on what to call each verb. Candidates are tried in
34
39
  # order and the first the backend responds to wins, so a host-registered
@@ -38,7 +43,11 @@ module ActionAgent
38
43
  status: %i[status container_status pod_status job_status],
39
44
  terminate: %i[terminate terminate_pod cancel_job],
40
45
  list: %i[list_sandboxes list_sandbox_pods list_jobs],
41
- cleanup: %i[cleanup_expired cleanup_expired_pods cleanup_expired_jobs]
46
+ cleanup: %i[cleanup_expired cleanup_expired_pods cleanup_expired_jobs],
47
+ # Claude Code sessions inside an app_runtime checkout. Optional: a
48
+ # backend without them simply cannot run sessions (see #supports?).
49
+ code_session: %i[run_code_session],
50
+ cancel_code_session: %i[cancel_code_session]
42
51
  }.freeze
43
52
 
44
53
  class UnsupportedBackendError < StandardError; end
@@ -101,7 +110,12 @@ module ActionAgent
101
110
  instance_tier: result[:instance_tier] || tier&.id,
102
111
  resources: result[:resources],
103
112
  hourly_cost: result[:hourly_cost] || tier&.hourly_cost&.to_f,
104
- created_at: result[:created_at] || Time.current
113
+ created_at: result[:created_at] || Time.current,
114
+ # An app_runtime sandbox's backend clones sandbox_session.checkout_spec,
115
+ # boots the app, and reports where its MCP facade answers (and the
116
+ # bearer token it expects) so agents can use the checkout's tools.
117
+ mcp_url: result[:mcp_url],
118
+ mcp_token: result[:mcp_token]
105
119
  }
106
120
  end
107
121
 
@@ -153,6 +167,60 @@ module ActionAgent
153
167
  @backend.public_send(adapter_method(:cleanup))
154
168
  end
155
169
 
170
+ # The handle the backend would give +sandbox_session+'s sandbox, for a
171
+ # backend that derives it from the session (nil otherwise).
172
+ def handle_for(sandbox_session)
173
+ @backend.respond_to?(:handle_for) ? @backend.handle_for(sandbox_session) : nil
174
+ end
175
+
176
+ # Whether the backend can name a session's sandbox without a recorded
177
+ # handle (see #handle_for).
178
+ def derives_handles?
179
+ @backend.respond_to?(:handle_for)
180
+ end
181
+
182
+ # Whether the backend runs sandboxes as processes of the dashboard, on
183
+ # its own machine and as its own user (LocalSandboxBackend): the only
184
+ # place ActionAgent.claude_code_auth = :local_login can work.
185
+ def local?
186
+ @backend.is_a?(LocalSandboxBackend)
187
+ end
188
+
189
+ # Whether the backend implements +verb+ (an ADAPTER_METHODS key).
190
+ def supports?(verb)
191
+ ADAPTER_METHODS.fetch(verb).any? { |m| @backend.respond_to?(m) }
192
+ end
193
+
194
+ # Existing backends predate runner selection and support Claude only.
195
+ # A backend must explicitly advertise Codex before accepting its keys.
196
+ def supports_code_runner?(runner)
197
+ return false unless supports?(:code_session)
198
+
199
+ runners = @backend.respond_to?(:code_runners) ? @backend.code_runners : [ "claude_code" ]
200
+ Array(runners).include?(runner)
201
+ end
202
+
203
+ # Runs a Claude Code session in +sandbox_session+'s checkout, yielding
204
+ # each stream-json event (a Hash) as it arrives. Returns the backend's
205
+ # outcome: { exit_status:, diff: }.
206
+ def run_code_session(sandbox_session, code_session, &on_event)
207
+ # Checked when a session is requested too; this covers one queued
208
+ # before the configuration changed.
209
+ runner = code_session.try(:runner) || "claude_code"
210
+ unless supports_code_runner?(runner)
211
+ raise UnsupportedBackendError, "The #{backend_name} sandbox backend cannot run #{runner} sessions"
212
+ end
213
+ refusal = ClaudeCodeAuth.backend_refusal(self) unless runner == "codex"
214
+ raise UnsupportedBackendError, refusal if refusal
215
+
216
+ @backend.public_send(adapter_method(:code_session), sandbox_session, code_session, &on_event)
217
+ end
218
+
219
+ # Stops a running Claude Code session.
220
+ def cancel_code_session(sandbox_session, code_session)
221
+ @backend.public_send(adapter_method(:cancel_code_session), sandbox_session, code_session)
222
+ end
223
+
156
224
  # Check if the backend is healthy
157
225
  #
158
226
  # @return [Boolean] true if backend is reachable
@@ -17,7 +17,16 @@ module ActionAgent
17
17
  # "_models" — per model: pass rate, mean score, latency, tokens, cost, fault counts
18
18
  # "_recommendations" — faults grouped across scenarios with the fix each calls for
19
19
  # "_verdict" — the best model and why (judge-written when a judge is available)
20
- # "_selection" — the scenarios and models this run covered
20
+ # "_selection" — the scenarios and models this run covered, and
21
+ # the checkout sandbox it replayed against, if any
22
+ #
23
+ # A selection's `sandbox_id` names a checkout sandbox whose app runtime
24
+ # every replay reaches beside the agent's own MCP servers, as if the agent
25
+ # listed it (see Api::RunSandbox, which checked the caller owns it). The
26
+ # agent is not changed. The runtime is resolved again here, among the
27
+ # agent's owner's sessions, since the run may start well after it was
28
+ # asked for; one that is no longer live fails the run rather than
29
+ # replaying without the tools it was meant to test.
21
30
  class ScenarioEvaluationRunner < EvaluationRunnerService
22
31
  Evals = ActiveAgent::Evals
23
32
 
@@ -43,6 +52,7 @@ module ActionAgent
43
52
  specs = model_specs
44
53
  run = @run || @evaluation.evaluation_runs.create!(status: :pending)
45
54
  run.update!(status: :running, selection: selection_summary(scenarios, specs))
55
+ ensure_sandbox_live!
46
56
 
47
57
  if scenarios.empty?
48
58
  run.update!(status: :failed, error_message: "No scenarios selected — add scenarios to the evaluation or widen the selection",
@@ -151,10 +161,45 @@ module ActionAgent
151
161
  "scenario_ids" => scenarios.map(&:id),
152
162
  "scenario_keys" => scenarios.map(&:key),
153
163
  "group" => @selection[:group].presence,
154
- "models" => specs.map(&:to_h)
164
+ "models" => specs.map(&:to_h),
165
+ "sandbox" => sandbox_summary
155
166
  }.compact
156
167
  end
157
168
 
169
+ # --- sandbox ----------------------------------------------------------
170
+
171
+ # "sandbox:<session_id>" for a run against a checkout sandbox, or nil.
172
+ def sandbox_server_key
173
+ id = @selection[:sandbox_id]
174
+ "#{SandboxSession::RUNTIME_SERVER_PREFIX}#{id}" if id.is_a?(String) && id.present?
175
+ end
176
+
177
+ def sandbox_session
178
+ return @sandbox_session if defined?(@sandbox_session)
179
+
180
+ @sandbox_session = sandbox_server_key && SandboxSession.for_owner(owner).find_by(session_id: @selection[:sandbox_id])
181
+ end
182
+
183
+ # What the run records about its sandbox: which checkout, never its
184
+ # token.
185
+ def sandbox_summary
186
+ return nil unless sandbox_server_key
187
+
188
+ {
189
+ "session_id" => @selection[:sandbox_id],
190
+ "server_key" => sandbox_server_key,
191
+ "repository" => sandbox_session&.repository,
192
+ "repository_ref" => sandbox_session&.repository_ref
193
+ }.compact
194
+ end
195
+
196
+ def ensure_sandbox_live!
197
+ return unless sandbox_server_key
198
+ return if SandboxSession.runtime_server_entry(sandbox_server_key, owner: owner)
199
+
200
+ raise ArgumentError, "Sandbox #{@selection[:sandbox_id]} is no longer running; start it again, or run without it"
201
+ end
202
+
158
203
  # --- replay -----------------------------------------------------------
159
204
 
160
205
  def replay(scenario, spec)
@@ -167,7 +212,8 @@ module ActionAgent
167
212
  scenario.prompt,
168
213
  model_override: spec.model,
169
214
  provider_override: spec.provider,
170
- actor: replay_actor
215
+ actor: replay_actor,
216
+ runtime_sandbox: sandbox_server_key
171
217
  )
172
218
 
173
219
  Evals::Replay.new(
@@ -177,7 +223,8 @@ module ActionAgent
177
223
  input_tokens: agent_run.input_tokens,
178
224
  output_tokens: agent_run.output_tokens,
179
225
  error: agent_run.failed? ? agent_run.error_message.presence || "run failed" : nil,
180
- cost: ModelPricing.estimate(model: spec.model, input_tokens: agent_run.input_tokens, output_tokens: agent_run.output_tokens),
226
+ cost: ModelPricing.estimate(model: spec.model, provider: spec.provider, input_tokens: agent_run.input_tokens,
227
+ output_tokens: agent_run.output_tokens),
181
228
  metadata: { "agent_run_id" => agent_run.id }
182
229
  )
183
230
  end
@@ -303,7 +350,7 @@ module ActionAgent
303
350
  end
304
351
 
305
352
  def mcp_dispatcher
306
- @mcp_dispatcher ||= MCPToolDispatcher.new(@evaluation.agent)
353
+ @mcp_dispatcher ||= MCPToolDispatcher.new(@evaluation.agent, extra_server_keys: [ sandbox_server_key ].compact)
307
354
  end
308
355
 
309
356
  # A run whose every declared MCP server failed discovery scored an agent
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Replaces known secrets in text or nested JSON-like data before it is
5
+ # stored or shown: a sandbox's GitHub token and Claude Code credential can
6
+ # surface in a process's output (an error echoing a URL, a session that
7
+ # prints its environment), and none of that may reach a transcript, a log
8
+ # tail or an API response.
9
+ module SecretScrubber
10
+ MASK = "[REDACTED]"
11
+ # Shorter values are not credentials, and masking them would mangle text.
12
+ MIN_SECRET_LENGTH = 8
13
+
14
+ module_function
15
+
16
+ # @param value [String, Hash, Array, Object] what to scrub
17
+ # @param secrets [Array<String>] the values to mask
18
+ # @return a copy of +value+ with every secret masked
19
+ def scrub(value, secrets)
20
+ secrets = Array(secrets).compact.map(&:to_s).select { |secret| secret.length >= MIN_SECRET_LENGTH }.uniq
21
+ return value if secrets.empty?
22
+
23
+ # Longest first, so a secret that contains another is masked whole.
24
+ pattern = Regexp.union(secrets.sort_by { |secret| -secret.length })
25
+ deep_scrub(value, pattern)
26
+ end
27
+
28
+ def deep_scrub(value, pattern)
29
+ case value
30
+ when String then value.gsub(pattern, MASK)
31
+ when Hash then value.to_h { |key, item| [ key, deep_scrub(item, pattern) ] }
32
+ when Array then value.map { |item| deep_scrub(item, pattern) }
33
+ else value
34
+ end
35
+ end
36
+ end
37
+ end
@@ -35,6 +35,9 @@ module ActionAgent
35
35
  #
36
36
  # Scopes are passed in rather than derived, so the caller's ownership
37
37
  # rules (single-user, per-user, or multi-tenant) decide what is visible.
38
+ # The same goes for checkout sandbox runtimes: the caller hands in the live
39
+ # ones it may see (SandboxSession.runtime_server_listings), and they are
40
+ # listed beside the catalog under their "sandbox:<session_id>" keys.
38
41
  class ToolDiscovery
39
42
  DEFAULT_WINDOW_HOURS = 24 * 7
40
43
  MAX_WINDOW_HOURS = 24 * 90
@@ -53,16 +56,20 @@ module ActionAgent
53
56
  ORIGIN_BUILTIN = "builtin"
54
57
  ORIGIN_AGENT = "agent"
55
58
 
56
- attr_reader :traces, :agents, :window_hours, :since
59
+ attr_reader :traces, :agents, :window_hours, :since, :runtimes
57
60
 
58
61
  # @param traces [ActiveRecord::Relation] the traces the caller may read
59
62
  # @param agents [ActiveRecord::Relation] the agents the caller may reach
60
63
  # @param hours [Integer] how far back to look
61
- def initialize(traces:, agents:, hours: DEFAULT_WINDOW_HOURS)
64
+ # @param runtimes [Array<Hash>] the live checkout sandbox runtimes the
65
+ # caller may see, as SandboxSession.runtime_server_listings returns
66
+ # them — token-free catalog-shaped entries
67
+ def initialize(traces:, agents:, hours: DEFAULT_WINDOW_HOURS, runtimes: [])
62
68
  @traces = traces
63
69
  @agents = agents
64
70
  @window_hours = hours.to_i.clamp(1, MAX_WINDOW_HOURS)
65
71
  @since = @window_hours.hours.ago
72
+ @runtimes = Array(runtimes).index_by { |runtime| runtime[:key] }
66
73
  end
67
74
 
68
75
  # The full inventory: every tool seen in the window, plus the MCP servers
@@ -500,7 +507,7 @@ module ActionAgent
500
507
  end
501
508
  end
502
509
 
503
- keys = (MCPCatalog.keys + detected.keys + configured_servers.keys).uniq
510
+ keys = (MCPCatalog.keys + runtimes.keys + detected.keys + configured_servers.keys).uniq
504
511
 
505
512
  # detected has a default block that would materialize a bucket on
506
513
  # lookup, so unseen servers are passed through as an explicit nil.
@@ -509,7 +516,11 @@ module ActionAgent
509
516
  end
510
517
 
511
518
  def server_row(key, bucket)
512
- catalog = MCPCatalog.find(key)
519
+ # A live checkout runtime reads like a catalog entry: known, named after
520
+ # its repository and ref, with the endpoint it serves on. Its listing
521
+ # carries no bearer token, so nothing below can render one.
522
+ catalog = MCPCatalog.find(key) || runtimes[key]
523
+ runtime = SandboxSession.runtime_server_key?(key)
513
524
  configured = configured_servers[key].to_a.sort
514
525
  calls = bucket ? bucket[:calls] : 0
515
526
 
@@ -524,11 +535,14 @@ module ActionAgent
524
535
  docs_url: catalog&.fetch(:docs_url, nil),
525
536
  first_party: catalog ? catalog[:first_party] : false,
526
537
  requires_credentials: catalog ? catalog[:requires_credentials] : [],
527
- launchable: catalog ? catalog[:sandbox] : false,
538
+ # A runtime is already running — it was started from Settings ->
539
+ # Integrations — so there is nothing here to launch.
540
+ launchable: catalog && !runtime ? catalog[:sandbox] : false,
528
541
  sandbox_type: catalog&.fetch(:sandbox_type, nil),
529
542
  # Catalog membership is what "known" means — a server detected purely
530
543
  # from traffic is real but undocumented here, and the view says so.
531
544
  known: !catalog.nil?,
545
+ runtime: runtime,
532
546
  status: server_status(calls, configured, catalog),
533
547
  calls: calls,
534
548
  errors: bucket ? bucket[:errors] : 0,
data/config/routes.rb CHANGED
@@ -71,8 +71,8 @@ ActionAgent::Engine.routes.draw do
71
71
  end
72
72
  end
73
73
 
74
- # Sandboxes. The engine ships the in-memory backend; an operator registers
75
- # real ones (see ActionAgent.sandbox_backends).
74
+ # Sandboxes. The engine ships the in-memory and local backends; an
75
+ # operator registers the rest (see ActionAgent.sandbox_backends).
76
76
  resources :sandboxes, param: :id, only: [ :index, :create, :show, :destroy ] do
77
77
  collection do
78
78
  post :compare
@@ -80,6 +80,12 @@ ActionAgent::Engine.routes.draw do
80
80
  member do
81
81
  post :run
82
82
  end
83
+ # Claude Code sessions in an app_runtime sandbox's checkout.
84
+ resources :code_sessions, only: [ :index, :create, :show ] do
85
+ member do
86
+ post :cancel
87
+ end
88
+ end
83
89
  end
84
90
 
85
91
  # Tool inventory — auto-detected from the tool roster each generation
@@ -136,7 +142,7 @@ ActionAgent::Engine.routes.draw do
136
142
 
137
143
  # Agent output evaluations. A scenario suite also manages its scenarios
138
144
  # here, and exposes each run's per-scenario, per-model results.
139
- resources :evaluations, only: [ :index, :show, :create, :destroy ] do
145
+ resources :evaluations, only: [ :index, :show, :create, :update, :destroy ] do
140
146
  member do
141
147
  post :run
142
148
  get "runs/:run_id", action: :show_run, as: :run_result
@@ -151,7 +157,20 @@ ActionAgent::Engine.routes.draw do
151
157
  # Credentials: dashboard API keys (token shown once on create) and the
152
158
  # owner's own LLM provider credentials, both encrypted at rest.
153
159
  resources :api_keys, only: [ :index, :create, :destroy ]
154
- resources :provider_keys, only: [ :index, :create, :destroy ], param: :provider
160
+ resources :provider_keys, only: [ :index, :create, :destroy ], param: :provider do
161
+ # Reachability + model list for host-based providers (Ollama), for a
162
+ # submitted or the stored host. Read-only.
163
+ post :test, on: :collection
164
+ end
165
+
166
+ # The owner's GitHub connection: the OAuth web flow (connect redirects to
167
+ # GitHub, which returns to callback) and the repositories it makes
168
+ # available to checkout sandboxes.
169
+ resource :github_connection, only: [ :show, :update, :destroy ], controller: "github_connections" do
170
+ get :repositories
171
+ get :connect
172
+ get :callback
173
+ end
155
174
 
156
175
  # Model catalogs for the agent builder (Ollama queried live from the
157
176
  # configured host; hosted providers curated).
@@ -24,6 +24,7 @@ module ActionAgent
24
24
  "mcp_client" => "MCPClient",
25
25
  "mcp_tool_dispatcher" => "MCPToolDispatcher",
26
26
  "mcp_controller" => "MCPController",
27
+ "mcp_dashboard_tools" => "MCPDashboardTools",
27
28
  "mcp_recording_middleware" => "MCPRecordingMiddleware",
28
29
  "mcp_servers_controller" => "MCPServersController",
29
30
  "playwright_mcp_client" => "PlaywrightMCPClient"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.7.2"
4
+ VERSION = "1.8.1"
5
5
  end