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.
- 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 +168 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +74 -131
- 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 +105 -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 +509 -0
- data/app/controllers/concerns/action_agent/api/run_sandbox.rb +65 -0
- data/app/jobs/action_agent/code_session_job.rb +171 -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 +49 -6
- data/app/models/action_agent/agent_run.rb +20 -1
- data/app/models/action_agent/code_session.rb +145 -0
- data/app/models/action_agent/evaluation.rb +64 -0
- data/app/models/action_agent/evaluation_run.rb +166 -54
- data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
- data/app/models/action_agent/github_connection.rb +75 -0
- data/app/models/action_agent/model_pricing.rb +214 -34
- data/app/models/action_agent/provider_key.rb +148 -10
- data/app/models/action_agent/sandbox_session.rb +194 -17
- data/app/serializers/action_agent/evaluation_serializer.rb +149 -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_scorecard.rb +50 -21
- 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/codex_session_events.rb +37 -0
- data/app/services/action_agent/dashboard_assistant_service.rb +47 -5
- data/app/services/action_agent/evaluation_report_import.rb +84 -5
- data/app/services/action_agent/evaluation_run_cost.rb +609 -0
- data/app/services/action_agent/evaluation_runner_service.rb +25 -5
- data/app/services/action_agent/evaluation_standing.rb +145 -0
- 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 +1715 -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 +82 -14
- data/app/services/action_agent/scenario_evaluation_runner.rb +52 -5
- data/app/services/action_agent/secret_scrubber.rb +37 -0
- data/app/services/action_agent/tool_discovery.rb +19 -5
- data/config/routes.rb +23 -4
- data/lib/action_agent/engine.rb +1 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +153 -3
- data/lib/generators/action_agent/install_generator.rb +35 -5
- data/lib/generators/action_agent/templates/action_agent.rb.erb +47 -0
- data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -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 +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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
#
|
|
15
|
-
# Default:
|
|
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
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
75
|
-
#
|
|
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).
|
data/lib/action_agent/engine.rb
CHANGED
|
@@ -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"
|
data/lib/action_agent/version.rb
CHANGED