actionagent 1.8.1 → 1.9.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 +76 -73
- data/app/assets/builds/action_agent_recorder.js +78 -0
- data/app/assets/builds/action_agent_replay.js +226 -0
- data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
- data/app/controllers/action_agent/api/agents_controller.rb +1 -0
- data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
- data/app/controllers/action_agent/api/base_controller.rb +63 -2
- data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
- data/app/controllers/action_agent/api/code_sessions_controller.rb +53 -6
- data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
- data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
- data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +9 -4
- data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
- data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
- data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
- data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
- data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
- data/app/controllers/action_agent/api/members_controller.rb +36 -0
- data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
- data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
- data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
- data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
- data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
- data/app/controllers/action_agent/api/projects_controller.rb +512 -0
- data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
- data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
- data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
- data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
- data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
- data/app/controllers/action_agent/api/sandboxes_controller.rb +140 -8
- data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
- data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
- data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
- data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
- data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
- data/app/controllers/action_agent/dashboard_controller.rb +17 -6
- data/app/controllers/action_agent/session_player_controller.rb +58 -0
- data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +623 -18
- data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
- data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
- data/app/jobs/action_agent/agent_execution_job.rb +9 -39
- data/app/jobs/action_agent/agent_resume_job.rb +84 -0
- data/app/jobs/action_agent/code_session_job.rb +26 -2
- data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
- data/app/jobs/action_agent/exploration_job.rb +80 -0
- data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
- data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
- data/app/jobs/action_agent/project_setup_job.rb +21 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
- data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
- data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
- data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
- data/app/models/action_agent/agent.rb +39 -26
- data/app/models/action_agent/agent_run.rb +134 -23
- data/app/models/action_agent/agent_template.rb +35 -1
- data/app/models/action_agent/api_key.rb +14 -0
- data/app/models/action_agent/catalog_scenario.rb +75 -0
- data/app/models/action_agent/code_session.rb +16 -4
- data/app/models/action_agent/draft_pull_request.rb +131 -0
- data/app/models/action_agent/evaluation.rb +84 -0
- data/app/models/action_agent/exploration.rb +847 -0
- data/app/models/action_agent/github_installation.rb +159 -0
- data/app/models/action_agent/input_request.rb +291 -0
- data/app/models/action_agent/project.rb +789 -0
- data/app/models/action_agent/project_secret.rb +339 -0
- data/app/models/action_agent/provider_key.rb +200 -3
- data/app/models/action_agent/recording_event.rb +145 -0
- data/app/models/action_agent/recording_snapshot.rb +7 -7
- data/app/models/action_agent/sandbox_session.rb +297 -14
- data/app/models/action_agent/scenario_catalog.rb +172 -0
- data/app/models/action_agent/scenario_product.rb +57 -0
- data/app/models/action_agent/scenario_set.rb +138 -0
- data/app/models/action_agent/session_recording.rb +121 -14
- data/app/models/action_agent/telemetry_trace.rb +74 -19
- data/app/models/concerns/action_agent/ownable.rb +84 -13
- data/app/serializers/action_agent/input_request_serializer.rb +47 -0
- data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
- data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
- data/app/services/action_agent/agent_execution_service.rb +450 -72
- data/app/services/action_agent/agent_sync.rb +5 -2
- data/app/services/action_agent/agent_tool_roster.rb +19 -4
- data/app/services/action_agent/agent_toolbox.rb +215 -9
- data/app/services/action_agent/browser_live_ticket.rb +78 -0
- data/app/services/action_agent/browser_sidecar.rb +213 -0
- data/app/services/action_agent/browser_sign_in.rb +163 -0
- data/app/services/action_agent/browser_tool_redaction.rb +92 -0
- data/app/services/action_agent/claude_code_auth.rb +70 -4
- data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
- data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
- data/app/services/action_agent/evaluation_fix.rb +101 -0
- data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
- data/app/services/action_agent/evaluation_runner_service.rb +4 -5
- data/app/services/action_agent/evaluation_standing.rb +15 -8
- data/app/services/action_agent/explorer_execution_service.rb +517 -0
- data/app/services/action_agent/github_client.rb +405 -64
- data/app/services/action_agent/live_updates.rb +31 -0
- data/app/services/action_agent/local_sandbox_backend.rb +1218 -83
- data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
- data/app/services/action_agent/mcp_client.rb +6 -2
- data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
- data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
- data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
- data/app/services/action_agent/playwright_mcp_client.rb +7 -0
- data/app/services/action_agent/project_boot_fix.rb +309 -0
- data/app/services/action_agent/project_capabilities.rb +200 -0
- data/app/services/action_agent/project_install_pull_request.rb +225 -0
- data/app/services/action_agent/project_preflight.rb +178 -0
- data/app/services/action_agent/project_secret_discovery.rb +142 -0
- data/app/services/action_agent/project_setup.rb +429 -0
- data/app/services/action_agent/provider_credentials.rb +102 -0
- data/app/services/action_agent/recording_event_ingest.rb +181 -0
- data/app/services/action_agent/runtime_tool_roster.rb +52 -0
- data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
- data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
- data/app/services/action_agent/sandbox_browser.rb +299 -0
- data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
- data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
- data/app/services/action_agent/sandbox_mail.rb +103 -0
- data/app/services/action_agent/sandbox_manifest.rb +166 -13
- data/app/services/action_agent/sandbox_orchestrator.rb +276 -22
- data/app/services/action_agent/sandbox_patch.rb +253 -0
- data/app/services/action_agent/scenario_catalog_import.rb +255 -0
- data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
- data/app/services/action_agent/scenario_evaluation_runner.rb +119 -16
- data/app/services/action_agent/secret_scrubber.rb +33 -4
- data/app/services/action_agent/session_index.rb +270 -0
- data/app/services/action_agent/session_recording_service.rb +11 -0
- data/app/services/action_agent/session_timeline.rb +505 -0
- data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
- data/app/views/action_agent/session_player/show.html.erb +19 -0
- data/config/routes.rb +152 -1
- data/lib/action_agent/claude_login_process.rb +107 -0
- data/lib/action_agent/engine.rb +55 -5
- data/lib/action_agent/secret_requests.rb +80 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +507 -22
- data/lib/generators/action_agent/install_generator.rb +37 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -0
- data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
- data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
- data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
- data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
- data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
- data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
- data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
- data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
- data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
- data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
- data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
- data/lib/tasks/action_agent.rake +33 -0
- data/lib/tasks/browser.rake +22 -0
- data/lib/tasks/sandbox.rake +1 -1
- data/lib/tasks/scenario_catalogs.rake +71 -0
- metadata +96 -2
|
@@ -53,9 +53,12 @@ module ActionAgent
|
|
|
53
53
|
# raised
|
|
54
54
|
# - judge_provider_error: true when that lookup raised, as it does
|
|
55
55
|
# when a stored key no longer decrypts
|
|
56
|
-
# - model_providers: the providers
|
|
57
|
-
# for, leaving out any whose
|
|
58
|
-
# cannot be read
|
|
56
|
+
# - model_providers: the providers scenario replays have
|
|
57
|
+
# credentials for, leaving out any whose
|
|
58
|
+
# credentials cannot be read. Resolved for
|
|
59
|
+
# the caller replays run as, never the
|
|
60
|
+
# signed-in member, whose personal keys no
|
|
61
|
+
# replay uses.
|
|
59
62
|
def index
|
|
60
63
|
scope = evaluations_scope
|
|
61
64
|
scope = scope.where(agent_id: params[:agent_id]) if params[:agent_id].present?
|
|
@@ -69,7 +72,9 @@ module ActionAgent
|
|
|
69
72
|
evaluations: evaluations.map { |evaluation| serialize(evaluation) },
|
|
70
73
|
archived_count: archived_count,
|
|
71
74
|
**judge_provider_fields(owner),
|
|
72
|
-
model_providers: AgentExecutionService.available_providers(
|
|
75
|
+
model_providers: AgentExecutionService.available_providers(
|
|
76
|
+
owner, actor: ScenarioEvaluationRunner.replay_actor_for(owner)
|
|
77
|
+
)
|
|
73
78
|
}
|
|
74
79
|
end
|
|
75
80
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# Explorations and the review of their candidate scenarios: listing them,
|
|
6
|
+
# reading one with its candidates, submitting candidates found outside
|
|
7
|
+
# the dashboard, editing, rejecting and accepting candidates, and
|
|
8
|
+
# stopping a walk to review what it found. See Exploration.
|
|
9
|
+
#
|
|
10
|
+
# Everything is scoped to the caller's owner, and another owner's
|
|
11
|
+
# exploration reads as 404. Accepting writes into an evaluation, so it
|
|
12
|
+
# needs :replace_scenarios; storing and editing candidates changes only
|
|
13
|
+
# the exploration.
|
|
14
|
+
class ExplorationsController < BaseController
|
|
15
|
+
include EvaluationRunStarting
|
|
16
|
+
|
|
17
|
+
LIST_LIMIT = 50
|
|
18
|
+
|
|
19
|
+
before_action :require_owner!
|
|
20
|
+
before_action :set_exploration, except: [ :index, :create ]
|
|
21
|
+
|
|
22
|
+
rescue_from Exploration::InvalidCandidate, Exploration::CandidateLimitExceeded, with: :invalid_candidate
|
|
23
|
+
rescue_from Exploration::AcceptRefused, with: :accept_refused
|
|
24
|
+
rescue_from Evaluation::ScenarioLimitExceeded, with: :scenario_limit
|
|
25
|
+
|
|
26
|
+
# GET /api/explorations?project_id=&evaluation_id=
|
|
27
|
+
def index
|
|
28
|
+
scope = owned(Exploration).recent
|
|
29
|
+
scope = scope.where(project_id: params[:project_id].to_s) if params[:project_id].present?
|
|
30
|
+
scope = scope.where(evaluation_id: params[:evaluation_id].to_s) if params[:evaluation_id].present?
|
|
31
|
+
|
|
32
|
+
render json: { explorations: scope.limit(LIST_LIMIT).map(&:summary) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# GET /api/explorations/:id
|
|
36
|
+
def show
|
|
37
|
+
render json: detail(@exploration)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# POST /api/explorations { project_id: | evaluation_id:, candidates: [...] }
|
|
41
|
+
# Stores candidates an agent outside the dashboard found, as a new
|
|
42
|
+
# exploration with source "external" that is ready for review. An
|
|
43
|
+
# observed agent's evaluation is refused, as the MCP facade's
|
|
44
|
+
# `explorations_submit` refuses it, unless a host adapter replays it.
|
|
45
|
+
def create
|
|
46
|
+
project, evaluation = submission_target
|
|
47
|
+
return if performed?
|
|
48
|
+
|
|
49
|
+
exploration = Exploration.build_for(project: project, evaluation: evaluation, user: current_user,
|
|
50
|
+
account: current_account, source: "external", status: "review", started_at: Time.current,
|
|
51
|
+
finished_at: Time.current)
|
|
52
|
+
exploration.save_with_candidates!(candidate_list)
|
|
53
|
+
|
|
54
|
+
render json: detail(exploration), status: :created
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# PATCH /api/explorations/:id/candidates/:candidate_id
|
|
58
|
+
# { prompt:, group:, notes: (or rubric:), tools:, contains:, not_contains:, state: }
|
|
59
|
+
# An edit, a rejection (state: "rejected"), or reconsidering a
|
|
60
|
+
# rejected candidate (state: "proposed"); see
|
|
61
|
+
# Exploration#update_candidate!.
|
|
62
|
+
def update_candidate
|
|
63
|
+
candidate = @exploration.update_candidate!(params[:candidate_id], candidate_attributes(params))
|
|
64
|
+
|
|
65
|
+
render json: { candidate: candidate, exploration: @exploration.summary }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# POST /api/explorations/:id/accept { candidate_ids: [...], edits: { id => {...} } }
|
|
69
|
+
# Merges the candidates into the target evaluation (see
|
|
70
|
+
# Exploration#accept!). A refused candidate answers 422 with
|
|
71
|
+
# `problems` keyed by candidate id, and nothing is written.
|
|
72
|
+
#
|
|
73
|
+
# :replace_scenarios is asked about the evaluation the candidates
|
|
74
|
+
# merge into, or about the exploration when a project's accept will
|
|
75
|
+
# create that evaluation.
|
|
76
|
+
#
|
|
77
|
+
# With scenario_set_id, the candidates become scenarios of that set of
|
|
78
|
+
# one of the owner's scenario catalogs instead (see
|
|
79
|
+
# Exploration#accept_into_set!), and :replace_scenarios is asked about
|
|
80
|
+
# its catalog.
|
|
81
|
+
def accept
|
|
82
|
+
return accept_into_set if params[:scenario_set_id].present?
|
|
83
|
+
return unless authorize_action!(:replace_scenarios, @exploration.target_evaluation || @exploration)
|
|
84
|
+
|
|
85
|
+
merged = @exploration.accept!(params[:candidate_ids], edits: accept_edits)
|
|
86
|
+
evaluation = merged[:evaluation]
|
|
87
|
+
|
|
88
|
+
render json: detail(@exploration.reload).merge(
|
|
89
|
+
accepted: merged.slice(:added, :updated, :unchanged),
|
|
90
|
+
evaluation: evaluation_summary(evaluation)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# POST /api/explorations/:id/stop
|
|
95
|
+
# Ends a pending or running exploration and keeps what it found for
|
|
96
|
+
# review. 409 once it has stopped.
|
|
97
|
+
def stop
|
|
98
|
+
unless @exploration.stop!(reason: "stopped")
|
|
99
|
+
return render json: { error: "This exploration is not running", status: @exploration.status }, status: :conflict
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
render json: detail(@exploration.reload)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def accept_into_set
|
|
108
|
+
products = ScenarioProduct.where(scenario_catalog_id: owned(ScenarioCatalog).select(:id))
|
|
109
|
+
set = ScenarioSet.where(scenario_product_id: products.select(:id)).find(params[:scenario_set_id].to_s)
|
|
110
|
+
return unless authorize_action!(:replace_scenarios, set.catalog)
|
|
111
|
+
|
|
112
|
+
merged = @exploration.accept_into_set!(params[:candidate_ids], set: set, edits: accept_edits)
|
|
113
|
+
render json: detail(@exploration.reload).merge(
|
|
114
|
+
accepted: merged.slice(:added, :updated, :unchanged),
|
|
115
|
+
scenario_set: scenario_set_summary(set)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def scenario_set_summary(set)
|
|
120
|
+
catalog = set.catalog
|
|
121
|
+
{ id: set.id, key: set.key, name: set.name, product_key: set.product.key,
|
|
122
|
+
catalog: { id: catalog.id, key: catalog.key, name: catalog.name } }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def set_exploration
|
|
126
|
+
@exploration = owned(Exploration).find(params[:id])
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# [project, evaluation] the submission names, one of them nil, or nil
|
|
130
|
+
# after rendering why there is none.
|
|
131
|
+
def submission_target
|
|
132
|
+
if params[:project_id].present?
|
|
133
|
+
[ owned(Project).find(params[:project_id].to_s), nil ]
|
|
134
|
+
elsif params[:evaluation_id].present?
|
|
135
|
+
evaluation = Evaluation.joins(:agent).where(agent: owner_agents).find(params[:evaluation_id].to_s)
|
|
136
|
+
if unexecutable_scenario_run?(evaluation)
|
|
137
|
+
render json: { error: "Observed agents are read-only — duplicate this agent to create an executable copy" },
|
|
138
|
+
status: :unprocessable_entity
|
|
139
|
+
return nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
[ nil, evaluation ]
|
|
143
|
+
else
|
|
144
|
+
render json: { error: "Name the project_id or evaluation_id the candidates are for" }, status: :bad_request
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def candidate_list
|
|
150
|
+
list = params[:candidates]
|
|
151
|
+
list.is_a?(Array) ? list.map { |entry| entry.respond_to?(:to_unsafe_h) ? entry.to_unsafe_h : entry } : list
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The fields of a candidate edit present in +source+. Nested values
|
|
155
|
+
# arrive as Parameters and are read as plain data by the model.
|
|
156
|
+
def candidate_attributes(source)
|
|
157
|
+
keys = %w[prompt group notes rubric tools contains not_contains expectations state]
|
|
158
|
+
source = source.to_unsafe_h if source.respond_to?(:to_unsafe_h)
|
|
159
|
+
source.is_a?(Hash) ? source.stringify_keys.slice(*keys) : {}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def accept_edits
|
|
163
|
+
edits = params[:edits]
|
|
164
|
+
edits = edits.to_unsafe_h if edits.respond_to?(:to_unsafe_h)
|
|
165
|
+
return {} unless edits.is_a?(Hash)
|
|
166
|
+
|
|
167
|
+
edits.transform_values { |edit| candidate_attributes(edit).except("state") }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The exploration with its candidates and what its review shows beside
|
|
171
|
+
# them: the evaluation they merge into, how many candidates to
|
|
172
|
+
# pre-select, and the owner's run allowance.
|
|
173
|
+
def detail(exploration)
|
|
174
|
+
evaluation = exploration.target_evaluation
|
|
175
|
+
usage = ActionAgent.usage_for(current_owner)
|
|
176
|
+
{
|
|
177
|
+
exploration: exploration.summary,
|
|
178
|
+
candidates: exploration.candidates,
|
|
179
|
+
project: exploration.project && { id: exploration.project.id, name: exploration.project.name },
|
|
180
|
+
target_agent: exploration.target_agent && { id: exploration.target_agent.id, name: exploration.target_agent.name },
|
|
181
|
+
evaluation: evaluation_summary(evaluation),
|
|
182
|
+
preselect_limit: ActionAgent.exploration_preselect_limit_for(current_owner),
|
|
183
|
+
runs_remaining: usage[:runs_remaining] || usage["runs_remaining"]
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# What the review needs to estimate a run of the whole suite: the
|
|
188
|
+
# enabled scenarios, and the models each replays under.
|
|
189
|
+
def evaluation_summary(evaluation)
|
|
190
|
+
return nil if evaluation.nil?
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
id: evaluation.id,
|
|
194
|
+
name: evaluation.name,
|
|
195
|
+
enabled_scenario_count: evaluation.scenarios.enabled.count,
|
|
196
|
+
model_count: [ evaluation.compare_models.size, 1 ].max
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def invalid_candidate(exception)
|
|
201
|
+
render json: { error: exception.message }, status: :unprocessable_entity
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def accept_refused(exception)
|
|
205
|
+
render json: { error: exception.message, problems: exception.problems }, status: :unprocessable_entity
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def scenario_limit(exception)
|
|
209
|
+
render json: { error: exception.message }, status: :unprocessable_entity
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# Creating this dashboard's GitHub App from a manifest (Settings ->
|
|
6
|
+
# Integrations -> Create GitHub App), so a self-hoster need not register
|
|
7
|
+
# one by hand. Offered only when multi_tenant is off: a multi-tenant
|
|
8
|
+
# platform registers its App once, per environment, outside the
|
|
9
|
+
# dashboard.
|
|
10
|
+
#
|
|
11
|
+
# The dashboard posts the manifest #create returns to GitHub, which
|
|
12
|
+
# creates the App and returns to #callback with a code. #callback
|
|
13
|
+
# converts it and shows the App's settings and credentials on a page of
|
|
14
|
+
# their own, once. Nothing here stores them: the operator adds them to
|
|
15
|
+
# the dashboard's configuration (see ActionAgent.github_app_id).
|
|
16
|
+
class GithubAppManifestsController < BaseController
|
|
17
|
+
STATE_SESSION_KEY = :action_agent_github_app_manifest_state
|
|
18
|
+
# The permissions the App requests. Contents and pull requests are
|
|
19
|
+
# written only by an explicit publish; the organization members read
|
|
20
|
+
# is what an installation's admin check needs. No workflows,
|
|
21
|
+
# administration or secrets permission.
|
|
22
|
+
PERMISSIONS = { contents: "write", pull_requests: "write", metadata: "read", members: "read" }.freeze
|
|
23
|
+
# GitHub's limit on an App's name.
|
|
24
|
+
MAX_NAME_LENGTH = 34
|
|
25
|
+
|
|
26
|
+
before_action :require_owner!
|
|
27
|
+
before_action :require_single_tenant!
|
|
28
|
+
before_action :authorize_github!
|
|
29
|
+
|
|
30
|
+
# POST /api/github_app_manifest { organization: "acme" (optional) }
|
|
31
|
+
#
|
|
32
|
+
# Returns the GitHub URL to post the manifest to, with a single-use
|
|
33
|
+
# state, and the manifest itself, which the dashboard submits as the
|
|
34
|
+
# form field +manifest+.
|
|
35
|
+
def create
|
|
36
|
+
organization = params[:organization].presence
|
|
37
|
+
unless organization.nil? || (organization.is_a?(String) && organization.match?(GithubClient::LOGIN))
|
|
38
|
+
return render json: { error: "organization must be a GitHub organization login" }, status: :bad_request
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
state = SecureRandom.urlsafe_base64(32)
|
|
42
|
+
session[STATE_SESSION_KEY] = { "state" => state, "user_id" => current_user_id }.compact
|
|
43
|
+
|
|
44
|
+
render json: { url: GithubClient.new_app_url(state: state, organization: organization), manifest: manifest }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# GET /api/github_app_manifest/callback?code=...&state=...
|
|
48
|
+
def callback
|
|
49
|
+
# Single use: read and cleared before anything else can fail.
|
|
50
|
+
issued = session.delete(STATE_SESSION_KEY)
|
|
51
|
+
return redirect_to_settings(github_app: "invalid_state") unless issued_here?(issued, params[:state])
|
|
52
|
+
return redirect_to_settings(github_app: "missing_code") unless params[:code].is_a?(String) && params[:code].present?
|
|
53
|
+
|
|
54
|
+
@app = GithubClient.convert_manifest(params[:code])
|
|
55
|
+
@settings_url = "#{request.script_name}/settings?tab=integrations"
|
|
56
|
+
|
|
57
|
+
# The page carries the App's secrets: no cache keeps it, no other
|
|
58
|
+
# origin's script runs on it, and its links send no referrer.
|
|
59
|
+
response.headers["Cache-Control"] = "no-store"
|
|
60
|
+
response.headers["Referrer-Policy"] = "no-referrer"
|
|
61
|
+
response.headers["Content-Security-Policy"] =
|
|
62
|
+
"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
63
|
+
render template: "action_agent/github_app_manifests/created", layout: false
|
|
64
|
+
rescue GithubClient::Error => e
|
|
65
|
+
Rails.logger.warn("[ActionAgent] GitHub App manifest conversion failed: #{e.message}")
|
|
66
|
+
redirect_to_settings(github_app: "manifest_error")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def manifest
|
|
72
|
+
base = "#{request.base_url}#{request.script_name}"
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
name: "ActiveAgent #{request.host}".first(MAX_NAME_LENGTH),
|
|
76
|
+
url: base.presence || request.base_url,
|
|
77
|
+
redirect_url: "#{base}/api/github_app_manifest/callback",
|
|
78
|
+
callback_urls: [ "#{base}/api/github_installations/callback" ],
|
|
79
|
+
request_oauth_on_install: true,
|
|
80
|
+
# Asks GitHub to return after an existing installation is
|
|
81
|
+
# reconfigured too, which is how one is linked again.
|
|
82
|
+
setup_on_update: true,
|
|
83
|
+
public: false,
|
|
84
|
+
default_permissions: PERMISSIONS,
|
|
85
|
+
default_events: []
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def issued_here?(issued, given)
|
|
90
|
+
issued.is_a?(Hash) && issued["state"].is_a?(String) && given.is_a?(String) &&
|
|
91
|
+
ActiveSupport::SecurityUtils.secure_compare(issued["state"], given) &&
|
|
92
|
+
issued["user_id"] == current_user_id
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def current_user_id
|
|
96
|
+
current_user.respond_to?(:id) ? current_user.id.to_s : nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def require_single_tenant!
|
|
100
|
+
return unless ActionAgent.multi_tenant?
|
|
101
|
+
return redirect_to_settings(github_app: "manifest_unavailable") if action_name == "callback"
|
|
102
|
+
|
|
103
|
+
render json: { error: "Creating a GitHub App is not available on a multi-tenant dashboard", code: "manifest_unavailable" },
|
|
104
|
+
status: :not_found
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def authorize_github!
|
|
108
|
+
authorize_action!(:manage_github, owned(GithubInstallation).new)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The callback is a browser navigation, so a refusal returns to
|
|
112
|
+
# Settings, discarding the state.
|
|
113
|
+
def permission_denied(action)
|
|
114
|
+
return super unless action_name == "callback"
|
|
115
|
+
|
|
116
|
+
session.delete(STATE_SESSION_KEY)
|
|
117
|
+
redirect_to_settings(github_app: "forbidden")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def redirect_to_settings(**query)
|
|
121
|
+
redirect_to "#{request.script_name}/settings?#{{ tab: 'integrations' }.merge(query).to_query}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -15,19 +15,23 @@ module ActionAgent
|
|
|
15
15
|
before_action :require_owner!
|
|
16
16
|
before_action :require_github_oauth!, only: [ :connect, :callback ]
|
|
17
17
|
before_action :set_connection, only: [ :repositories, :update, :destroy ]
|
|
18
|
+
before_action :authorize_github!, only: [ :connect, :callback, :update, :destroy ]
|
|
18
19
|
|
|
19
20
|
# Later handlers win, so the subclass is registered last.
|
|
20
21
|
rescue_from GithubClient::Error, with: :github_unavailable
|
|
21
22
|
rescue_from GithubClient::Unauthorized, with: :github_unauthorized
|
|
22
23
|
|
|
23
|
-
# GET /api/github_connection
|
|
24
|
+
# GET /api/github_connection — the OAuth connection, and the GitHub App
|
|
25
|
+
# path beside it under +app+: whether an App is configured, whether one
|
|
26
|
+
# can be created from a manifest here, and the owner's installations.
|
|
24
27
|
def show
|
|
25
28
|
connection = owned(GithubConnection).first
|
|
26
29
|
|
|
27
30
|
render json: {
|
|
28
31
|
configured: ActionAgent.github_oauth_configured?,
|
|
29
32
|
connected: connection.present?,
|
|
30
|
-
connection: connection&.as_summary
|
|
33
|
+
connection: connection&.as_summary,
|
|
34
|
+
app: github_app_status
|
|
31
35
|
}
|
|
32
36
|
end
|
|
33
37
|
|
|
@@ -100,6 +104,9 @@ module ActionAgent
|
|
|
100
104
|
# A different GitHub account starts with nothing selected: the old
|
|
101
105
|
# selection was checked against the other account's access.
|
|
102
106
|
connection.repositories = [] if connection.persisted? && connection.github_user_id != user["id"]
|
|
107
|
+
# The user who authorized, also where the account owns the
|
|
108
|
+
# connection: only they may publish pull requests with its token.
|
|
109
|
+
connection.user_id = current_user.id if ActionAgent.user_class.present? && current_user.respond_to?(:id)
|
|
103
110
|
connection.update!(
|
|
104
111
|
access_token: grant[:access_token],
|
|
105
112
|
scopes: grant[:scope],
|
|
@@ -120,6 +127,23 @@ module ActionAgent
|
|
|
120
127
|
@connection = owned(GithubConnection).first!
|
|
121
128
|
end
|
|
122
129
|
|
|
130
|
+
# Asks about the owner's connection, or about an unsaved one when
|
|
131
|
+
# #connect or #callback is about to create it.
|
|
132
|
+
def authorize_github!
|
|
133
|
+
subject = @connection || owned(GithubConnection).first || owned(GithubConnection).new
|
|
134
|
+
authorize_action!(:manage_github, subject)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# #connect and #callback are browser navigations, so a refusal returns
|
|
138
|
+
# to Settings like their other outcomes. The callback's state is
|
|
139
|
+
# discarded with it.
|
|
140
|
+
def permission_denied(action)
|
|
141
|
+
return super unless action_name.in?(%w[connect callback])
|
|
142
|
+
|
|
143
|
+
session.delete(STATE_SESSION_KEY)
|
|
144
|
+
redirect_to_settings(github: "forbidden")
|
|
145
|
+
end
|
|
146
|
+
|
|
123
147
|
def require_github_oauth!
|
|
124
148
|
return if ActionAgent.github_oauth_configured?
|
|
125
149
|
|
|
@@ -130,6 +154,20 @@ module ActionAgent
|
|
|
130
154
|
"#{request.base_url}#{request.script_name}/api/github_connection/callback"
|
|
131
155
|
end
|
|
132
156
|
|
|
157
|
+
def github_app_status
|
|
158
|
+
configured = ActionAgent.github_app_configured?
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
configured: configured,
|
|
162
|
+
slug: configured ? ActionAgent.github_app_slug : nil,
|
|
163
|
+
callback_url: "#{request.base_url}#{request.script_name}/api/github_installations/callback",
|
|
164
|
+
manifest_available: !ActionAgent.multi_tenant?,
|
|
165
|
+
# Listed only while an App is configured: without one nothing can
|
|
166
|
+
# mint an installation's tokens, so its repositories cannot be used.
|
|
167
|
+
installations: configured ? owned(GithubInstallation).order(:id).map(&:as_summary) : []
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
133
171
|
def redirect_to_settings(**query)
|
|
134
172
|
redirect_to "#{request.script_name}/settings?#{{ tab: 'integrations' }.merge(query).to_query}"
|
|
135
173
|
end
|