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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# The project's install pull request (see ProjectInstallPullRequest): its
|
|
6
|
+
# status, the preview of exactly what it would publish, publishing it and
|
|
7
|
+
# updating its branch later, and the patch to download where publishing
|
|
8
|
+
# is not available.
|
|
9
|
+
#
|
|
10
|
+
# Everything is read from the project's live sandbox. A request made
|
|
11
|
+
# while none is live boots one (Project#ensure_sandbox!) and answers 202,
|
|
12
|
+
# to be asked again once it serves. Publishing is an explicit request
|
|
13
|
+
# that asks ActionAgent.permission_checker about :publish_pull_request,
|
|
14
|
+
# as DraftPullRequestJob asks again before it writes.
|
|
15
|
+
class ProjectInstallPullRequestsController < BaseController
|
|
16
|
+
before_action :require_owner!
|
|
17
|
+
before_action :set_project
|
|
18
|
+
|
|
19
|
+
rescue_from DraftPullRequestPublisher::Refused, with: :refused
|
|
20
|
+
rescue_from Project::ConfirmationRequired, with: :confirmation_required
|
|
21
|
+
rescue_from Project::BootRefused, SandboxOrchestrator::UnsupportedBackendError, with: :boot_refused
|
|
22
|
+
|
|
23
|
+
# GET /api/projects/:project_id/install_pull_request
|
|
24
|
+
# The install pull request, its state read again from GitHub at most
|
|
25
|
+
# once a minute. Once GitHub reports it merged, the project counts as
|
|
26
|
+
# installed (Project#install_merged!).
|
|
27
|
+
def show
|
|
28
|
+
record = @project.install_pull_request
|
|
29
|
+
if record
|
|
30
|
+
DraftPullRequest.where(id: record.id).fail_stalled!
|
|
31
|
+
record.reload
|
|
32
|
+
DraftPullRequestPublisher.refresh_status!(record) unless record.in_progress?
|
|
33
|
+
@project.install_merged! if record.state == "merged"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
render json: { pull_request: record&.summary, publishing: publishing_status, allowlist: allowlist_json,
|
|
37
|
+
project: @project.reload.summary }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# POST /api/projects/:project_id/install_pull_request/preview
|
|
41
|
+
# Every file the sandbox changed or the project generates, each with why
|
|
42
|
+
# it is refused or with its exact diff.
|
|
43
|
+
def preview
|
|
44
|
+
return unless live_sandbox!
|
|
45
|
+
|
|
46
|
+
preview = install.publisher(@sandbox).preview(allowlist: install.allowlist)
|
|
47
|
+
preview[:suggested_branch] = ProjectInstallPullRequest::DEFAULT_BRANCH
|
|
48
|
+
render json: { preview: preview, publishing: publishing_status }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# POST /api/projects/:project_id/install_pull_request
|
|
52
|
+
#
|
|
53
|
+
# { title:, body:, branch:, files: [{ path:, digest: }] }
|
|
54
|
+
# opens the install pull request as a draft, with the files
|
|
55
|
+
# { update: true, files:, message: }
|
|
56
|
+
# publishes the files as a new commit on its branch, from the
|
|
57
|
+
# project's sandbox now, which may be a later one than the
|
|
58
|
+
# sandbox it was opened from
|
|
59
|
+
# { open: true } or { regular: true }
|
|
60
|
+
# opens a draft, or a regular, pull request for its branch when
|
|
61
|
+
# the branch was published without one
|
|
62
|
+
#
|
|
63
|
+
# Answers 202 with the queued record.
|
|
64
|
+
def create
|
|
65
|
+
return unless live_sandbox!
|
|
66
|
+
return open_branch(regular: boolean_param(:regular)) if boolean_param(:open) || boolean_param(:regular)
|
|
67
|
+
|
|
68
|
+
on_branch = false
|
|
69
|
+
if boolean_param(:update)
|
|
70
|
+
record = update_record or return
|
|
71
|
+
on_branch = record.new_record? && @sandbox.repository_ref == record.branch
|
|
72
|
+
else
|
|
73
|
+
return unless openable!
|
|
74
|
+
|
|
75
|
+
record = @sandbox.draft_pull_requests.new(
|
|
76
|
+
operation: "create", repository: @sandbox.repository, branch: string_param(:branch).to_s.strip.presence ||
|
|
77
|
+
ProjectInstallPullRequest::DEFAULT_BRANCH,
|
|
78
|
+
title: string_param(:title).to_s.strip.presence || "Install ActiveAgent", body: string_param(:body)
|
|
79
|
+
)
|
|
80
|
+
refusal = DraftPullRequestPublisher.branch_refusal(record.branch)
|
|
81
|
+
raise DraftPullRequestPublisher::Refused.new(refusal, code: "invalid_branch") if refusal
|
|
82
|
+
end
|
|
83
|
+
assign_owner(record)
|
|
84
|
+
return unless authorize_action!(:publish_pull_request, record)
|
|
85
|
+
|
|
86
|
+
queue(record, on_branch: on_branch)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# GET /api/projects/:project_id/install_pull_request/patch?paths[]=…
|
|
90
|
+
# The files named, or every publishable one, each within the project's
|
|
91
|
+
# allowlist.
|
|
92
|
+
def patch
|
|
93
|
+
return unless live_sandbox!
|
|
94
|
+
|
|
95
|
+
paths = params[:paths]
|
|
96
|
+
paths = nil unless paths.is_a?(Array) && paths.all? { |path| path.is_a?(String) }
|
|
97
|
+
paths ||= install.publisher(@sandbox).changes(allowlist: install.allowlist).publishable.map(&:path)
|
|
98
|
+
data = install.publisher(@sandbox).patch(paths: paths, title: string_param(:title) || "Install ActiveAgent", allowlist: install.allowlist)
|
|
99
|
+
send_data data, type: "text/x-diff", disposition: "attachment", filename: "#{@project.repository.tr('/', '-')}-install.patch"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def set_project
|
|
105
|
+
@project = owned(Project).find(params[:project_id])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def install
|
|
109
|
+
@install ||= ProjectInstallPullRequest.new(@project, user: current_user)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The project's sandbox when it serves; otherwise boots one, renders
|
|
113
|
+
# 202 and answers false.
|
|
114
|
+
def live_sandbox!
|
|
115
|
+
sandbox = @project.current_sandbox_session
|
|
116
|
+
if sandbox && (sandbox.ready? || sandbox.running?) && sandbox.active?
|
|
117
|
+
@sandbox = sandbox
|
|
118
|
+
return true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
unless ActionAgent.execution_enabled?
|
|
122
|
+
render json: { error: "The project's sandbox is not running, and agent execution is disabled on this dashboard" },
|
|
123
|
+
status: :forbidden
|
|
124
|
+
return false
|
|
125
|
+
end
|
|
126
|
+
enforce_execution_quota!
|
|
127
|
+
return false if performed?
|
|
128
|
+
|
|
129
|
+
before = @project.current_sandbox_session_id
|
|
130
|
+
booting = @project.ensure_sandbox!(confirm: ActiveModel::Type::Boolean.new.cast(params[:confirm]) == true, confirmed_by: current_user)
|
|
131
|
+
record_execution_usage if booting.id != before
|
|
132
|
+
render json: { booting: true, code: "sandbox_booting", project: @project.reload.summary,
|
|
133
|
+
error: "The project's sandbox is booting. Publishing reads its checkout, so ask again once it is ready" },
|
|
134
|
+
status: :accepted
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Whether a new install pull request may be opened: not while one is
|
|
139
|
+
# open, or its branch is published without one.
|
|
140
|
+
def openable!
|
|
141
|
+
current = @project.install_pull_request
|
|
142
|
+
return true if current.nil? || (current.head_commit.blank? && !current.in_progress?) || current.state.in?(%w[closed merged])
|
|
143
|
+
|
|
144
|
+
error = if current.branch_only?
|
|
145
|
+
"The install branch is published without a pull request. Open a pull request for it instead"
|
|
146
|
+
else
|
|
147
|
+
"The project has an install pull request already. Update it instead"
|
|
148
|
+
end
|
|
149
|
+
render json: { error: error, code: "already_opened", pull_request: current.summary }, status: :conflict
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The record an update publishes: the install pull request itself when
|
|
154
|
+
# the sandbox it was published from is the one live now, and otherwise
|
|
155
|
+
# a record of the live sandbox for the same pull request. A sandbox of
|
|
156
|
+
# the pull request's branch starts from the commit it checked out, so
|
|
157
|
+
# the update goes on top of the branch as that sandbox saw it.
|
|
158
|
+
def update_record
|
|
159
|
+
current = @project.install_pull_request
|
|
160
|
+
unless current&.opened? && current.state.in?([ nil, "open" ])
|
|
161
|
+
render json: { error: "The project has no open install pull request to update", code: "not_opened" }, status: :unprocessable_entity
|
|
162
|
+
return nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
message = string_param(:message).to_s.strip.presence || DraftPullRequest::DEFAULT_UPDATE_MESSAGE
|
|
166
|
+
return current.tap { current.assign_attributes(operation: "update", commit_message: message) } if current.sandbox_session_id == @sandbox.id
|
|
167
|
+
|
|
168
|
+
@sandbox.draft_pull_requests.new(
|
|
169
|
+
current.attributes.slice(*%w[repository base_branch branch title body number url state draft head_commit compare_url])
|
|
170
|
+
.merge("operation" => "update", "commit_message" => message, "status" => "queued")
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Opens a pull request for the install branch, which was published
|
|
175
|
+
# without one.
|
|
176
|
+
def open_branch(regular:)
|
|
177
|
+
record = @project.install_pull_request
|
|
178
|
+
unless record&.branch_only? && !record.in_progress?
|
|
179
|
+
return render json: { error: "The install branch is not waiting to be opened as a pull request", code: "nothing_published" },
|
|
180
|
+
status: :unprocessable_entity
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
record.assign_attributes(operation: regular ? "open_regular" : "open_draft")
|
|
184
|
+
assign_owner(record)
|
|
185
|
+
return unless authorize_action!(:publish_pull_request, record)
|
|
186
|
+
|
|
187
|
+
queue(record, files: false)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Checks the files against the preview and queues the publish, one at a
|
|
191
|
+
# time per sandbox, then makes the record the project's install pull
|
|
192
|
+
# request. A choice that leaves out a file the branch's boots need
|
|
193
|
+
# (ProjectInstallPullRequest#missing_required_paths) is refused.
|
|
194
|
+
# +on_branch+ is a record of a sandbox that checked out the pull
|
|
195
|
+
# request's branch, whose checkout commit is the head it updates.
|
|
196
|
+
def queue(record, on_branch: false, files: true)
|
|
197
|
+
publisher = install.publisher(@sandbox)
|
|
198
|
+
refusal = publisher.read_refusal
|
|
199
|
+
raise refusal if refusal
|
|
200
|
+
|
|
201
|
+
credential = publisher.credential
|
|
202
|
+
unless credential.available?
|
|
203
|
+
return render json: { error: credential.refusal, code: "no_credential", patch_available: true }, status: :unprocessable_entity
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if files
|
|
207
|
+
selection = selection_param
|
|
208
|
+
missing = install.missing_required_paths(publisher, selection.keys)
|
|
209
|
+
if missing.any?
|
|
210
|
+
raise DraftPullRequestPublisher::Refused.new("Every boot of the branch needs #{missing.to_sentence}, which the sandbox " \
|
|
211
|
+
"changed: choose #{missing.one? ? "it" : "them"} too", code: "incomplete_install")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
changes = publisher.changes(allowlist: install.allowlist, paths: selection.keys)
|
|
215
|
+
chosen = publisher.select!(changes, selection)
|
|
216
|
+
record.base_commit = changes.base_commit
|
|
217
|
+
record.head_commit = changes.base_commit if on_branch
|
|
218
|
+
record.files = chosen.map { |file| { "path" => file.path, "status" => file.status, "mode" => file.mode, "digest" => file.digest } }
|
|
219
|
+
end
|
|
220
|
+
record.assign_attributes(status: "queued", credential_kind: credential.kind, error_code: nil, error_message: nil)
|
|
221
|
+
return render json: { error: record.errors.full_messages.to_sentence }, status: :unprocessable_entity unless record.valid?
|
|
222
|
+
|
|
223
|
+
busy = nil
|
|
224
|
+
@sandbox.with_lock do
|
|
225
|
+
@sandbox.draft_pull_requests.fail_stalled!
|
|
226
|
+
busy = @sandbox.draft_pull_requests.where(status: %w[queued publishing]).first
|
|
227
|
+
unless busy
|
|
228
|
+
record.save!
|
|
229
|
+
@project.update!(settings: @project.settings.merge("install_pull_request_id" => record.id))
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
if busy
|
|
233
|
+
return render json: { error: "A publish from this sandbox is already #{busy.status}", pull_request: busy.summary }, status: :conflict
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
DraftPullRequestJob.perform_later(record.id)
|
|
237
|
+
render json: { pull_request: record.summary, project: @project.reload.summary }, status: :accepted
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def assign_owner(record)
|
|
241
|
+
record.user_id = current_user.id if ActionAgent.user_class.present? && current_user.respond_to?(:id)
|
|
242
|
+
record.account_id = @sandbox.account_id if @sandbox.has_attribute?(:account_id) && @sandbox.account_id
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def publishing_status
|
|
246
|
+
sandbox = @project.current_sandbox_session
|
|
247
|
+
live = sandbox && (sandbox.ready? || sandbox.running?) && sandbox.active?
|
|
248
|
+
return { supported: true, available: false, live: false, refusal: "The project's sandbox is not running", patch_available: false } unless live
|
|
249
|
+
|
|
250
|
+
publisher = install.publisher(sandbox)
|
|
251
|
+
refusal = publisher.read_refusal
|
|
252
|
+
credential = refusal ? nil : publisher.credential
|
|
253
|
+
{
|
|
254
|
+
supported: refusal&.code != "unsupported",
|
|
255
|
+
available: refusal.nil? && credential.available?,
|
|
256
|
+
live: true,
|
|
257
|
+
refusal: refusal&.message || credential&.refusal,
|
|
258
|
+
refusal_code: refusal&.code || (credential&.available? == false ? "no_credential" : nil),
|
|
259
|
+
credential: credential&.kind,
|
|
260
|
+
patch_available: refusal.nil?
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The allowlist as the dialog shows it: paths, and a description of the
|
|
265
|
+
# migration pattern.
|
|
266
|
+
def allowlist_json
|
|
267
|
+
install.allowlist.map do |entry|
|
|
268
|
+
entry.is_a?(Regexp) ? "db/migrate/<timestamp>_<a migration action_agent:install emits>.rb" : entry
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def selection_param
|
|
273
|
+
files = params[:files]
|
|
274
|
+
files = files.map { |file| file.respond_to?(:permit) ? file.permit(:path, :digest).to_h : file } if files.is_a?(Array)
|
|
275
|
+
unless files.is_a?(Array) && files.all? { |file| file.is_a?(Hash) && file["path"].is_a?(String) && file["digest"].is_a?(String) }
|
|
276
|
+
raise DraftPullRequestPublisher::Refused.new("files must be a list of { path, digest } from the preview", code: "nothing_selected")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
files.to_h { |file| [ file["path"], file["digest"] ] }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def string_param(name)
|
|
283
|
+
value = params[name]
|
|
284
|
+
value.is_a?(String) ? value : nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def boolean_param(name)
|
|
288
|
+
ActiveModel::Type::Boolean.new.cast(params[name]) == true
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def refused(error)
|
|
292
|
+
status = error.code == "changed_since_preview" ? :conflict : :unprocessable_entity
|
|
293
|
+
render json: { error: error.message, code: error.code }, status: status
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def confirmation_required(exception)
|
|
297
|
+
render json: { error: exception.message, code: "confirmation_required", confirmation: exception.message }, status: :conflict
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def boot_refused(exception)
|
|
301
|
+
render json: { error: exception.message, code: "boot_refused" }, status: :unprocessable_entity
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# A project's secrets (the Environment tab): names, sources and who set
|
|
6
|
+
# them, never values. Setting, replacing and removing one needs
|
|
7
|
+
# :manage_project_secrets, and using the organization's provider key
|
|
8
|
+
# :manage_credentials as well (see ProjectSecretAuthorization). A name
|
|
9
|
+
# the sandbox sets itself, one that changes how code is loaded, or one
|
|
10
|
+
# the project's sign-in keeps is refused with 422 (see ProjectSecret), and
|
|
11
|
+
# so is a source a person cannot give
|
|
12
|
+
# (ProjectSecretAuthorization#secret_sources_allowed!).
|
|
13
|
+
class ProjectSecretsController < BaseController
|
|
14
|
+
include ProjectSecretAuthorization
|
|
15
|
+
|
|
16
|
+
before_action :require_owner!
|
|
17
|
+
before_action :set_project
|
|
18
|
+
|
|
19
|
+
# GET /api/projects/:project_id/secrets
|
|
20
|
+
def index
|
|
21
|
+
render json: { secrets: summaries }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# PUT /api/projects/:project_id/secrets
|
|
25
|
+
# { secrets: [{ name:, value: } | { name:, source: "organization_key", consent: true }] }
|
|
26
|
+
# Sets every listed secret, replacing those that exist, or none of them
|
|
27
|
+
# when one is refused. Each saved secret comes back with the warnings
|
|
28
|
+
# its value raises (ProjectSecret.warnings_for).
|
|
29
|
+
def upsert
|
|
30
|
+
entries = params[:secrets]
|
|
31
|
+
unless entries.is_a?(Array) && entries.all? { |entry| entry.respond_to?(:permit) }
|
|
32
|
+
return render json: { error: "secrets must be a list of { name:, value: }" }, status: :bad_request
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return unless secret_sources_allowed!(entries)
|
|
36
|
+
|
|
37
|
+
records = entries.map { |entry| assign(entry) }
|
|
38
|
+
return unless records.all? { |record| authorize_secret!(record) }
|
|
39
|
+
|
|
40
|
+
ProjectSecret.transaction { records.each(&:save!) }
|
|
41
|
+
render json: { secrets: summaries, saved: records.map { |record| saved_json(record) } }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# PUT /api/projects/:project_id/secrets/:name { value: } or
|
|
45
|
+
# { source: "organization_key", consent: true }
|
|
46
|
+
def update
|
|
47
|
+
return unless secret_sources_allowed!([ params ])
|
|
48
|
+
|
|
49
|
+
record = assign(params)
|
|
50
|
+
return unless authorize_secret!(record)
|
|
51
|
+
|
|
52
|
+
record.save!
|
|
53
|
+
render json: { secret: saved_json(record) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# DELETE /api/projects/:project_id/secrets/:name
|
|
57
|
+
def destroy
|
|
58
|
+
record = @project.secrets.find_by!(name: params[:name])
|
|
59
|
+
return unless authorize_action!(:manage_project_secrets, record)
|
|
60
|
+
|
|
61
|
+
record.destroy!
|
|
62
|
+
head :no_content
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def set_project
|
|
68
|
+
@project = owned(Project).find(params[:project_id])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def assign(entry)
|
|
72
|
+
attributes = entry.slice(:name, :value, :source, :consent).permit(:name, :value, :source, :consent).to_h
|
|
73
|
+
@project.assign_secret(
|
|
74
|
+
name: attributes["name"], value: attributes["value"], source: attributes["source"],
|
|
75
|
+
consent: ActiveModel::Type::Boolean.new.cast(attributes["consent"]) == true, set_by: current_user
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def summaries
|
|
80
|
+
records = @project.secrets.reload.ordered.to_a
|
|
81
|
+
setters = setters_for(records)
|
|
82
|
+
records.map { |record| record.as_summary(setters) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def saved_json(record)
|
|
86
|
+
record.as_summary(setters_for([ record ])).merge(warnings: record.warnings)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def setters_for(records)
|
|
90
|
+
ids = records.filter_map(&:set_by_id).uniq
|
|
91
|
+
user_class = ActionAgent.user_class&.safe_constantize
|
|
92
|
+
ids.empty? || user_class.nil? ? {} : user_class.where(id: ids).index_by(&:id)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# Getting a project's sandbox to boot and choosing what its App assistant
|
|
6
|
+
# may read:
|
|
7
|
+
#
|
|
8
|
+
# - starting the setup assistant on a failed boot (see ProjectSetup), or
|
|
9
|
+
# Claude Code in its checkout (see ProjectBootFix)
|
|
10
|
+
# - the requests for input the project's agents are waiting on, which
|
|
11
|
+
# the Project page answers through the input requests API
|
|
12
|
+
# - the app's models as a boot listed them, and the models and columns
|
|
13
|
+
# the App assistant gets schema tools over
|
|
14
|
+
class ProjectSetupController < BaseController
|
|
15
|
+
include ProjectSecretAuthorization
|
|
16
|
+
|
|
17
|
+
before_action :require_owner!
|
|
18
|
+
before_action :set_project
|
|
19
|
+
before_action :require_execution_enabled!, only: [ :start, :fix_with_claude_code ]
|
|
20
|
+
|
|
21
|
+
rescue_from Project::ConfirmationRequired, with: :confirmation_required
|
|
22
|
+
rescue_from Project::BootRefused, SandboxOrchestrator::UnsupportedBackendError, with: :boot_refused
|
|
23
|
+
|
|
24
|
+
# POST /api/projects/:project_id/setup
|
|
25
|
+
# Starts the setup assistant on the project's failed boot. Its tools
|
|
26
|
+
# set the project's environment, so this needs :manage_project_secrets.
|
|
27
|
+
def start
|
|
28
|
+
return unless authorize_action!(:manage_project_secrets, secret_subject)
|
|
29
|
+
|
|
30
|
+
enforce_execution_quota!
|
|
31
|
+
return if performed?
|
|
32
|
+
|
|
33
|
+
run = ProjectSetup.start!(@project, trigger: "requested")
|
|
34
|
+
record_execution_usage
|
|
35
|
+
render json: { project: @project.reload.summary, run: { id: run.id, status: run.status, agent_id: run.agent_id } },
|
|
36
|
+
status: :accepted
|
|
37
|
+
rescue ProjectSetup::Unavailable => e
|
|
38
|
+
render json: { error: e.message, code: "setup_unavailable" }, status: :conflict
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# POST /api/projects/:project_id/setup/claude_code
|
|
42
|
+
# Starts a Claude Code session in the failed boot's checkout to fix the
|
|
43
|
+
# step that failed; the boot resumes once it changed something. It
|
|
44
|
+
# runs on the caller's Claude Code, as a session started from the
|
|
45
|
+
# sandbox's page would.
|
|
46
|
+
def fix_with_claude_code
|
|
47
|
+
enforce_execution_quota!
|
|
48
|
+
return if performed?
|
|
49
|
+
|
|
50
|
+
session = ProjectBootFix.start!(@project, trigger: "requested", user_id: current_user.respond_to?(:id) ? current_user.id : @project.user_id)
|
|
51
|
+
record_execution_usage
|
|
52
|
+
render json: { project: @project.reload.summary, code_session: session.summary }, status: :accepted
|
|
53
|
+
rescue ProjectBootFix::Unavailable => e
|
|
54
|
+
render json: { error: e.message, code: "claude_code_unavailable" }, status: :conflict
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# PATCH /api/projects/:project_id/setup { auto: false }
|
|
58
|
+
# Whether a failed boot starts Claude Code or the setup assistant on its
|
|
59
|
+
# own.
|
|
60
|
+
def update
|
|
61
|
+
return unless authorize_action!(:manage_project_secrets, secret_subject)
|
|
62
|
+
|
|
63
|
+
@project.update_setup_settings!("auto" => ActiveModel::Type::Boolean.new.cast(params[:auto]) != false)
|
|
64
|
+
render json: { project: @project.summary }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# GET /api/projects/:project_id/input_requests
|
|
68
|
+
# The requests pending on runs of the project's setup assistant and of
|
|
69
|
+
# the agent it evaluates, newest first, as GET /api/input_requests
|
|
70
|
+
# lists them. Overdue ones are expired first.
|
|
71
|
+
def input_requests
|
|
72
|
+
InputRequest.expire_overdue!(@project.input_requests)
|
|
73
|
+
requests = @project.pending_input_requests.for_listing.includes(:subject).recent.limit(InputRequestsController::LIST_LIMIT)
|
|
74
|
+
render json: { input_requests: requests.map { |request| InputRequestSerializer.call(request) } }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# GET /api/projects/:project_id/app_models
|
|
78
|
+
# The app's models and columns as the last boot's manifest listed them,
|
|
79
|
+
# with the choices made so far. 409 before a boot listed any.
|
|
80
|
+
def app_models
|
|
81
|
+
models = @project.app_models
|
|
82
|
+
if models.nil?
|
|
83
|
+
return render json: { error: "Boot the project first: a boot lists the app's models", code: "not_listed" }, status: :conflict
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
render json: { models: models, schema_tools: @project.schema_tools }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# PUT /api/projects/:project_id/schema_tools
|
|
90
|
+
# { schema_tools: [{ model:, filterable: [], returns: [] }], apply: true }
|
|
91
|
+
# Stores the models and columns the App assistant may read. Every boot
|
|
92
|
+
# after this writes their tools; with `apply`, a running sandbox is
|
|
93
|
+
# replaced by a new boot now, so the assistant's tools follow at once.
|
|
94
|
+
def schema_tools
|
|
95
|
+
unless @project.app_assistant?
|
|
96
|
+
return render json: { error: "Only a project evaluated with the App assistant chooses what it may read", code: "not_app_assistant" },
|
|
97
|
+
status: :conflict
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
choices = params[:schema_tools]
|
|
101
|
+
unless choices.is_a?(Array) && choices.all? { |choice| choice.respond_to?(:permit) }
|
|
102
|
+
return render json: { error: "schema_tools must be a list of { model:, filterable:, returns: }" }, status: :bad_request
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@project.choose_schema_tools!(choices.map { |choice| choice.permit(:model, filterable: [], returns: []).to_h })
|
|
106
|
+
rebooted = ActiveModel::Type::Boolean.new.cast(params[:apply]) == true && reboot!
|
|
107
|
+
return if performed?
|
|
108
|
+
|
|
109
|
+
render json: { project: @project.reload.summary, rebooted: rebooted == true }
|
|
110
|
+
rescue SandboxBootSpec::Invalid => e
|
|
111
|
+
render json: { error: e.message, code: "invalid_schema_tools" }, status: :unprocessable_entity
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def set_project
|
|
117
|
+
@project = owned(Project).find(params[:project_id])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def secret_subject
|
|
121
|
+
ProjectSecret.new(project: @project, account_id: @project.account_id, user_id: @project.user_id)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Stops the current sandbox and boots a new one from the project's
|
|
125
|
+
# spec, as POST /api/projects/:id/boot boots one. Answers whether a
|
|
126
|
+
# boot started.
|
|
127
|
+
def reboot!
|
|
128
|
+
unless ActionAgent.execution_enabled?
|
|
129
|
+
render json: { error: "Agent execution is disabled on this dashboard" }, status: :forbidden
|
|
130
|
+
return false
|
|
131
|
+
end
|
|
132
|
+
enforce_execution_quota!
|
|
133
|
+
return false if performed?
|
|
134
|
+
|
|
135
|
+
current = @project.current_sandbox_session
|
|
136
|
+
current.expire! if current && !current.expired?
|
|
137
|
+
@project.ensure_sandbox!(confirm: ActiveModel::Type::Boolean.new.cast(params[:confirm]) == true, confirmed_by: current_user)
|
|
138
|
+
record_execution_usage
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def confirmation_required(exception)
|
|
143
|
+
render json: { error: exception.message, code: "confirmation_required", confirmation: exception.message }, status: :conflict
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def boot_refused(exception)
|
|
147
|
+
render json: { error: exception.message, code: "boot_refused" }, status: :unprocessable_entity
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# The test account step: how the project's browser signs in to the app,
|
|
6
|
+
# kept as sign-in ProjectSecrets that never reach the sandbox's
|
|
7
|
+
# environment or a model.
|
|
8
|
+
#
|
|
9
|
+
# GET /api/projects/:project_id/sign_in what is set, never the password
|
|
10
|
+
# PUT /api/projects/:project_id/sign_in { login_url, login, password,
|
|
11
|
+
# login_field, password_field, submit_field }
|
|
12
|
+
# POST /api/projects/:project_id/sign_in/check signs the browser in with them
|
|
13
|
+
# POST /api/projects/:project_id/sign_in/save_browser keeps the running browser's sign-in
|
|
14
|
+
# DELETE /api/projects/:project_id/sign_in no sign-in
|
|
15
|
+
#
|
|
16
|
+
# Changing them needs :manage_project_secrets, asked about the secret.
|
|
17
|
+
# A check answers BrowserSignIn's status, "unsupported" when the login
|
|
18
|
+
# page has no password field.
|
|
19
|
+
class ProjectSignInsController < BaseController
|
|
20
|
+
include ProjectBrowser
|
|
21
|
+
|
|
22
|
+
before_action :require_owner!
|
|
23
|
+
before_action :set_project
|
|
24
|
+
before_action :require_execution_enabled!, only: [ :check ]
|
|
25
|
+
|
|
26
|
+
def show
|
|
27
|
+
render json: { sign_in: summary }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update
|
|
31
|
+
secret = @project.assign_sign_in(credential_params, set_by: current_user)
|
|
32
|
+
return unless authorize_action!(:manage_project_secrets, secret)
|
|
33
|
+
|
|
34
|
+
if secret.save
|
|
35
|
+
render json: { sign_in: summary, warnings: secret.warnings }
|
|
36
|
+
else
|
|
37
|
+
render json: { error: secret.errors.full_messages.to_sentence }, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
secrets = @project.secrets.where(kind: %w[sign_in storage_state]).to_a
|
|
43
|
+
return unless secrets.all? { |secret| authorize_action!(:manage_project_secrets, secret) }
|
|
44
|
+
|
|
45
|
+
secrets.each(&:destroy!)
|
|
46
|
+
render json: { sign_in: summary }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check
|
|
50
|
+
secret = @project.secrets.sign_in.find_by(name: Project::SIGN_IN_SECRET)
|
|
51
|
+
unless secret
|
|
52
|
+
return render json: { error: "Enter the test account's login and password first", code: "no_sign_in" },
|
|
53
|
+
status: :unprocessable_entity
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sandbox = ready_project_sandbox!(@project) or return
|
|
57
|
+
sandbox, = ensure_project_browser!(@project, sandbox)
|
|
58
|
+
return if performed?
|
|
59
|
+
|
|
60
|
+
render json: { result: BrowserSignIn.call(sandbox, secret).to_h, sign_in: summary }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def save_browser
|
|
64
|
+
sandbox = @project.current_sandbox_session
|
|
65
|
+
unless sandbox&.browser_running?
|
|
66
|
+
return render json: { error: "Start the project's browser and sign in to the app there first", code: "browser_not_running" },
|
|
67
|
+
status: :conflict
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
state = SandboxBrowser.storage_state(sandbox)
|
|
71
|
+
secret = @project.assign_storage_state(state, set_by: current_user)
|
|
72
|
+
return unless authorize_action!(:manage_project_secrets, secret)
|
|
73
|
+
|
|
74
|
+
if secret.save
|
|
75
|
+
render json: { sign_in: summary }
|
|
76
|
+
else
|
|
77
|
+
render json: { error: secret.errors.full_messages.to_sentence }, status: :unprocessable_entity
|
|
78
|
+
end
|
|
79
|
+
rescue SandboxBrowser::Error => e
|
|
80
|
+
render json: { error: e.message, code: "browser_unavailable" }, status: :unprocessable_entity
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def set_project
|
|
86
|
+
@project = owned(Project).find(params[:project_id])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def credential_params
|
|
90
|
+
params.permit(*ProjectSecret::SIGN_IN_FIELDS).to_h
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def summary
|
|
94
|
+
credentials = @project.secrets.sign_in.find_by(name: Project::SIGN_IN_SECRET)
|
|
95
|
+
state = @project.secrets.storage_state.find_by(name: Project::STORAGE_STATE_SECRET)
|
|
96
|
+
fields = credentials&.sign_in_credentials || {}
|
|
97
|
+
{
|
|
98
|
+
credentials: credentials && {
|
|
99
|
+
secret_ref: credentials.name,
|
|
100
|
+
login_url: fields["login_url"],
|
|
101
|
+
login: fields["login"],
|
|
102
|
+
password_set: fields["password"].present?,
|
|
103
|
+
fields: fields.slice("login_field", "password_field", "submit_field"),
|
|
104
|
+
updated_at: credentials.updated_at&.iso8601
|
|
105
|
+
},
|
|
106
|
+
storage_state: state && { saved: true, updated_at: state.updated_at&.iso8601 },
|
|
107
|
+
browser_running: @project.current_sandbox_session&.browser_running? || false
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|