actionagent 1.9.4 → 1.9.6
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/README.md +9 -1
- data/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +61 -61
- data/app/controllers/action_agent/api/agents_controller.rb +1 -17
- data/app/controllers/action_agent/api/evaluation_reports_controller.rb +11 -7
- data/app/controllers/action_agent/api/github_app_webhooks_controller.rb +98 -0
- data/app/controllers/action_agent/api/github_installations_controller.rb +9 -1
- data/app/controllers/action_agent/api/mcp_controller.rb +5 -2
- data/app/controllers/action_agent/api/projects_controller.rb +148 -44
- data/app/controllers/action_agent/api/sandboxes_controller.rb +31 -6
- data/app/controllers/action_agent/dashboard_controller.rb +26 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +199 -24
- data/app/jobs/action_agent/github_webhook_job.rb +102 -0
- data/app/models/action_agent/github_connection.rb +21 -0
- data/app/models/action_agent/github_installation.rb +28 -1
- data/app/models/action_agent/github_webhook_delivery.rb +45 -0
- data/app/models/action_agent/provider_key.rb +8 -1
- data/app/models/action_agent/scenario_catalog.rb +8 -2
- data/app/serializers/action_agent/agent_version_serializer.rb +29 -0
- data/app/serializers/action_agent/evaluation_serializer.rb +9 -0
- data/app/services/action_agent/evaluation_report_import.rb +112 -19
- data/app/services/action_agent/github_client.rb +11 -2
- data/app/services/action_agent/local_sandbox_claude_login.rb +1 -1
- data/app/services/action_agent/project_capabilities.rb +6 -3
- data/config/routes.rb +15 -0
- data/lib/action_agent/claude_login_process.rb +1 -1
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +15 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +6 -0
- data/lib/generators/action_agent/templates/migrations/029_create_active_agent_github_webhook_deliveries.rb.erb +23 -0
- metadata +7 -2
|
@@ -17,9 +17,18 @@ module ActionAgent
|
|
|
17
17
|
# evaluation — that agent's evaluation for the `suite` and the report's
|
|
18
18
|
# scope (SCOPE_KEYS in its metadata), named for both
|
|
19
19
|
# scenarios — one per reported scenario key, updated to the reported prompt
|
|
20
|
-
# run — one
|
|
20
|
+
# run — one EvaluationRun per tenant and run_id, with one
|
|
21
21
|
# EvaluationScenarioResult per scenario and model
|
|
22
22
|
#
|
|
23
|
+
# An envelope's `status` is "complete" when absent. A "running" envelope
|
|
24
|
+
# stores the results so far as a running run, and each later delivery under
|
|
25
|
+
# its run_id replaces them, until a "complete" one finishes the run:
|
|
26
|
+
# running — "running" or "complete" replaces the stored results
|
|
27
|
+
# complete — "complete" is an identical retry or a Conflict, as for any
|
|
28
|
+
# stored run; "running" changes nothing, so a progress
|
|
29
|
+
# delivery that lands after the final one is harmless
|
|
30
|
+
# Only the first delivery under a run_id is admitted and counted.
|
|
31
|
+
#
|
|
23
32
|
# The run's per-model summary, criterion scores and recommendations are
|
|
24
33
|
# computed from the stored results, not taken from the report. The judge's
|
|
25
34
|
# verdict and label are taken from it, and so is what the judge spent —
|
|
@@ -85,6 +94,11 @@ module ActionAgent
|
|
|
85
94
|
MAX_JUDGE_KINDS = 10
|
|
86
95
|
SCOPE_PATTERN = %r{\A[\w .:/@-]{1,100}\z}
|
|
87
96
|
STATUSES = %w[passed failed errored].freeze
|
|
97
|
+
# The envelope statuses a delivery may name.
|
|
98
|
+
RUN_STATUSES = %w[running complete].freeze
|
|
99
|
+
# What this collector accepts beyond a version-1 report, sent with its
|
|
100
|
+
# refusal of a payload that is not one so a publisher can ask before a run.
|
|
101
|
+
CAPABILITIES = %w[progress].freeze
|
|
88
102
|
# Report metadata that tells one evaluation of a suite from another, in the
|
|
89
103
|
# order it is written into the evaluation's name.
|
|
90
104
|
SCOPE_KEYS = %w[scope environment role].freeze
|
|
@@ -102,20 +116,22 @@ module ActionAgent
|
|
|
102
116
|
# The evaluation_runs columns an import writes that later migrations added.
|
|
103
117
|
REQUIRED_RUN_COLUMNS = %w[external_tenant external_run_id external_report_digest agent_version_id].freeze
|
|
104
118
|
|
|
105
|
-
# Returns `[run, duplicate]`: the stored EvaluationRun,
|
|
106
|
-
# earlier
|
|
107
|
-
#
|
|
119
|
+
# Returns `[run, duplicate, progressed]`: the stored EvaluationRun, whether
|
|
120
|
+
# an earlier delivery had already stored this report, and whether this
|
|
121
|
+
# delivery replaced the results of a running run. `account` is the tenant,
|
|
122
|
+
# or nil on a single-tenant install.
|
|
108
123
|
#
|
|
109
|
-
# `admit` is asked only for a
|
|
110
|
-
#
|
|
111
|
-
# is raised as Denied.
|
|
124
|
+
# `admit` is asked only for a run_id the tenant holds no run under. It
|
|
125
|
+
# returns nil to allow the report, or a denial, which is raised as Denied.
|
|
112
126
|
#
|
|
113
127
|
# The envelope's identities are validated as sent. NUL characters, which
|
|
114
128
|
# PostgreSQL cannot store, are removed from every string inside `report`.
|
|
115
129
|
#
|
|
116
130
|
# Raises Invalid for a payload that is not a valid version-1 report, or an
|
|
117
131
|
# evaluation name the agent already holds for another suite or scope;
|
|
118
|
-
# Conflict for different content under a
|
|
132
|
+
# Conflict for different content under a complete run_id, or a delivery
|
|
133
|
+
# under a running run_id that the dashboard executed or that names another
|
|
134
|
+
# agent, suite or scope; Refused (and
|
|
119
135
|
# LimitExceeded) when storing needs an operator to act first; Denied; and
|
|
120
136
|
# Busy.
|
|
121
137
|
def self.call(payload:, account: nil, admit: nil)
|
|
@@ -162,24 +178,25 @@ module ActionAgent
|
|
|
162
178
|
validate!
|
|
163
179
|
digest = self.class.digest(@payload)
|
|
164
180
|
existing = stored_run
|
|
165
|
-
return
|
|
181
|
+
return settled(existing, digest) if settles?(existing, digest)
|
|
166
182
|
|
|
167
183
|
attempts = 0
|
|
168
184
|
begin
|
|
169
185
|
OwnerLock.new(EvaluationRun.connection, owner_lock_key).synchronize do
|
|
170
186
|
EvaluationRun.transaction(requires_new: true) do
|
|
171
187
|
existing = stored_run
|
|
172
|
-
next
|
|
188
|
+
next settled(existing, digest) if settles?(existing, digest)
|
|
189
|
+
next [ progress(existing, digest), false, true ] if existing
|
|
173
190
|
|
|
174
191
|
admit!
|
|
175
|
-
[ import(digest), false ]
|
|
192
|
+
[ import(digest), false, false ]
|
|
176
193
|
end
|
|
177
194
|
end
|
|
178
195
|
rescue ActiveRecord::RecordNotUnique
|
|
179
196
|
# A concurrent delivery committed this run, or an agent, evaluation or
|
|
180
197
|
# scenario this import was creating, first. Reading again sees it.
|
|
181
198
|
existing = stored_run
|
|
182
|
-
return
|
|
199
|
+
return settled(existing, digest) if settles?(existing, digest)
|
|
183
200
|
|
|
184
201
|
attempts += 1
|
|
185
202
|
retry if attempts < 2
|
|
@@ -265,6 +282,29 @@ module ActionAgent
|
|
|
265
282
|
EvaluationReportExport.find(@payload["run_id"], account: @account)
|
|
266
283
|
end
|
|
267
284
|
|
|
285
|
+
def running_delivery?
|
|
286
|
+
@payload["status"] == "running"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Whether a delivery under a stored run's run_id leaves its results as
|
|
290
|
+
# they are. Only a running run this collector imported takes new ones.
|
|
291
|
+
def settles?(run, digest)
|
|
292
|
+
return false unless run
|
|
293
|
+
return true unless run.running? && run.imported?
|
|
294
|
+
|
|
295
|
+
run.external_report_digest == digest
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Returns `[run, true, false]` for a delivery that leaves the stored run as
|
|
299
|
+
# it is. Raises Conflict for a run the dashboard is executing, and for a
|
|
300
|
+
# different "complete" report under a run that has finished.
|
|
301
|
+
def settled(run, digest)
|
|
302
|
+
raise Conflict, "run_id names a run the dashboard is executing" if run.running? && !run.imported?
|
|
303
|
+
return [ run, true, false ] if running_delivery? || run.running?
|
|
304
|
+
|
|
305
|
+
[ identical!(run, digest), true, false ]
|
|
306
|
+
end
|
|
307
|
+
|
|
268
308
|
# The stored run, when the payload is the report it was stored from or
|
|
269
309
|
# the run's own export (EvaluationReportExport), which is how an
|
|
270
310
|
# exported run posted back to its install is told from a different
|
|
@@ -286,26 +326,60 @@ module ActionAgent
|
|
|
286
326
|
raise Denied, denial if denial.present?
|
|
287
327
|
end
|
|
288
328
|
|
|
329
|
+
# Replaces a running run's results with the delivery's.
|
|
330
|
+
def progress(run, digest)
|
|
331
|
+
agent = find_or_create_agent
|
|
332
|
+
evaluation = find_or_initialize_evaluation(agent)
|
|
333
|
+
raise Conflict, "run_id already exists for another agent, suite or scope" unless evaluation.id == run.evaluation_id
|
|
334
|
+
|
|
335
|
+
scenarios = upsert_scenarios(evaluation)
|
|
336
|
+
run.scenario_results.destroy_all
|
|
337
|
+
run.update!(run_attributes(agent, digest))
|
|
338
|
+
store_results(run, scenarios, agent)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# A new run is dated by the time its report says it finished, when it
|
|
342
|
+
# names one (see #finished_at), so a run exported from one install and
|
|
343
|
+
# imported into another keeps its place in the run history.
|
|
289
344
|
def import(digest)
|
|
290
345
|
agent = find_or_create_agent
|
|
291
346
|
evaluation = find_or_initialize_evaluation(agent)
|
|
292
347
|
scenarios = upsert_scenarios(evaluation)
|
|
293
348
|
run = evaluation.evaluation_runs.create!(
|
|
294
|
-
external_tenant: tenant_key,
|
|
295
|
-
|
|
349
|
+
run_attributes(agent, digest).merge(external_tenant: tenant_key, external_run_id: @payload["run_id"], created_at: finished_at)
|
|
350
|
+
)
|
|
351
|
+
store_results(run, scenarios, agent)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def run_attributes(agent, digest)
|
|
355
|
+
{
|
|
296
356
|
external_report_digest: digest,
|
|
297
357
|
agent_version: reported_release_version(agent),
|
|
298
|
-
status: :complete,
|
|
358
|
+
status: running_delivery? ? :running : :complete,
|
|
299
359
|
selection: selection,
|
|
300
360
|
scores: recorded_scores,
|
|
301
361
|
samples_evaluated: results.size,
|
|
302
362
|
samples_passed: results.count { |result| result["status"] == "passed" },
|
|
303
|
-
completed_at:
|
|
304
|
-
|
|
363
|
+
completed_at: running_delivery? ? nil : finished_at
|
|
364
|
+
}
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# The report's `metadata.finished`, which EvaluationRun#to_report records
|
|
368
|
+
# as the run's completion time, or now when the report names none. A
|
|
369
|
+
# time later than now is read as now.
|
|
370
|
+
def finished_at
|
|
371
|
+
@finished_at ||= begin
|
|
372
|
+
now = Time.current
|
|
373
|
+
reported = metadata["finished"] && Time.iso8601(metadata["finished"])
|
|
374
|
+
reported && reported < now ? reported : now
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def store_results(run, scenarios, agent)
|
|
305
379
|
results.each { |result| persist(run, scenarios.fetch(result["scenario_key"]), result) }
|
|
306
380
|
run.update!(scores: summarized_scores(run))
|
|
307
|
-
evaluation.touch
|
|
308
|
-
agent.update_columns(last_observed_at:
|
|
381
|
+
run.evaluation.touch
|
|
382
|
+
agent.update_columns(last_observed_at: Time.current, updated_at: Time.current)
|
|
309
383
|
run
|
|
310
384
|
end
|
|
311
385
|
|
|
@@ -621,6 +695,9 @@ module ActionAgent
|
|
|
621
695
|
object!(@payload, "payload")
|
|
622
696
|
validate_json!(@payload)
|
|
623
697
|
raise Invalid, "version must be 1" unless @payload["version"] == 1
|
|
698
|
+
unless @payload["status"].nil? || RUN_STATUSES.include?(@payload["status"])
|
|
699
|
+
raise Invalid, "status must be one of #{RUN_STATUSES.join(', ')}"
|
|
700
|
+
end
|
|
624
701
|
|
|
625
702
|
%w[run_id source suite].each { |key| identifier!(@payload[key], key) }
|
|
626
703
|
unless @payload["agent_name"].is_a?(String) && AGENT_NAME_PATTERN.match?(@payload["agent_name"]) && @payload["agent_name"].strip.length >= 2
|
|
@@ -634,6 +711,7 @@ module ActionAgent
|
|
|
634
711
|
raise Invalid, "suite and scope name an evaluation longer than #{MAX_EVALUATION_NAME} characters"
|
|
635
712
|
end
|
|
636
713
|
|
|
714
|
+
validate_finished!
|
|
637
715
|
judge_trace_ids!(metadata["judge_trace_ids"])
|
|
638
716
|
string!(report["judge"], "report.judge", 200)
|
|
639
717
|
validate_release!
|
|
@@ -736,6 +814,21 @@ module ActionAgent
|
|
|
736
814
|
string!(judge.dig("suggested_tool", "name"), "result.diagnosis.judge.suggested_tool.name", 200)
|
|
737
815
|
end
|
|
738
816
|
|
|
817
|
+
def validate_finished!
|
|
818
|
+
finished = metadata["finished"]
|
|
819
|
+
return if finished.nil?
|
|
820
|
+
return if finished.is_a?(String) && finished.length <= 40 && iso8601?(finished)
|
|
821
|
+
|
|
822
|
+
raise Invalid, "report.metadata.finished must be an ISO 8601 time"
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def iso8601?(value)
|
|
826
|
+
Time.iso8601(value)
|
|
827
|
+
true
|
|
828
|
+
rescue ArgumentError
|
|
829
|
+
false
|
|
830
|
+
end
|
|
831
|
+
|
|
739
832
|
# The release the report says it evaluated: a digest the dashboard can
|
|
740
833
|
# match to a version cut on deploy, and the deploy and label as text.
|
|
741
834
|
def validate_release!
|
|
@@ -60,6 +60,10 @@ module ActionAgent
|
|
|
60
60
|
# cannot see, which GitHub does not tell apart. Also 409, which GitHub
|
|
61
61
|
# answers for the commits and trees of a repository with no commits.
|
|
62
62
|
class NotFound < Error; end
|
|
63
|
+
# GitHub answered 403 to an organization membership lookup: the GitHub
|
|
64
|
+
# App lacks the organization permission Members (read), or the
|
|
65
|
+
# organization's SAML single sign-on has not authorized the token.
|
|
66
|
+
class MembershipForbidden < Error; end
|
|
63
67
|
# GitHub minted no token because the installation was removed from its
|
|
64
68
|
# account (404) or suspended (403). +reason+ is :removed or :suspended.
|
|
65
69
|
class InstallationUnavailable < Error
|
|
@@ -323,14 +327,19 @@ module ActionAgent
|
|
|
323
327
|
|
|
324
328
|
# The user's membership in +organization+ ({ "state" =>, "role" => }), or
|
|
325
329
|
# nil when GitHub reports none.
|
|
330
|
+
#
|
|
331
|
+
# @raise [MembershipForbidden] when GitHub answers 403, as it does to a
|
|
332
|
+
# GitHub App user token while the App lacks Members (read)
|
|
326
333
|
def organization_membership(organization)
|
|
327
334
|
raise Error, "#{organization.inspect} is not a GitHub login" unless organization.is_a?(String) && organization.match?(LOGIN)
|
|
328
335
|
|
|
329
336
|
get("/user/memberships/orgs/#{organization}")
|
|
337
|
+
rescue NotFound
|
|
338
|
+
nil
|
|
330
339
|
rescue Error => e
|
|
331
|
-
raise unless e.status
|
|
340
|
+
raise unless e.status == 403
|
|
332
341
|
|
|
333
|
-
|
|
342
|
+
raise MembershipForbidden.new("GitHub refused to show the membership in #{organization}: #{e.message}", status: 403)
|
|
334
343
|
end
|
|
335
344
|
|
|
336
345
|
# The repositories an installation token reaches, reduced like
|
|
@@ -108,7 +108,7 @@ module ActionAgent
|
|
|
108
108
|
data = JSON.parse(File.read(path, 8192))
|
|
109
109
|
status = data["status"].to_s
|
|
110
110
|
result = { status: %w[starting awaiting_code submitted completed failed expired cancelled].include?(status) ? status : "failed" }
|
|
111
|
-
if status == "awaiting_code" && data["authorize_url"].to_s.match?(%r{\Ahttps://(?:claude\.ai|platform\.claude\.com)/oauth/authorize\?[^\s]+\z})
|
|
111
|
+
if status == "awaiting_code" && data["authorize_url"].to_s.match?(%r{\Ahttps://(?:claude\.ai|claude\.com|platform\.claude\.com)/(?:cai/)?oauth/authorize\?[^\s]+\z})
|
|
112
112
|
result[:authorize_url] = data["authorize_url"]
|
|
113
113
|
end
|
|
114
114
|
result
|
|
@@ -14,7 +14,8 @@ module ActionAgent
|
|
|
14
14
|
# project's secrets and bootstrap reach it
|
|
15
15
|
# execution agent execution is enabled
|
|
16
16
|
# github a GitHub App or OAuth app is configured
|
|
17
|
-
# github_connection the owner
|
|
17
|
+
# github_connection the owner linked a usable GitHub App installation
|
|
18
|
+
# or connected GitHub through OAuth
|
|
18
19
|
# model_credentials credentials for the model the project's agent
|
|
19
20
|
# runs on (not blocking)
|
|
20
21
|
# browser a browser for the project's sandbox, started and
|
|
@@ -24,7 +25,8 @@ module ActionAgent
|
|
|
24
25
|
Item = Struct.new(:key, :label, :ok, :blocking, :detail, :fix, keyword_init: true)
|
|
25
26
|
|
|
26
27
|
# @param owner [Object, nil] the caller's owner, for its credentials
|
|
27
|
-
# @param github_connected [Boolean]
|
|
28
|
+
# @param github_connected [Boolean] whether the owner has a usable GitHub
|
|
29
|
+
# App installation or an OAuth connection
|
|
28
30
|
# @param base_url [String] the engine's absolute mount URL, for callback URLs
|
|
29
31
|
# @param environment [ActiveSupport::EnvironmentInquirer]
|
|
30
32
|
def initialize(owner:, github_connected:, base_url:, environment: Rails.env)
|
|
@@ -137,9 +139,10 @@ module ActionAgent
|
|
|
137
139
|
end
|
|
138
140
|
|
|
139
141
|
def github_connection_item
|
|
142
|
+
fix = github[:mode] == "app" ? "Install the GitHub App in Settings → Integrations" : "Connect GitHub in Settings → Integrations"
|
|
140
143
|
Item.new(key: "github_connection", label: "GitHub connection", ok: @github_connected, blocking: true,
|
|
141
144
|
detail: @github_connected ? "Connected" : "GitHub is not connected",
|
|
142
|
-
fix: @github_connected ? nil :
|
|
145
|
+
fix: @github_connected ? nil : fix)
|
|
143
146
|
end
|
|
144
147
|
|
|
145
148
|
def default_model
|
data/config/routes.rb
CHANGED
|
@@ -134,6 +134,7 @@ ActionAgent::Engine.routes.draw do
|
|
|
134
134
|
resources :projects, only: [ :index, :show, :create, :update, :destroy ] do
|
|
135
135
|
collection do
|
|
136
136
|
get :capabilities
|
|
137
|
+
get :repositories
|
|
137
138
|
get :preflight
|
|
138
139
|
get :discover_secrets
|
|
139
140
|
end
|
|
@@ -318,6 +319,11 @@ ActionAgent::Engine.routes.draw do
|
|
|
318
319
|
end
|
|
319
320
|
end
|
|
320
321
|
|
|
322
|
+
# The GitHub App's webhook deliveries. Authenticated by GitHub's
|
|
323
|
+
# signature (github_app_webhook_secret), not a session; the App's
|
|
324
|
+
# Webhook URL is <mount>/api/github_app/webhooks.
|
|
325
|
+
post "github_app/webhooks", to: "github_app_webhooks#create", as: :github_app_webhooks
|
|
326
|
+
|
|
321
327
|
# Creating the GitHub App from a manifest on a self-hosted dashboard.
|
|
322
328
|
resource :github_app_manifest, only: [ :create ] do
|
|
323
329
|
get :callback
|
|
@@ -365,6 +371,15 @@ ActionAgent::Engine.routes.draw do
|
|
|
365
371
|
# data. See SessionPlayerController.
|
|
366
372
|
get "session_player", to: "session_player#show", as: :session_player
|
|
367
373
|
|
|
374
|
+
# Shareable evaluation links: an evaluation, one of its runs, or one
|
|
375
|
+
# scenario result of a run. The React app reads these paths like any other
|
|
376
|
+
# client-side route; they are matched here first so the page itself is
|
|
377
|
+
# checked against the caller's scope, and a link to an evaluation outside
|
|
378
|
+
# it answers 404 whether or not the evaluation exists.
|
|
379
|
+
get "evaluations/:evaluation_id(/runs/:run_id(/results/:result_id))", to: "dashboard#evaluation",
|
|
380
|
+
as: :evaluation_link, format: false,
|
|
381
|
+
constraints: { evaluation_id: /\d+/, run_id: /\d+/, result_id: /\d+/ }
|
|
382
|
+
|
|
368
383
|
# Everything else under the mount is a client-side route: render the
|
|
369
384
|
# dashboard and let the browser resolve it. Anchored last so it can only
|
|
370
385
|
# ever catch what the routes above did not, and refuses /api paths so a
|
|
@@ -76,7 +76,7 @@ begin
|
|
|
76
76
|
buffer << chunk.force_encoding("UTF-8").scrub
|
|
77
77
|
buffer = buffer[-32_768..] || buffer
|
|
78
78
|
clean = buffer.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
79
|
-
url = clean[%r{https://(?:claude\.ai|platform\.claude\.com)/oauth/authorize\?[^\s\e]+}]
|
|
79
|
+
url = clean[%r{https://(?:claude\.ai|claude\.com|platform\.claude\.com)/(?:cai/)?oauth/authorize\?[^\s\e\a]+}]
|
|
80
80
|
if url && URI.parse(url).query.to_s.include?("client_id=")
|
|
81
81
|
write_status.call("awaiting_code", url)
|
|
82
82
|
end
|
data/lib/action_agent/version.rb
CHANGED
data/lib/action_agent.rb
CHANGED
|
@@ -867,6 +867,16 @@ module ActionAgent
|
|
|
867
867
|
attr_writer :github_app_id, :github_app_private_key, :github_app_slug,
|
|
868
868
|
:github_app_client_id, :github_app_client_secret
|
|
869
869
|
|
|
870
|
+
# The secret GitHub signs the App's webhook deliveries with, which
|
|
871
|
+
# POST <mount>/api/github_app/webhooks verifies. Falls back to
|
|
872
|
+
# GITHUB_APP_WEBHOOK_SECRET. Not one of the settings
|
|
873
|
+
# github_app_configured? needs: without it the endpoint answers 404 and
|
|
874
|
+
# the dashboard learns of installation changes only when a token mint is
|
|
875
|
+
# refused. Set the App's Webhook URL to <mount>/api/github_app/webhooks
|
|
876
|
+
# and subscribe it to Installation and Installation repositories.
|
|
877
|
+
# @return [String, nil]
|
|
878
|
+
attr_writer :github_app_webhook_secret
|
|
879
|
+
|
|
870
880
|
# MCP servers the host app itself serves or connects, appended to the
|
|
871
881
|
# built-in catalog (MCPCatalog) so the MCP Services view lists them and
|
|
872
882
|
# telemetry traffic attributes to them. Each entry is a hash shaped like
|
|
@@ -971,6 +981,10 @@ module ActionAgent
|
|
|
971
981
|
@github_app_client_secret.presence || ENV["GITHUB_APP_CLIENT_SECRET"].presence
|
|
972
982
|
end
|
|
973
983
|
|
|
984
|
+
def github_app_webhook_secret
|
|
985
|
+
@github_app_webhook_secret.presence || ENV["GITHUB_APP_WEBHOOK_SECRET"].presence
|
|
986
|
+
end
|
|
987
|
+
|
|
974
988
|
# Whether the GitHub App installation flow can run on this install: every
|
|
975
989
|
# github_app_* setting is present.
|
|
976
990
|
# @return [Boolean]
|
|
@@ -1334,6 +1348,7 @@ module ActionAgent
|
|
|
1334
1348
|
@github_app_slug = nil
|
|
1335
1349
|
@github_app_client_id = nil
|
|
1336
1350
|
@github_app_client_secret = nil
|
|
1351
|
+
@github_app_webhook_secret = nil
|
|
1337
1352
|
@trace_retention = nil
|
|
1338
1353
|
@trace_owner_resolver = nil
|
|
1339
1354
|
@usage_recorder = nil
|
|
@@ -215,6 +215,12 @@ ActionAgent.configure do |config|
|
|
|
215
215
|
# config.github_app_client_secret = Rails.application.credentials.dig(:github_app, :client_secret)
|
|
216
216
|
# config.github_app_private_key = Rails.application.credentials.dig(:github_app, :private_key)
|
|
217
217
|
#
|
|
218
|
+
# With the App's webhook pointed at <mount>/api/github_app/webhooks, the
|
|
219
|
+
# secret its deliveries are signed with (GITHUB_APP_WEBHOOK_SECRET when
|
|
220
|
+
# unset). Settings then learns of an uninstall or suspension when it happens.
|
|
221
|
+
#
|
|
222
|
+
# config.github_app_webhook_secret = Rails.application.credentials.dig(:github_app, :webhook_secret)
|
|
223
|
+
#
|
|
218
224
|
# Or an OAuth App, connected per owner with the callback URL
|
|
219
225
|
# <mount>/api/github_connection/callback (GITHUB_CLIENT_ID and
|
|
220
226
|
# GITHUB_CLIENT_SECRET when unset).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The GitHub App webhook deliveries the dashboard accepted, kept briefly so a
|
|
4
|
+
# redelivery of the same X-GitHub-Delivery is a no-op (see
|
|
5
|
+
# ActionAgent::GithubWebhookDelivery). Nothing here is a credential or a
|
|
6
|
+
# payload.
|
|
7
|
+
class CreateActiveAgentGithubWebhookDeliveries < ActiveRecord::Migration<%= migration_version %>
|
|
8
|
+
def change
|
|
9
|
+
prefix = ActionAgent.table_name_prefix
|
|
10
|
+
|
|
11
|
+
create_table "#{prefix}github_webhook_deliveries" do |t|
|
|
12
|
+
# GitHub's X-GitHub-Delivery, a GUID. Unique: a second insert of the
|
|
13
|
+
# same delivery is how a redelivery is recognized.
|
|
14
|
+
t.string :delivery_id, null: false, limit: 64
|
|
15
|
+
t.string :event, null: false, limit: 64
|
|
16
|
+
t.string :event_action, limit: 64
|
|
17
|
+
t.bigint :installation_id
|
|
18
|
+
t.datetime :created_at, null: false
|
|
19
|
+
t.index :delivery_id, unique: true, name: "index_#{prefix}webhook_deliveries_on_delivery"
|
|
20
|
+
t.index :created_at, name: "index_#{prefix}webhook_deliveries_on_created_at"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: actionagent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Bowen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activeagent
|
|
@@ -142,6 +142,7 @@ files:
|
|
|
142
142
|
- app/controllers/action_agent/api/feedback_ingest_controller.rb
|
|
143
143
|
- app/controllers/action_agent/api/feedbacks_controller.rb
|
|
144
144
|
- app/controllers/action_agent/api/github_app_manifests_controller.rb
|
|
145
|
+
- app/controllers/action_agent/api/github_app_webhooks_controller.rb
|
|
145
146
|
- app/controllers/action_agent/api/github_connections_controller.rb
|
|
146
147
|
- app/controllers/action_agent/api/github_installations_controller.rb
|
|
147
148
|
- app/controllers/action_agent/api/input_requests_controller.rb
|
|
@@ -194,6 +195,7 @@ files:
|
|
|
194
195
|
- app/jobs/action_agent/evaluation_replay_job.rb
|
|
195
196
|
- app/jobs/action_agent/evaluation_run_job.rb
|
|
196
197
|
- app/jobs/action_agent/exploration_job.rb
|
|
198
|
+
- app/jobs/action_agent/github_webhook_job.rb
|
|
197
199
|
- app/jobs/action_agent/input_request_expiry_job.rb
|
|
198
200
|
- app/jobs/action_agent/process_telemetry_traces_job.rb
|
|
199
201
|
- app/jobs/action_agent/project_evaluation_job.rb
|
|
@@ -224,6 +226,7 @@ files:
|
|
|
224
226
|
- app/models/action_agent/exploration.rb
|
|
225
227
|
- app/models/action_agent/github_connection.rb
|
|
226
228
|
- app/models/action_agent/github_installation.rb
|
|
229
|
+
- app/models/action_agent/github_webhook_delivery.rb
|
|
227
230
|
- app/models/action_agent/input_request.rb
|
|
228
231
|
- app/models/action_agent/interaction_feedback.rb
|
|
229
232
|
- app/models/action_agent/model_pricing.rb
|
|
@@ -248,6 +251,7 @@ files:
|
|
|
248
251
|
- app/queries/action_agent/metrics_report.rb
|
|
249
252
|
- app/queries/action_agent/trace_window_report.rb
|
|
250
253
|
- app/serializers/action_agent/agent_message_serializer.rb
|
|
254
|
+
- app/serializers/action_agent/agent_version_serializer.rb
|
|
251
255
|
- app/serializers/action_agent/code_conversation_serializer.rb
|
|
252
256
|
- app/serializers/action_agent/evaluation_serializer.rb
|
|
253
257
|
- app/serializers/action_agent/input_request_serializer.rb
|
|
@@ -372,6 +376,7 @@ files:
|
|
|
372
376
|
- lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb
|
|
373
377
|
- lib/generators/action_agent/templates/migrations/027_create_active_agent_interaction_feedbacks.rb.erb
|
|
374
378
|
- lib/generators/action_agent/templates/migrations/028_add_conversations_to_code_sessions.rb.erb
|
|
379
|
+
- lib/generators/action_agent/templates/migrations/029_create_active_agent_github_webhook_deliveries.rb.erb
|
|
375
380
|
- lib/tasks/action_agent.rake
|
|
376
381
|
- lib/tasks/browser.rake
|
|
377
382
|
- lib/tasks/claude_code.rake
|