activeagent 1.5.2 → 1.6.2

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.
@@ -24,6 +24,17 @@ module ActiveAgent
24
24
  # @return [Boolean] Whether to store traces in the app's own database
25
25
  attr_reader :local_storage
26
26
 
27
+ # The deploy every trace is stamped with (`service.version`). Unset, it
28
+ # is whatever ActiveAgent::Release.revision resolves — a git SHA from
29
+ # the conventional deploy variables — so traces from two deploys of
30
+ # the same service can be told apart without any host configuration.
31
+ attr_writer :service_version
32
+
33
+ def service_version
34
+ value = @service_version.respond_to?(:call) ? @service_version.call : @service_version
35
+ (value.presence || ActiveAgent::Release.revision)&.to_s
36
+ end
37
+
27
38
  def initialize
28
39
  super
29
40
  # The framework predates the shared gem and has always been opt-in;
@@ -58,6 +58,15 @@ module ActiveAgent
58
58
  span.set_attribute("agent.action", action_name.to_s)
59
59
  span.set_attribute("agent.provider", provider_name)
60
60
  span.set_attribute("agent.model", model_name)
61
+ # Which release of the agent ran: the digest of what the model was
62
+ # given (ActiveAgent::Release), so a dashboard can pin this trace
63
+ # to the version it cut on deploy.
64
+ if self.class.respond_to?(:release_digest)
65
+ span.set_attribute("agent.version", self.class.release_digest)
66
+ if (revision = ActiveAgent::Release.revision).present?
67
+ span.set_attribute("agent.revision", revision)
68
+ end
69
+ end
61
70
 
62
71
  # Add prompt span, carrying the prompt contents (instructions +
63
72
  # outbound messages) so dashboards can show what was sent.
@@ -169,12 +169,15 @@ module ActiveAgent
169
169
 
170
170
  # Returns default attributes for all spans.
171
171
  def default_attributes
172
- {
172
+ attributes = {
173
173
  "service.name" => configuration.resolved_service_name,
174
174
  "service.environment" => configuration.resolved_environment,
175
175
  "telemetry.sdk.name" => "activeagent",
176
176
  "telemetry.sdk.version" => ActiveAgent::VERSION
177
177
  }
178
+ version = configuration.respond_to?(:service_version) ? configuration.service_version : nil
179
+ attributes["service.version"] = version if version.present?
180
+ attributes
178
181
  end
179
182
  end
180
183
  end
@@ -78,9 +78,31 @@ module ActiveAgent
78
78
  # end
79
79
  def configure
80
80
  yield configuration if block_given?
81
+ install_instrumentation! if enabled?
81
82
  configuration
82
83
  end
83
84
 
85
+ # Installs generation instrumentation on ActiveAgent::Base.
86
+ #
87
+ # The railtie also does this at boot, but only for configuration already
88
+ # loaded by then (activeagent.yml, config.active_agent.telemetry). A host
89
+ # app that enables telemetry from its own initializer runs *after*
90
+ # railties, so that check has already seen `enabled? == false` and
91
+ # skipped the install — leaving telemetry enabled but nothing
92
+ # instrumented, and so no traces despite a valid local_store. Calling it
93
+ # from {.configure} as well makes the install order-independent;
94
+ # `instrument_telemetry!` is idempotent, so the two paths cannot
95
+ # double-prepend.
96
+ #
97
+ # @api private
98
+ # @return [void]
99
+ def install_instrumentation!
100
+ return unless defined?(ActiveAgent::Base)
101
+
102
+ ActiveAgent::Base.include(Instrumentation)
103
+ ActiveAgent::Base.instrument_telemetry!
104
+ end
105
+
84
106
  # Resets the configuration to defaults.
85
107
  #
86
108
  # @return [Configuration] New default configuration
@@ -1,3 +1,3 @@
1
1
  module ActiveAgent
2
- VERSION = "1.5.2"
2
+ VERSION = "1.6.2"
3
3
  end
data/lib/active_agent.rb CHANGED
@@ -92,6 +92,9 @@ module ActiveAgent
92
92
  #
93
93
  # These components are loaded on-demand when first referenced.
94
94
  autoload :Base
95
+ # The refusal an agent raises, and the default in Base.authorization_errors.
96
+ # Reachable before any agent class has loaded, so the dashboard can name it.
97
+ autoload :NotAuthorized, "active_agent/concerns/authorization"
95
98
  autoload :Callbacks, "active_agent/concerns/callbacks"
96
99
  autoload :Delegation, "active_agent/concerns/delegation"
97
100
  autoload :Streaming, "active_agent/concerns/streaming"
@@ -0,0 +1,22 @@
1
+ Description:
2
+ Writes a starter ActiveAgent::SchemaTools class for a model under
3
+ app/agent_tools. The class generates find_<records>, count_<records> and
4
+ get_<record> tools, and exposes nothing beyond `id` until you move a
5
+ column into `filterable` or `returns` — every column the model has is
6
+ listed, commented out, so the allowlist is a review step rather than a
7
+ blank page. Columns that look like secrets are left out of the list.
8
+
9
+ When <Model>Policy::Scope exists the class scopes every read through it
10
+ (`scope_by_policy`); otherwise a `scope` block is suggested. Pass
11
+ --policy or --no-policy to decide explicitly.
12
+
13
+ Examples:
14
+ `bin/rails generate active_agent:schema_tools Reservation`
15
+
16
+ creates:
17
+ app/agent_tools/reservation_tools.rb
18
+
19
+ `bin/rails generate active_agent:schema_tools Reservation --policy`
20
+
21
+ scopes reads through ReservationPolicy::Scope even if it cannot be
22
+ found from here.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAgent
4
+ module Generators
5
+ # Writes a starter ActiveAgent::SchemaTools class for one model:
6
+ #
7
+ # bin/rails generate active_agent:schema_tools Reservation
8
+ #
9
+ # The class it writes exposes nothing beyond +id+ until someone uncomments
10
+ # a column, because which columns an agent may filter on and read back is
11
+ # a judgement about exposure, not a fact about the table (#440). Every
12
+ # column the model has is listed, commented out, minus the ones that look
13
+ # like secrets, so the allowlist is a review step rather than a blank page.
14
+ class SchemaToolsGenerator < ::Rails::Generators::NamedBase
15
+ source_root File.expand_path("templates", __dir__)
16
+
17
+ # Columns never suggested, whatever the model: reading one back would
18
+ # hand a model a credential, and filtering on one leaks it a character
19
+ # at a time through the row counts.
20
+ SECRET_COLUMNS = /password|digest|token|secret|api_key|otp|encrypted|ssn/i
21
+
22
+ class_option :policy, type: :boolean, default: nil,
23
+ desc: "Scope every read through <Model>Policy::Scope (default: when that policy exists)"
24
+
25
+ check_class_collision suffix: "Tools"
26
+
27
+ def create_tools_file
28
+ template "schema_tools.rb.tt", File.join("app/agent_tools", class_path, "#{file_name}_tools.rb")
29
+ end
30
+
31
+ private
32
+
33
+ # "Reservation" and "ReservationTools" both name the Reservation model.
34
+ def file_name # :doc:
35
+ @_file_name ||= super.sub(/_tools\z/i, "")
36
+ end
37
+
38
+ def model_class
39
+ @model_class ||= class_name.safe_constantize
40
+ end
41
+
42
+ def policy_class_name
43
+ "#{class_name}Policy"
44
+ end
45
+
46
+ def policy?
47
+ return options[:policy] unless options[:policy].nil?
48
+
49
+ "#{policy_class_name}::Scope".safe_constantize.present?
50
+ end
51
+
52
+ # [name, type] for every column the model has, or [] when the model or
53
+ # its table cannot be read from here (a model that does not exist yet,
54
+ # or a database that is not set up) — the file is still written.
55
+ def columns
56
+ @columns ||= begin
57
+ if model_class.respond_to?(:columns) && model_class.respond_to?(:table_exists?) && model_class.table_exists?
58
+ model_class.columns.map { |column| [ column.name, column.type ] }
59
+ else
60
+ []
61
+ end
62
+ rescue StandardError
63
+ []
64
+ end
65
+ end
66
+
67
+ def suggested_columns
68
+ columns.reject { |name, _type| name == "id" || name.match?(SECRET_COLUMNS) }
69
+ end
70
+
71
+ def secret_columns
72
+ columns.select { |name, _type| name.match?(SECRET_COLUMNS) }.map(&:first)
73
+ end
74
+
75
+ def collection_name
76
+ class_name.demodulize.underscore.pluralize
77
+ end
78
+
79
+ def resource_name
80
+ class_name.demodulize.underscore
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,48 @@
1
+ <% module_namespacing do -%>
2
+ # Bounded, read-only agent tools over <%= class_name %>:
3
+ # find_<%= collection_name %>, count_<%= collection_name %>, get_<%= resource_name %>.
4
+ #
5
+ # Nothing is exposed until you say so. Move a column from the commented lists
6
+ # into `filterable` (an agent may filter on it) or `returns` (an agent may
7
+ # read it back). Each one is a judgement about exposure, not a fact about the
8
+ # table: leave out anything a model should never see.
9
+ class <%= class_name %>Tools < ActiveAgent::SchemaTools
10
+ model <%= class_name %>
11
+
12
+ filterable :id
13
+ <% if suggested_columns.any? -%>
14
+ # filterable <%= suggested_columns.map { |name, _type| ":#{name}" }.join(", ") %>
15
+ <% else -%>
16
+ # filterable :status, :owner_id # columns an agent may filter on
17
+ <% end -%>
18
+
19
+ returns :id
20
+ <% if suggested_columns.any? -%>
21
+ # returns <%= suggested_columns.map { |name, _type| ":#{name}" }.join(", ") %>
22
+ <% else -%>
23
+ # returns :id, :title, :status # columns an agent may read back
24
+ <% end -%>
25
+ <% if suggested_columns.any? -%>
26
+ #
27
+ # Columns and their types:
28
+ <% suggested_columns.each do |name, type| -%>
29
+ # <%= name.ljust(24) %> <%= type %>
30
+ <% end -%>
31
+ <% end -%>
32
+ <% if secret_columns.any? -%>
33
+ #
34
+ # Not suggested, and not to be added: <%= secret_columns.join(", ") %>.
35
+ <% end -%>
36
+
37
+ <% if policy? -%>
38
+ # Every read runs through <%= policy_class_name %>::Scope for the acting
39
+ # user, so an agent sees exactly what that user could see.
40
+ scope_by_policy
41
+ <% else -%>
42
+ # Every read should run through a relation scoped to the acting user. Without
43
+ # a scope the tools read the whole table. Return <%= class_name %>.none for an
44
+ # actor with no access; never widen for a nil actor.
45
+ # scope { |actor| actor ? <%= class_name %>.where(owner: actor) : <%= class_name %>.none }
46
+ <% end -%>
47
+ end
48
+ <% end -%>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.6.2
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-09-11 00:00:00.000000000 Z
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -410,6 +410,7 @@ files:
410
410
  - lib/active_agent.rb
411
411
  - lib/active_agent/base.rb
412
412
  - lib/active_agent/collector.rb
413
+ - lib/active_agent/concerns/authorization.rb
413
414
  - lib/active_agent/concerns/callbacks.rb
414
415
  - lib/active_agent/concerns/delegation.rb
415
416
  - lib/active_agent/concerns/observers.rb
@@ -417,6 +418,7 @@ files:
417
418
  - lib/active_agent/concerns/preview.rb
418
419
  - lib/active_agent/concerns/provider.rb
419
420
  - lib/active_agent/concerns/queueing.rb
421
+ - lib/active_agent/concerns/release.rb
420
422
  - lib/active_agent/concerns/rescue.rb
421
423
  - lib/active_agent/concerns/streaming.rb
422
424
  - lib/active_agent/concerns/tooling.rb
@@ -578,6 +580,9 @@ files:
578
580
  - lib/generators/active_agent/agent/agent_generator.rb
579
581
  - lib/generators/active_agent/install/USAGE
580
582
  - lib/generators/active_agent/install/install_generator.rb
583
+ - lib/generators/active_agent/schema_tools/USAGE
584
+ - lib/generators/active_agent/schema_tools/schema_tools_generator.rb
585
+ - lib/generators/active_agent/schema_tools/templates/schema_tools.rb.tt
581
586
  - lib/generators/active_agent/templates/active_agent.yml
582
587
  - lib/generators/active_agent/templates/agent.rb.tt
583
588
  - lib/generators/active_agent/templates/application_agent.rb.tt