ask-rails-harness 0.3.0 → 0.4.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/CHANGELOG.md +23 -0
- data/lib/ask/rails/harness/railtie.rb +1 -0
- data/lib/ask/rails/harness/tool.rb +4 -33
- data/lib/ask/rails/harness/tools.rb +24 -0
- data/lib/ask/rails/harness/version.rb +1 -1
- data/lib/ask/rails/harness.rb +13 -139
- metadata +3 -41
- data/lib/ask/rails/harness/audit_log.rb +0 -185
- data/lib/ask/rails/harness/configuration.rb +0 -73
- data/lib/ask/rails/harness/environment_permissions.rb +0 -41
- data/lib/ask/rails/harness/minitest_json_reporter.rb +0 -83
- data/lib/ask/rails/harness/tools/query_database.rb +0 -75
- data/lib/ask/rails/harness/tools/read_log.rb +0 -122
- data/lib/ask/rails/harness/tools/read_model.rb +0 -75
- data/lib/ask/rails/harness/tools/run_command.rb +0 -60
- data/lib/ask/rails/harness/tools/run_tests.rb +0 -249
- data/lib/ask/rails/harness/tools/schema_graph.rb +0 -176
- data/lib/minitest/ask_rails_harness_plugin.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3138db7c116e0f0742a091cad7c5ecfdbe8263de2b27c892830e56d32e259691
|
|
4
|
+
data.tar.gz: b0a87afaed52e3d66313fea3c1d1536d19934876e1e52dd6503b4e3ce112aaed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e3e71b34c262d91350dd2ccd213bdbca3915d75302307dbc3e94b89b1e803b98d0568ef7ea5b94b6a197b0859b53276f337a7d4885d8cf496cddf9f5ea65904
|
|
7
|
+
data.tar.gz: a6db27db6dfc1b050c45aa5bea45d6c1862937e45998f265ccb5226a5a09df94b4f73d0be486468a421854c55b7483ff655ed554d80e880979b90f9996d122cf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## [0.4.0] — 2026-08-10
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Generic tools moved to the new `ask-ruby-harness` gem** — `RunCommand`,
|
|
6
|
+
`QueryDatabase`, `ReadModel`, `ReadLog`, `SchemaGraph`, and `RunTests` now
|
|
7
|
+
live in the language-agnostic harness (`Ask::Ruby::Harness`), which this
|
|
8
|
+
gem depends on. This gem keeps the Rails-native surface: the Engine
|
|
9
|
+
(mount at /ask), railtie, generators, auth, persistence, and
|
|
10
|
+
`RouteInspector`.
|
|
11
|
+
- **Backward-compatible aliases** — `Ask::Rails::Harness::Tools::*`,
|
|
12
|
+
`Ask::Rails::Harness::AuditLog`, `Ask::Rails::Harness::Configuration`, and
|
|
13
|
+
`Ask::Rails::Harness::MinitestJsonReporter` still resolve (to the generic
|
|
14
|
+
implementations), and `Ask::Rails::Harness::Tool` subclasses the generic
|
|
15
|
+
base. `app_root` is pinned to `::Rails.root` by the railtie.
|
|
16
|
+
- **Audit notification renamed** — the generic audit log now fires
|
|
17
|
+
`audit_log.ask_ruby_harness` (was `audit_log.ask_rails_harness`).
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- `lib/minitest/ask_rails_harness_plugin.rb` — superseded by
|
|
22
|
+
`minitest/ask_ruby_harness_plugin.rb` in ask-ruby-harness.
|
|
23
|
+
|
|
1
24
|
## [0.3.0] — 2026-08-10
|
|
2
25
|
|
|
3
26
|
### Removed
|
|
@@ -17,6 +17,7 @@ module Ask
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
initializer "ask_rails_harness.configure" do |app|
|
|
20
|
+
Ask::Ruby::Harness.app_root = ::Rails.root
|
|
20
21
|
Ask::Rails::Harness.configuration.default_model ||= ENV["ASK_DEFAULT_MODEL"] || "gpt-4o"
|
|
21
22
|
Ask::Rails::Harness.configuration.max_turns ||= (ENV["ASK_MAX_TURNS"] || 25).to_i
|
|
22
23
|
end
|
|
@@ -3,39 +3,10 @@
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Rails
|
|
5
5
|
module Harness
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Override call to add audit logging around every tool execution.
|
|
12
|
-
# Logs the intent (sanitized params) and outcome (status, timing),
|
|
13
|
-
# but not the returned data.
|
|
14
|
-
def call(args = {}, abort_controller = nil)
|
|
15
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
|
-
result = super
|
|
17
|
-
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round
|
|
18
|
-
|
|
19
|
-
AuditLog.log(
|
|
20
|
-
session_id: Thread.current[:ask_session_id],
|
|
21
|
-
tool_name: name,
|
|
22
|
-
params: args,
|
|
23
|
-
result: result,
|
|
24
|
-
duration_ms: duration_ms
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
result
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Allow the session to set its ID for audit log correlation.
|
|
31
|
-
# Called by the agent loop before executing a tool.
|
|
32
|
-
def self.session_id=(id)
|
|
33
|
-
Thread.current[:ask_session_id] = id
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.session_id
|
|
37
|
-
Thread.current[:ask_session_id]
|
|
38
|
-
end
|
|
6
|
+
# The Rails edition's tool base. app_root is pinned to ::Rails.root by
|
|
7
|
+
# the railtie; everything else (audit logging, session correlation)
|
|
8
|
+
# comes from the generic ask-ruby-harness base.
|
|
9
|
+
class Tool < Ask::Ruby::Harness::Tool
|
|
39
10
|
end
|
|
40
11
|
end
|
|
41
12
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Backward-compatible constant aliases: the generic tools moved to
|
|
4
|
+
# ask-ruby-harness (Ask::Ruby::Harness::Tools::*). Existing references to
|
|
5
|
+
# Ask::Rails::Harness::Tools::*, AuditLog, and Configuration keep working.
|
|
6
|
+
# (MinitestJsonReporter is intentionally not aliased — it lives behind the
|
|
7
|
+
# minitest plugin and must not load minitest at app boot.)
|
|
8
|
+
module Ask
|
|
9
|
+
module Rails
|
|
10
|
+
module Harness
|
|
11
|
+
Configuration = Ask::Ruby::Harness::Configuration
|
|
12
|
+
AuditLog = Ask::Ruby::Harness::AuditLog
|
|
13
|
+
|
|
14
|
+
module Tools
|
|
15
|
+
RunCommand = Ask::Ruby::Harness::Tools::RunCommand
|
|
16
|
+
QueryDatabase = Ask::Ruby::Harness::Tools::QueryDatabase
|
|
17
|
+
ReadModel = Ask::Ruby::Harness::Tools::ReadModel
|
|
18
|
+
ReadLog = Ask::Ruby::Harness::Tools::ReadLog
|
|
19
|
+
SchemaGraph = Ask::Ruby::Harness::Tools::SchemaGraph
|
|
20
|
+
RunTests = Ask::Ruby::Harness::Tools::RunTests
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/ask/rails/harness.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails"
|
|
4
|
-
require "ask/
|
|
4
|
+
require "ask/ruby/harness"
|
|
5
5
|
require "ask/auth"
|
|
6
6
|
require "time"
|
|
7
7
|
|
|
@@ -9,148 +9,31 @@ module Ask
|
|
|
9
9
|
module Rails
|
|
10
10
|
module Harness
|
|
11
11
|
class << self
|
|
12
|
+
# The Rails edition shares the generic configuration — the generic
|
|
13
|
+
# gem owns the runtime, this gem adapts it to Rails.
|
|
12
14
|
def configure
|
|
13
15
|
yield configuration
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def configuration
|
|
17
|
-
|
|
19
|
+
Ask::Ruby::Harness.configuration
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def agent_session(**extra)
|
|
21
|
-
|
|
22
|
-
cleanup! if configuration.max_session_age || configuration.max_sessions
|
|
23
|
-
|
|
24
|
-
tools = configuration.tools.map { |t| t.is_a?(Class) ? t.new : t }
|
|
25
|
-
prompt = extra.delete(:system_prompt) || configuration.system_prompt || default_system_prompt
|
|
26
|
-
|
|
27
|
-
# Resolve environment-specific permissions and wire into agent hooks
|
|
28
|
-
hooks = build_environment_hooks
|
|
29
|
-
|
|
30
|
-
Ask::Agent::Session.new(
|
|
31
|
-
model: configuration.default_model,
|
|
32
|
-
max_turns: configuration.max_turns,
|
|
33
|
-
system_prompt: prompt,
|
|
34
|
-
tools: tools,
|
|
35
|
-
state: configuration.persistence_adapter,
|
|
36
|
-
hooks: hooks,
|
|
37
|
-
**extra
|
|
38
|
-
)
|
|
23
|
+
Ask::Ruby::Harness.agent_session(**extra)
|
|
39
24
|
end
|
|
40
25
|
|
|
41
26
|
def discover_tools!
|
|
42
|
-
|
|
27
|
+
Ask::Ruby::Harness.discover_tools!
|
|
43
28
|
end
|
|
44
29
|
|
|
45
|
-
# Prune old sessions and audit logs based on configuration limits.
|
|
46
|
-
#
|
|
47
|
-
# Removes sessions older than +max_session_age+ seconds, and limits the
|
|
48
|
-
# total number of sessions to +max_sessions+ (deleting the oldest first).
|
|
49
|
-
# Audit log entries older than the oldest kept session are also removed.
|
|
50
|
-
#
|
|
51
|
-
# Call manually via rake task or cron, or configure limits to auto-prune
|
|
52
|
-
# on agent_session creation.
|
|
53
30
|
def cleanup!
|
|
54
|
-
|
|
55
|
-
limit_session_count
|
|
31
|
+
Ask::Ruby::Harness.cleanup!
|
|
56
32
|
end
|
|
57
33
|
|
|
58
34
|
def root
|
|
59
35
|
@root ||= Pathname.new(File.expand_path("../..", __dir__))
|
|
60
36
|
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def build_environment_hooks
|
|
65
|
-
env_mode = configuration.effective_mode
|
|
66
|
-
return {} unless env_mode
|
|
67
|
-
|
|
68
|
-
perms = Ask::Agent::Policies::Permissions.new(mode: env_mode)
|
|
69
|
-
{ before_tool: [perms.method(:before_tool_call)] }
|
|
70
|
-
rescue ArgumentError => e
|
|
71
|
-
warn "[ask-rails-harness] Invalid environment mode: #{e.message}"
|
|
72
|
-
{}
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def prune_old_sessions
|
|
76
|
-
age = configuration.max_session_age
|
|
77
|
-
return unless age&.> 0
|
|
78
|
-
return unless persistence_available?
|
|
79
|
-
|
|
80
|
-
cutoff = age.seconds.ago
|
|
81
|
-
count = 0
|
|
82
|
-
|
|
83
|
-
configuration.persistence_adapter.list.each do |id|
|
|
84
|
-
data = configuration.persistence_adapter.load(id)
|
|
85
|
-
created = data&.dig(:metadata, :created_at)
|
|
86
|
-
if created && Time.parse(created) < cutoff
|
|
87
|
-
configuration.persistence_adapter.delete(id)
|
|
88
|
-
count += 1
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
count
|
|
93
|
-
rescue StandardError
|
|
94
|
-
nil
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def limit_session_count
|
|
98
|
-
max = configuration.max_sessions
|
|
99
|
-
return unless max&.> 0
|
|
100
|
-
return unless persistence_available?
|
|
101
|
-
|
|
102
|
-
sessions = configuration.persistence_adapter.list
|
|
103
|
-
excess = sessions.size - max
|
|
104
|
-
return unless excess > 0
|
|
105
|
-
|
|
106
|
-
# Delete oldest sessions first
|
|
107
|
-
with_timestamps = sessions.map { |id|
|
|
108
|
-
data = configuration.persistence_adapter.load(id)
|
|
109
|
-
created = data&.dig(:metadata, :created_at)
|
|
110
|
-
[id, created ? Time.parse(created) : Time.at(0)]
|
|
111
|
-
}.sort_by(&:last)
|
|
112
|
-
|
|
113
|
-
with_timestamps.first(excess).each do |id, _|
|
|
114
|
-
configuration.persistence_adapter.delete(id)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
excess
|
|
118
|
-
rescue StandardError
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def persistence_available?
|
|
123
|
-
defined?(ActiveRecord::Base) &&
|
|
124
|
-
ActiveRecord::Base.connection.data_source_exists?("ask_sessions")
|
|
125
|
-
rescue StandardError
|
|
126
|
-
false
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def core_rails_tools
|
|
130
|
-
CORE_RAILS_TOOLS.map(&:new)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def discovered_user_tools
|
|
134
|
-
tools = []
|
|
135
|
-
files = Dir[::Rails.root.join("app", "tools", "*.rb")]
|
|
136
|
-
files.each do |f|
|
|
137
|
-
require f
|
|
138
|
-
klass = File.basename(f, ".rb").camelize.constantize rescue next
|
|
139
|
-
tools << klass if klass < Ask::Rails::Harness::Tool
|
|
140
|
-
end
|
|
141
|
-
tools
|
|
142
|
-
rescue
|
|
143
|
-
tools
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def default_system_prompt
|
|
147
|
-
<<~PROMPT
|
|
148
|
-
You are a Ruby on Rails software engineer.
|
|
149
|
-
You have direct access to the application's code, database, and runtime.
|
|
150
|
-
Use your tools to inspect and modify the codebase.
|
|
151
|
-
Once you have enough information, stop calling tools and give your answer.
|
|
152
|
-
PROMPT
|
|
153
|
-
end
|
|
154
37
|
end
|
|
155
38
|
end
|
|
156
39
|
end
|
|
@@ -158,31 +41,22 @@ end
|
|
|
158
41
|
|
|
159
42
|
require_relative "harness/version"
|
|
160
43
|
require_relative "harness/engine"
|
|
161
|
-
require_relative "harness/configuration"
|
|
162
|
-
require_relative "harness/audit_log"
|
|
163
|
-
require_relative "harness/environment_permissions"
|
|
164
44
|
require_relative "harness/auth"
|
|
165
45
|
require_relative "harness/persistence"
|
|
166
46
|
require_relative "harness/service_discovery"
|
|
167
47
|
require_relative "harness/tool"
|
|
168
|
-
require_relative "harness/tools
|
|
169
|
-
require_relative "harness/tools/query_database"
|
|
170
|
-
require_relative "harness/tools/read_model"
|
|
171
|
-
require_relative "harness/tools/read_log"
|
|
172
|
-
require_relative "harness/tools/schema_graph"
|
|
48
|
+
require_relative "harness/tools" # backward-compat constant aliases
|
|
173
49
|
require_relative "harness/tools/route_inspector"
|
|
174
|
-
require_relative "harness/tools/run_tests"
|
|
175
|
-
require_relative "harness/minitest_json_reporter"
|
|
176
50
|
|
|
177
51
|
# Railtie is loaded only when Rails is fully available
|
|
178
52
|
if defined?(::Rails::Railtie)
|
|
179
53
|
require_relative "harness/railtie"
|
|
180
54
|
end
|
|
181
55
|
|
|
182
|
-
# Define after all tool files are loaded so the constants resolve
|
|
56
|
+
# Define after all tool files are loaded so the constants resolve. The
|
|
57
|
+
# generic Rails-aware tools come from ask-ruby-harness; this gem adds the
|
|
58
|
+
# Rails-native ones (RouteInspector) on top.
|
|
183
59
|
Ask::Rails::Harness::CORE_RAILS_TOOLS = [
|
|
184
|
-
Ask::
|
|
185
|
-
Ask::Rails::Harness::Tools::
|
|
186
|
-
Ask::Rails::Harness::Tools::ReadLog, Ask::Rails::Harness::Tools::SchemaGraph,
|
|
187
|
-
Ask::Rails::Harness::Tools::RouteInspector, Ask::Rails::Harness::Tools::RunTests
|
|
60
|
+
*Ask::Ruby::Harness::HARNESS_TOOLS,
|
|
61
|
+
Ask::Rails::Harness::Tools::RouteInspector
|
|
188
62
|
].freeze
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-rails-harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -24,7 +24,7 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: ask-
|
|
27
|
+
name: ask-ruby-harness
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
@@ -37,34 +37,6 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0.1'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: ask-tools-shell
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.1'
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.1'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: ask-agent
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - ">="
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.28.0
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - ">="
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.28.0
|
|
68
40
|
- !ruby/object:Gem::Dependency
|
|
69
41
|
name: ask-auth
|
|
70
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -155,23 +127,14 @@ files:
|
|
|
155
127
|
- config/routes.rb
|
|
156
128
|
- lib/ask-rails-harness.rb
|
|
157
129
|
- lib/ask/rails/harness.rb
|
|
158
|
-
- lib/ask/rails/harness/audit_log.rb
|
|
159
130
|
- lib/ask/rails/harness/auth.rb
|
|
160
|
-
- lib/ask/rails/harness/configuration.rb
|
|
161
131
|
- lib/ask/rails/harness/engine.rb
|
|
162
|
-
- lib/ask/rails/harness/environment_permissions.rb
|
|
163
|
-
- lib/ask/rails/harness/minitest_json_reporter.rb
|
|
164
132
|
- lib/ask/rails/harness/persistence.rb
|
|
165
133
|
- lib/ask/rails/harness/railtie.rb
|
|
166
134
|
- lib/ask/rails/harness/service_discovery.rb
|
|
167
135
|
- lib/ask/rails/harness/tool.rb
|
|
168
|
-
- lib/ask/rails/harness/tools
|
|
169
|
-
- lib/ask/rails/harness/tools/read_log.rb
|
|
170
|
-
- lib/ask/rails/harness/tools/read_model.rb
|
|
136
|
+
- lib/ask/rails/harness/tools.rb
|
|
171
137
|
- lib/ask/rails/harness/tools/route_inspector.rb
|
|
172
|
-
- lib/ask/rails/harness/tools/run_command.rb
|
|
173
|
-
- lib/ask/rails/harness/tools/run_tests.rb
|
|
174
|
-
- lib/ask/rails/harness/tools/schema_graph.rb
|
|
175
138
|
- lib/ask/rails/harness/version.rb
|
|
176
139
|
- lib/ask/skills/rails.db_debug/SKILL.md
|
|
177
140
|
- lib/ask/skills/rails.deploy_pipeline/SKILL.md
|
|
@@ -180,7 +143,6 @@ files:
|
|
|
180
143
|
- lib/generators/ask/rails/harness/install/templates/audit_log_migration.rb
|
|
181
144
|
- lib/generators/ask/rails/harness/install/templates/initializer.rb
|
|
182
145
|
- lib/generators/ask/rails/harness/install/templates/migration.rb
|
|
183
|
-
- lib/minitest/ask_rails_harness_plugin.rb
|
|
184
146
|
homepage: https://github.com/ask-rb/ask-rails-harness
|
|
185
147
|
licenses:
|
|
186
148
|
- MIT
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module Ask
|
|
6
|
-
module Rails
|
|
7
|
-
module Harness
|
|
8
|
-
# Append-only audit log for tool executions.
|
|
9
|
-
#
|
|
10
|
-
# Every tool call made by an agent is recorded in the +ask_audit_logs+
|
|
11
|
-
# table with the intent (sanitized params) and outcome (status, timing),
|
|
12
|
-
# but not the data returned. This gives a trustworthy, queryable record
|
|
13
|
-
# of what the agent did without becoming a PII liability.
|
|
14
|
-
#
|
|
15
|
-
# Sensitive param values (keys matching +password+, +secret+, +token+,
|
|
16
|
-
# +api_key+, +key+) are automatically redacted before logging.
|
|
17
|
-
module AuditLog
|
|
18
|
-
SENSITIVE_KEYS = /\A(password|secret|token|api_key|key|auth_token|access_token)\z/i
|
|
19
|
-
|
|
20
|
-
class << self
|
|
21
|
-
# Log a tool execution event.
|
|
22
|
-
#
|
|
23
|
-
# @param session_id [String] The agent session that triggered this call
|
|
24
|
-
# @param tool_name [String] Name of the tool that ran
|
|
25
|
-
# @param params [Hash] The parameters passed to the tool (sanitized automatically)
|
|
26
|
-
# @param result [Ask::Result, Hash, nil] The result returned by the tool
|
|
27
|
-
# @param error [StandardError, nil] The exception if the tool raised
|
|
28
|
-
# @param duration_ms [Integer] Wall-clock time for the tool execution
|
|
29
|
-
# @param user_context [Hash, nil] Who initiated the session (from config)
|
|
30
|
-
def log(session_id:, tool_name:, params:, result: nil, error: nil, duration_ms:)
|
|
31
|
-
now = Time.now.utc
|
|
32
|
-
entry = {
|
|
33
|
-
session_id: session_id,
|
|
34
|
-
tool_name: tool_name,
|
|
35
|
-
params: sanitize_params(params),
|
|
36
|
-
result_summary: build_summary(tool_name, result, error),
|
|
37
|
-
status: determine_status(result, error),
|
|
38
|
-
error_message: determine_error(result, error),
|
|
39
|
-
duration_ms: duration_ms,
|
|
40
|
-
user_context: resolve_user_context,
|
|
41
|
-
environment: environment_name,
|
|
42
|
-
recorded_at: now,
|
|
43
|
-
created_at: now,
|
|
44
|
-
updated_at: now
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if table_exists?
|
|
48
|
-
write_entry(entry)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Fire an ActiveSupport notification so host apps can subscribe
|
|
52
|
-
ActiveSupport::Notifications.instrument("audit_log.ask_rails_harness", entry)
|
|
53
|
-
|
|
54
|
-
entry
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def sanitize_params(params)
|
|
60
|
-
return {} unless params.is_a?(Hash)
|
|
61
|
-
|
|
62
|
-
params.each_with_object({}) do |(key, value), sanitized|
|
|
63
|
-
if SENSITIVE_KEYS.match?(key.to_s)
|
|
64
|
-
sanitized[key] = "[REDACTED]"
|
|
65
|
-
else
|
|
66
|
-
sanitized[key] = value
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def determine_status(result, error)
|
|
72
|
-
return "error" if error
|
|
73
|
-
return "rejected" if result.is_a?(Ask::Result) && (result.error? || result.blocked?)
|
|
74
|
-
"success"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def determine_error(result, error)
|
|
78
|
-
return error.message if error
|
|
79
|
-
if result.is_a?(Ask::Result)
|
|
80
|
-
return result.error.to_s if result.error?
|
|
81
|
-
return result.content.to_s if result.blocked?
|
|
82
|
-
end
|
|
83
|
-
nil
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def extract_data(result)
|
|
87
|
-
return nil unless result
|
|
88
|
-
|
|
89
|
-
if result.is_a?(Ask::Result)
|
|
90
|
-
result.content.is_a?(Hash) ? result.content : nil
|
|
91
|
-
elsif result.is_a?(Hash)
|
|
92
|
-
result
|
|
93
|
-
else
|
|
94
|
-
nil
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def build_summary(tool_name, result, error)
|
|
99
|
-
if error
|
|
100
|
-
return { error: error.class.name }
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
if result.is_a?(Ask::Result)
|
|
104
|
-
if result.error?
|
|
105
|
-
return { error: "rejected: #{result.error.to_s.truncate(200)}" }
|
|
106
|
-
end
|
|
107
|
-
if result.blocked?
|
|
108
|
-
return { error: "blocked: #{result.content.to_s.truncate(200)}" }
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
data = extract_data(result)
|
|
113
|
-
return {} unless data
|
|
114
|
-
|
|
115
|
-
summary = {}
|
|
116
|
-
summary[:rows] = data[:rows]&.length if data.key?(:rows)
|
|
117
|
-
summary[:columns] = data[:columns]&.length if data.key?(:columns)
|
|
118
|
-
summary[:exit_status] = data[:exit_status] if data.key?(:exit_status)
|
|
119
|
-
summary[:size] = data[:size] if data.key?(:size)
|
|
120
|
-
summary[:matched_lines] = data[:matched_lines] if data.key?(:matched_lines)
|
|
121
|
-
summary[:results] = data[:results]&.length if data.key?(:results)
|
|
122
|
-
summary[:model] = data[:name] if data.key?(:name)
|
|
123
|
-
summary
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def resolve_user_context
|
|
127
|
-
proc = Ask::Rails::Harness.configuration.current_user
|
|
128
|
-
return nil unless proc.respond_to?(:call)
|
|
129
|
-
|
|
130
|
-
result = proc.call
|
|
131
|
-
result.is_a?(Hash) ? result : nil
|
|
132
|
-
rescue StandardError
|
|
133
|
-
nil
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def environment_name
|
|
137
|
-
defined?(::Rails) && ::Rails.respond_to?(:env) ? ::Rails.env.to_s : "unknown"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def table_exists?
|
|
141
|
-
return false unless defined?(ActiveRecord::Base)
|
|
142
|
-
|
|
143
|
-
# Only cache the true result — recheck if it was false
|
|
144
|
-
return @table_exists if @table_exists
|
|
145
|
-
|
|
146
|
-
@table_exists = begin
|
|
147
|
-
conn = ActiveRecord::Base.connection
|
|
148
|
-
conn.data_source_exists?("ask_audit_logs")
|
|
149
|
-
rescue StandardError
|
|
150
|
-
false
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def write_entry(entry)
|
|
155
|
-
# Serialize JSON fields for database storage
|
|
156
|
-
serialized = entry.dup
|
|
157
|
-
%i[params result_summary user_context].each do |key|
|
|
158
|
-
serialized[key] = ::JSON.generate(serialized[key]) if serialized[key].is_a?(Hash)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Use raw SQL to avoid requiring a model class
|
|
162
|
-
columns = serialized.keys
|
|
163
|
-
values = columns.map { |col| ActiveRecord::Base.connection.quote(serialized[col]) }
|
|
164
|
-
ActiveRecord::Base.connection.execute(
|
|
165
|
-
"INSERT INTO ask_audit_logs (#{columns.join(', ')})
|
|
166
|
-
VALUES (#{values.join(', ')})"
|
|
167
|
-
)
|
|
168
|
-
rescue StandardError => e
|
|
169
|
-
# Silently fail — audit log should never crash the caller.
|
|
170
|
-
# ::Rails avoids the bare-`Rails` constant resolving to Ask::Rails;
|
|
171
|
-
# `&.` guards against Rails.logger returning nil (no app booted).
|
|
172
|
-
::Rails.logger&.warn("[ask-rails-harness] Audit log write failed: #{e.message}") if defined?(::Rails.logger)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Reset cached table check (useful in tests)
|
|
176
|
-
public
|
|
177
|
-
|
|
178
|
-
def reset_table_check!
|
|
179
|
-
@table_exists = nil
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ask
|
|
4
|
-
module Rails
|
|
5
|
-
module Harness
|
|
6
|
-
class Configuration
|
|
7
|
-
attr_accessor :default_model, :max_turns, :system_prompt,
|
|
8
|
-
:tool_concurrency, :persistence_adapter, :tools,
|
|
9
|
-
:current_user, :allowed_commands, :denied_commands,
|
|
10
|
-
:max_session_age, :max_sessions
|
|
11
|
-
|
|
12
|
-
# @return [Hash{Symbol => EnvironmentPermissions}] per-environment permission rules
|
|
13
|
-
attr_reader :environments
|
|
14
|
-
|
|
15
|
-
def initialize
|
|
16
|
-
@default_model = "gpt-4o"
|
|
17
|
-
@max_turns = 25
|
|
18
|
-
@system_prompt = nil
|
|
19
|
-
@tool_concurrency = 5
|
|
20
|
-
@persistence_adapter = nil
|
|
21
|
-
@tools = []
|
|
22
|
-
@current_user = nil
|
|
23
|
-
@allowed_commands = nil
|
|
24
|
-
@denied_commands = nil
|
|
25
|
-
@max_session_age = nil
|
|
26
|
-
@max_sessions = nil
|
|
27
|
-
@environments = {}
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Configure permissions for a specific Rails environment.
|
|
31
|
-
#
|
|
32
|
-
# config.environment :production do |env|
|
|
33
|
-
# env.mode = :read_only
|
|
34
|
-
# env.allowed_commands = [/^rails routes/]
|
|
35
|
-
# env.denied_commands = [/rm/, /dropdb/]
|
|
36
|
-
# end
|
|
37
|
-
#
|
|
38
|
-
# @param name [Symbol, String] environment name (:production, :development, :staging, etc.)
|
|
39
|
-
def environment(name)
|
|
40
|
-
env = EnvironmentPermissions.new
|
|
41
|
-
yield env
|
|
42
|
-
@environments[name.to_sym] = env
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Resolved allowed commands for the current Rails environment.
|
|
46
|
-
# Falls back to the global +allowed_commands+ if no per-env config.
|
|
47
|
-
#
|
|
48
|
-
# @return [Array<Regexp>, nil]
|
|
49
|
-
def effective_allowed_commands
|
|
50
|
-
env = @environments[::Rails.env.to_sym]
|
|
51
|
-
env&.allowed_commands || @allowed_commands
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Resolved denied commands for the current Rails environment.
|
|
55
|
-
# Falls back to the global +denied_commands+ if no per-env config.
|
|
56
|
-
#
|
|
57
|
-
# @return [Array<Regexp>, nil]
|
|
58
|
-
def effective_denied_commands
|
|
59
|
-
env = @environments[::Rails.env.to_sym]
|
|
60
|
-
env&.denied_commands || @denied_commands
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Resolved access mode for the current Rails environment.
|
|
64
|
-
#
|
|
65
|
-
# @return [Symbol, nil] +:full_access+, +:read_only+, +:ask_before_changes+, or nil
|
|
66
|
-
def effective_mode
|
|
67
|
-
env = @environments[::Rails.env.to_sym]
|
|
68
|
-
env&.mode || nil
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|