Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **A delegated run inherits its parent's caller.** `delegate_to` hands the
sub-agent the parent's `current_user` before its action runs, so its own
`before_action` callbacks and any scope its tools read through decide
against the same person. A parent authorized as one user no longer hands
its specialists an unattributed run — which a correctly written host scope
reads as "no access", a wrong answer wearing a right one's clothes. A
parent with no caller still delegates an unattributed run, never someone
else's.
- **`rails generate active_agent:schema_tools Reservation`** writes a starter
`ActiveAgent::SchemaTools` class under `app/agent_tools`. It exposes nothing
beyond `id` until a column is moved into `filterable` or `returns`; every
Expand Down
17 changes: 17 additions & 0 deletions lib/active_agent/delegation/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def coerce_arguments(arguments)
def generate(arguments)
agent = definition.resolved_agent_class.new
agent.params = resolved_params(arguments)
inherit_actor(agent)
agent.process(definition.action, **arguments)

definition.backend.apply(agent)
Expand All @@ -126,6 +127,22 @@ def generate(arguments)
agent.process_prompt
end

# A delegated generation runs on behalf of whoever the parent runs for.
# The sub-agent gets the parent's caller before its action runs, so its
# own before_action callbacks and any scope its tools read through decide
# against the same person — a parent authorized as one user must not
# hand its specialists an unattributed run, which a correctly written
# host scope reads as "no access". Hosts on an older framework, where an
# agent has no caller to carry, are left as they were.
#
# @param agent [ActiveAgent::Base]
# @return [void]
def inherit_actor(agent)
return unless owner.respond_to?(:current_user) && agent.respond_to?(:current_user=)

agent.current_user = owner.current_user
end

# A delegated generation is part of its parent's work, so it carries the
# parent's trace id — otherwise the sub-agent's tokens and latency land
# in a separate trace and the budget you set has nothing to show for it.
Expand Down
43 changes: 43 additions & 0 deletions test/authorization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,47 @@ def tool_call(agent_class, action, actor: nil, **kwargs)
assert_equal "an unauthenticated caller is not allowed to call `delete_ticket`",
agent.tools_function.call(:delete_ticket, id: 7)[:error]
end

# A parent authorized as one caller must hand its specialists the same
# caller: an unattributed delegated run reads, through any host scope, as
# "no access", which is a wrong answer wearing a right one's clothes.
class SpecialistAgent < ActiveAgent::Base
generate_with :mock

SEEN = []

before_action { SEEN << current_user&.name }

delegation :summarize, description: "Condense text" do
string :text, required: true
end

def summarize(text:)
prompt(message: text)
end
end

class OrchestratorAgent < ActiveAgent::Base
generate_with :mock

delegate_to SpecialistAgent
end

test "a delegated run inherits the parent's caller" do
SpecialistAgent::SEEN.clear
orchestrator = OrchestratorAgent.new
orchestrator.current_user = Caller.new("alice")

orchestrator.perform_delegation(:summarize, text: "The order shipped on Monday.")

assert_equal [ "alice" ], SpecialistAgent::SEEN
end

test "a parent with no caller delegates an unattributed run, never someone else's" do
SpecialistAgent::SEEN.clear

OrchestratorAgent.new.perform_delegation(:summarize, text: "The order shipped.")

assert_equal [ nil ], SpecialistAgent::SEEN
end
end