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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/action_agent.css +1 -1
  3. data/app/assets/builds/action_agent.js +76 -73
  4. data/app/assets/builds/action_agent_recorder.js +78 -0
  5. data/app/assets/builds/action_agent_replay.js +226 -0
  6. data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
  7. data/app/controllers/action_agent/api/agents_controller.rb +1 -0
  8. data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
  9. data/app/controllers/action_agent/api/base_controller.rb +63 -2
  10. data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
  11. data/app/controllers/action_agent/api/code_sessions_controller.rb +53 -6
  12. data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
  13. data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
  14. data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
  15. data/app/controllers/action_agent/api/evaluations_controller.rb +9 -4
  16. data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
  17. data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
  18. data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
  19. data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
  20. data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
  21. data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
  22. data/app/controllers/action_agent/api/members_controller.rb +36 -0
  23. data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
  24. data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
  25. data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
  26. data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
  27. data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
  28. data/app/controllers/action_agent/api/projects_controller.rb +512 -0
  29. data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
  30. data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
  31. data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
  32. data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
  33. data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
  34. data/app/controllers/action_agent/api/sandboxes_controller.rb +140 -8
  35. data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
  36. data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
  37. data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
  38. data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
  39. data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
  40. data/app/controllers/action_agent/dashboard_controller.rb +17 -6
  41. data/app/controllers/action_agent/session_player_controller.rb +58 -0
  42. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
  43. data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +623 -18
  44. data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
  45. data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
  46. data/app/jobs/action_agent/agent_execution_job.rb +9 -39
  47. data/app/jobs/action_agent/agent_resume_job.rb +84 -0
  48. data/app/jobs/action_agent/code_session_job.rb +26 -2
  49. data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
  50. data/app/jobs/action_agent/exploration_job.rb +80 -0
  51. data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
  52. data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
  53. data/app/jobs/action_agent/project_setup_job.rb +21 -0
  54. data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
  55. data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
  56. data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
  57. data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
  58. data/app/models/action_agent/agent.rb +39 -26
  59. data/app/models/action_agent/agent_run.rb +134 -23
  60. data/app/models/action_agent/agent_template.rb +35 -1
  61. data/app/models/action_agent/api_key.rb +14 -0
  62. data/app/models/action_agent/catalog_scenario.rb +75 -0
  63. data/app/models/action_agent/code_session.rb +16 -4
  64. data/app/models/action_agent/draft_pull_request.rb +131 -0
  65. data/app/models/action_agent/evaluation.rb +84 -0
  66. data/app/models/action_agent/exploration.rb +847 -0
  67. data/app/models/action_agent/github_installation.rb +159 -0
  68. data/app/models/action_agent/input_request.rb +291 -0
  69. data/app/models/action_agent/project.rb +789 -0
  70. data/app/models/action_agent/project_secret.rb +339 -0
  71. data/app/models/action_agent/provider_key.rb +200 -3
  72. data/app/models/action_agent/recording_event.rb +145 -0
  73. data/app/models/action_agent/recording_snapshot.rb +7 -7
  74. data/app/models/action_agent/sandbox_session.rb +297 -14
  75. data/app/models/action_agent/scenario_catalog.rb +172 -0
  76. data/app/models/action_agent/scenario_product.rb +57 -0
  77. data/app/models/action_agent/scenario_set.rb +138 -0
  78. data/app/models/action_agent/session_recording.rb +121 -14
  79. data/app/models/action_agent/telemetry_trace.rb +74 -19
  80. data/app/models/concerns/action_agent/ownable.rb +84 -13
  81. data/app/serializers/action_agent/input_request_serializer.rb +47 -0
  82. data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
  83. data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
  84. data/app/services/action_agent/agent_execution_service.rb +450 -72
  85. data/app/services/action_agent/agent_sync.rb +5 -2
  86. data/app/services/action_agent/agent_tool_roster.rb +19 -4
  87. data/app/services/action_agent/agent_toolbox.rb +215 -9
  88. data/app/services/action_agent/browser_live_ticket.rb +78 -0
  89. data/app/services/action_agent/browser_sidecar.rb +213 -0
  90. data/app/services/action_agent/browser_sign_in.rb +163 -0
  91. data/app/services/action_agent/browser_tool_redaction.rb +92 -0
  92. data/app/services/action_agent/claude_code_auth.rb +70 -4
  93. data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
  94. data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
  95. data/app/services/action_agent/evaluation_fix.rb +101 -0
  96. data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
  97. data/app/services/action_agent/evaluation_runner_service.rb +4 -5
  98. data/app/services/action_agent/evaluation_standing.rb +15 -8
  99. data/app/services/action_agent/explorer_execution_service.rb +517 -0
  100. data/app/services/action_agent/github_client.rb +405 -64
  101. data/app/services/action_agent/live_updates.rb +31 -0
  102. data/app/services/action_agent/local_sandbox_backend.rb +1218 -83
  103. data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
  104. data/app/services/action_agent/mcp_client.rb +6 -2
  105. data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
  106. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
  107. data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
  108. data/app/services/action_agent/playwright_mcp_client.rb +7 -0
  109. data/app/services/action_agent/project_boot_fix.rb +309 -0
  110. data/app/services/action_agent/project_capabilities.rb +200 -0
  111. data/app/services/action_agent/project_install_pull_request.rb +225 -0
  112. data/app/services/action_agent/project_preflight.rb +178 -0
  113. data/app/services/action_agent/project_secret_discovery.rb +142 -0
  114. data/app/services/action_agent/project_setup.rb +429 -0
  115. data/app/services/action_agent/provider_credentials.rb +102 -0
  116. data/app/services/action_agent/recording_event_ingest.rb +181 -0
  117. data/app/services/action_agent/runtime_tool_roster.rb +52 -0
  118. data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
  119. data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
  120. data/app/services/action_agent/sandbox_browser.rb +299 -0
  121. data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
  122. data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
  123. data/app/services/action_agent/sandbox_mail.rb +103 -0
  124. data/app/services/action_agent/sandbox_manifest.rb +166 -13
  125. data/app/services/action_agent/sandbox_orchestrator.rb +276 -22
  126. data/app/services/action_agent/sandbox_patch.rb +253 -0
  127. data/app/services/action_agent/scenario_catalog_import.rb +255 -0
  128. data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
  129. data/app/services/action_agent/scenario_evaluation_runner.rb +119 -16
  130. data/app/services/action_agent/secret_scrubber.rb +33 -4
  131. data/app/services/action_agent/session_index.rb +270 -0
  132. data/app/services/action_agent/session_recording_service.rb +11 -0
  133. data/app/services/action_agent/session_timeline.rb +505 -0
  134. data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
  135. data/app/views/action_agent/session_player/show.html.erb +19 -0
  136. data/config/routes.rb +152 -1
  137. data/lib/action_agent/claude_login_process.rb +107 -0
  138. data/lib/action_agent/engine.rb +55 -5
  139. data/lib/action_agent/secret_requests.rb +80 -0
  140. data/lib/action_agent/version.rb +1 -1
  141. data/lib/action_agent.rb +507 -22
  142. data/lib/generators/action_agent/install_generator.rb +37 -0
  143. data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -0
  144. data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
  145. data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
  146. data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
  147. data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
  148. data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
  149. data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
  150. data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
  151. data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
  152. data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
  153. data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
  154. data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
  155. data/lib/tasks/action_agent.rake +33 -0
  156. data/lib/tasks/browser.rake +22 -0
  157. data/lib/tasks/sandbox.rake +1 -1
  158. data/lib/tasks/scenario_catalogs.rake +71 -0
  159. metadata +96 -2
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Used to store a batch of events a browser posted to a session recording.
5
+ #
6
+ # A batch is a JSON object:
7
+ #
8
+ # {
9
+ # "sent_at": 1767225600000,
10
+ # "recording_events": [
11
+ # { "kind": "rrweb", "timestamp": 1767225599500, "data": { "type": 3, "data": {} } },
12
+ # { "kind": "console", "timestamp": 1767225599800, "data": { "level": "error", "message": "boom" } }
13
+ # ]
14
+ # }
15
+ #
16
+ # `sent_at` is the client's clock when it sent the batch, and `timestamp`
17
+ # the client's clock when the event happened, both in epoch milliseconds
18
+ # (`sent_at` may also be ISO 8601). The batch's clock offset, receive time
19
+ # minus `sent_at`, is added to every timestamp, so a batch from a client
20
+ # whose clock is wrong is still stored in server time. An event without a
21
+ # timestamp is placed at `sent_at`. A timestamp more than EVENT_WINDOW_BEFORE
22
+ # before `sent_at`, or more than EVENT_WINDOW_AFTER after it, refuses the
23
+ # batch.
24
+ #
25
+ # The batch is stored whole or not at all. RecordingEvent.limits caps it,
26
+ # and caps the recording's totals. A refused batch over a cap is counted
27
+ # in the recording's dropped_event_count.
28
+ #
29
+ # Given +secrets+, every string in the batch's events has each of them
30
+ # masked (SecretScrubber) before it is stored.
31
+ class RecordingEventIngest
32
+ # The outcome as an HTTP response: +status+ and the JSON +body+.
33
+ Result = Struct.new(:status, :body, keyword_init: true)
34
+
35
+ PAYLOAD_TOO_LARGE = 413
36
+
37
+ # The key a batch carries its events under. The engine's filter_parameters
38
+ # initializer keeps it out of request logs, so it is a name no host
39
+ # parameter is likely to share.
40
+ BATCH_KEY = "recording_events"
41
+
42
+ EVENT_WINDOW_BEFORE = 24.hours
43
+ EVENT_WINDOW_AFTER = 1.minute
44
+ # 1970 through the end of 9999, in epoch milliseconds: what every
45
+ # supported database can store.
46
+ SENT_AT_RANGE = (0..253_402_300_799_999).freeze
47
+
48
+ # @param recording [SessionRecording] the recording the batch was posted to
49
+ # @param body [String] the request body
50
+ # @param kinds [Array<String>] the event kinds the caller may write
51
+ # @param secrets [Array<String>] values to mask in the events
52
+ # @return [Result]
53
+ def self.call(recording, body, received_at: Time.current, kinds: RecordingEvent::CLIENT_KINDS, secrets: [])
54
+ new(recording, body, received_at: received_at, kinds: kinds, secrets: secrets).call
55
+ end
56
+
57
+ def initialize(recording, body, received_at:, kinds:, secrets: [])
58
+ @recording = recording
59
+ @body = body.to_s
60
+ @received_at = received_at
61
+ @kinds = kinds.map(&:to_s)
62
+ @secrets = secrets
63
+ end
64
+
65
+ def call
66
+ batch = parse_body
67
+ return invalid("The batch nests deeper than #{RecordingEvent::MAX_NESTING} levels") if batch == :too_deep
68
+
69
+ events = batch.is_a?(Hash) ? batch[BATCH_KEY] : nil
70
+ return invalid("The body must be a JSON object with a non-empty #{BATCH_KEY} array") unless events.is_a?(Array) && events.any?
71
+
72
+ limits = RecordingEvent.limits
73
+ if events.size > limits[:batch_events] || @body.bytesize > limits[:batch_bytes]
74
+ return too_large(events.size, "The batch is over the limit of #{limits[:batch_events]} events or #{limits[:batch_bytes]} bytes")
75
+ end
76
+
77
+ sent_at = epoch_milliseconds(batch["sent_at"])
78
+ return invalid("sent_at must be the client's send time, in epoch milliseconds or ISO 8601") unless sent_at
79
+
80
+ problem = event_problem(events, sent_at)
81
+ return invalid(problem) if problem
82
+
83
+ offset = RecordingEvent.milliseconds(@received_at) - sent_at
84
+ store(build_rows(SecretScrubber.scrub(events, @secrets), sent_at, offset), offset, limits)
85
+ end
86
+
87
+ private
88
+
89
+ # The parsed body, :too_deep when it nests past RecordingEvent::MAX_NESTING,
90
+ # or nil when it is not JSON.
91
+ def parse_body
92
+ RecordingEvent.parse_json(@body)
93
+ rescue JSON::NestingError
94
+ :too_deep
95
+ rescue JSON::ParserError
96
+ nil
97
+ end
98
+
99
+ # Why +events+ cannot be stored, or nil when they can.
100
+ def event_problem(events, sent_at)
101
+ return "Every event must be a JSON object" unless events.all?(Hash)
102
+
103
+ refused = events.map { |event| event["kind"].to_s.presence || "(missing)" }.uniq - @kinds
104
+ return "Event kinds not accepted here: #{refused.join(', ')}. Accepted: #{@kinds.join(', ')}" if refused.any?
105
+
106
+ return "An event timestamp must be epoch milliseconds" unless events.all? { |event| event["timestamp"].nil? || event["timestamp"].is_a?(Numeric) }
107
+
108
+ window = (sent_at - EVENT_WINDOW_BEFORE.in_milliseconds)..(sent_at + EVENT_WINDOW_AFTER.in_milliseconds)
109
+ unless events.all? { |event| event["timestamp"].nil? || window.cover?(event["timestamp"]) }
110
+ return "An event timestamp must be within #{EVENT_WINDOW_BEFORE.inspect} before sent_at and #{EVENT_WINDOW_AFTER.inspect} after it"
111
+ end
112
+
113
+ nil
114
+ end
115
+
116
+ # One unsaved row per kind, holding that kind's events in batch order.
117
+ def build_rows(events, sent_at, offset)
118
+ grouped = {}
119
+ events.each_with_index do |event, index|
120
+ group = (grouped[event["kind"].to_s] ||= { index: index, events: [] })
121
+ client_time = event["timestamp"] || sent_at
122
+ group[:events] << { "at" => client_time.floor + offset, "data" => event["data"] }
123
+ end
124
+
125
+ grouped.map do |kind, group|
126
+ row = RecordingEvent.new(session_recording: @recording, kind: kind, batch_index: group[:index], clock_offset_ms: offset)
127
+ row.events = group[:events]
128
+ row
129
+ end
130
+ end
131
+
132
+ def store(rows, offset, limits)
133
+ count = rows.sum(&:event_count)
134
+ bytes = rows.sum(&:byte_size)
135
+ outcome = nil
136
+
137
+ @recording.with_lock do
138
+ outcome =
139
+ if !@recording.recording?
140
+ :finished
141
+ elsif @recording.event_count + count > limits[:recording_events] || @recording.event_bytes + bytes > limits[:recording_bytes]
142
+ :over_cap
143
+ else
144
+ rows.each(&:save!)
145
+ @recording.update_columns(event_count: @recording.event_count + count, event_bytes: @recording.event_bytes + bytes)
146
+ :stored
147
+ end
148
+ end
149
+
150
+ case outcome
151
+ when :finished then invalid("Recording already completed")
152
+ when :over_cap
153
+ too_large(count, "The recording is over the limit of #{limits[:recording_events]} events or #{limits[:recording_bytes]} bytes")
154
+ else
155
+ Result.new(status: :created, body: { stored: count, clock_offset_ms: offset })
156
+ end
157
+ end
158
+
159
+ # +value+ in epoch milliseconds, or nil when it is not a time in
160
+ # SENT_AT_RANGE.
161
+ def epoch_milliseconds(value)
162
+ milliseconds =
163
+ case value
164
+ when Numeric then value.floor if value.finite?
165
+ when String then RecordingEvent.milliseconds(Time.iso8601(value))
166
+ end
167
+ milliseconds if SENT_AT_RANGE.cover?(milliseconds)
168
+ rescue ArgumentError
169
+ nil
170
+ end
171
+
172
+ def invalid(message)
173
+ Result.new(status: :unprocessable_entity, body: { error: message })
174
+ end
175
+
176
+ def too_large(dropped, message)
177
+ SessionRecording.update_counters(@recording.id, dropped_event_count: dropped)
178
+ Result.new(status: PAYLOAD_TOO_LARGE, body: { error: message, code: "recording_limit", dropped: dropped })
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Used to list the tools an agent can call in a run: the engine's toolbox
5
+ # tools its saved +tools+ name, and whatever its MCP servers and the run's
6
+ # extra checkout sandboxes and browsers serve (MCPToolDispatcher). When a
7
+ # sandbox's browser is attached, the toolbox's own browser tools drop. A scenario run's
8
+ # diagnosis and an exploration's answerability verdicts read the same list.
9
+ #
10
+ # AgentToolRoster is a different list: the agent editor's view of every
11
+ # tool and service the agent could be given, with usage.
12
+ class RuntimeToolRoster
13
+ attr_reader :agent, :dispatcher
14
+
15
+ # @param agent [Agent]
16
+ # @param extra_server_keys [Array<String>] "sandbox:<session_id>" and
17
+ # "browser:<session_id>" keys the run reaches beside the agent's own
18
+ # servers
19
+ def initialize(agent, extra_server_keys: [])
20
+ @agent = agent
21
+ @dispatcher = MCPToolDispatcher.new(agent, extra_server_keys: extra_server_keys)
22
+ end
23
+
24
+ # Returns { name => description } for every tool the agent can call. A
25
+ # server that fails discovery contributes nothing and is named in
26
+ # #discovery_errors.
27
+ #
28
+ # @raise [MCPToolDispatcher::SandboxUnavailable] when an extra sandbox is
29
+ # not live or does not answer discovery
30
+ # @return [Hash{String => String}]
31
+ def tools
32
+ @tools ||= (dispatcher.tool_definitions +
33
+ AgentToolbox.definitions_for(agent.tools, browser_attached: dispatcher.browser_attached?))
34
+ .to_h { |definition| [ definition[:name].to_s, definition[:description].to_s ] }
35
+ end
36
+
37
+ # Why each of the agent's own servers contributed no tools, keyed by
38
+ # server; empty when every one answered.
39
+ #
40
+ # @return [Hash{String => String}]
41
+ def discovery_errors
42
+ tools
43
+ dispatcher.discovery_errors
44
+ end
45
+
46
+ # Whether every server the agent declares failed discovery.
47
+ def all_servers_failed?
48
+ tools
49
+ dispatcher.all_servers_failed?
50
+ end
51
+ end
52
+ end