Skip to content

Schema tools: derive the roster from models/agents rather than a hand-written config array #440

Description

@TonsOfFun

Design feedback on #435, from the repo owner while reviewing it for release. Filing so it is not lost in a PR thread — the current shape ships for testing, this is about where it should land.

The concern

Declaring tools as a config array is the weakest part of the current design:

ActionAgent.configure do |config|
  config.schema_tools = [TicketTools, TaskTools, MilestoneTools]
end

Three things are already known to the framework, and the config restates them:

  1. The modelTicketTools already says model Ticket.
  2. The agent — the naming convention already maps TicketTicketAgent.
  3. The schema — the columns are in the database.

So a host declares a class whose entire content is derivable, then declares it again in an initializer. Something like Reservation < ApplicationRecord being reachable by ReservationAgent with no config at all is the shape worth aiming for.

Why it was not done that way in #435

Not because it is hard. Enumerating models after eager_load! is a few lines, and the convention already works. The problem is what auto-registration would expose.

Measured on a real host app (27 models):

ApiToken, Bill, BillLineItem, CliDeviceGrant, Client, ClientContact,
ClientSupportAssignment, GithubPullRequest, LiveNote, MeetingAttendee,
MeetingRecording, Milestone, MilestoneTemplate, OosBillLineItem,
OutOfScopeNote, OutOfScopeRequest, Project, ProjectMembership,
ProjectNote, Proof, Task, TaskTemplate, TaskTimeEntry, Ticket,
TicketStatusChange, TimeEntry, User

Auto-registering all of them puts User, ApiToken and CliDeviceGrant in front of a model by default. PROPOSAL_schema_tools.md called this out directly: "Nothing should appear in an agent's toolset because a table exists."

The filterable / returns allowlists are the other half of the same argument. They cannot be derived from the schema, because "which columns may a model filter on" is a judgement about exposure, not a fact about the table. Deriving them would mean either exposing every column or inventing a heuristic that quietly decides password_digest is uninteresting.

Middle grounds worth considering

Ordered by how much they keep the boundary explicit:

  1. Drop the config array; discover SchemaTools subclasses. The declaration stays the boundary, but it is declared once instead of twice — SchemaTools.descendants after eager-loading app/agent_tools. This is what the host shim did before Schema-derived agent tools: generate a bounded tool roster from an ActiveRecord model #435's seam existed, and it worked. Smallest change, removes the duplication actually complained about.
  2. A generator. rails g active_agent:schema_tools Reservation writes a starter class with every column commented out, so the allowlist is a review step rather than a blank page.
  3. Opt-in auto-derivation with an explicit include list. config.schema_tools_from_models = [Reservation, Ticket] — derives the roster but still names the models. Removes the tool class for simple cases.
  4. Full auto-registration with a denylist. Most convenient, and the one that eventually exposes a table someone forgot to deny. Would need a hard-coded refusal for anything matching the Devise/token/session shape, which is a heuristic guarding a security boundary.

(1) looks like the clear near-term win; it is strictly less config with no change to what is exposed. (3) is the interesting one if the goal is to delete tool classes entirely for CRUD-shaped models.

Not blocking the current release

#435 ships as-is for testing and demo. This is about the next iteration.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions