Module: Parse::Agent::Describe

Included in:
Parse::Agent
Defined in:
lib/parse/agent/describe.rb

Overview

Developer-facing introspection mixin. Mixed into Parse::Agent via ‘include Describe` so `agent.describe`, `agent.describe_for(class_name)`, and `agent.would_permit?(…)` are instance methods on every agent.

SECURITY POSTURE — this is operator-side observability, NOT data exposed to the LLM. The operator wrote every rule the helper echoes back; showing them their own configuration is just transparency. The output is NOT included in any tool response, MCP ‘tools/list`, or `parse.agent.tool_call` notification payload by default. If a deployment chooses to surface the output (e.g. via a debug HTTP endpoint), it should be auth-gated on the same boundary that authenticates the operator console.

The ‘session_token` value is NEVER returned verbatim. #auth_descriptor emits a stable SHA256-truncated fingerprint so two `describe` calls on the same session correlate, but the raw bearer token never leaves the method. Master-key mode is identified by the `:master_key` symbol only.

Instance Method Summary collapse

Instance Method Details

#describe(pretty: false) ⇒ Hash, String

Full introspection Hash for the agent. Lists every layer that gates what the agent can see and do, plus per-class metadata for the classes the agent explicitly references.

Parameters:

  • pretty (Boolean) (defaults to: false)

    when true, returns a multi-line String formatted for ‘puts` debugging instead of the structured Hash. The String is generated from the same data the Hash exposes.

Returns:



33
34
35
36
# File 'lib/parse/agent/describe.rb', line 33

def describe(pretty: false)
  data = describe_hash
  pretty ? describe_pretty(data) : data
end

#describe_for(class_name) ⇒ Hash

Per-class breakdown for a single Parse class. Includes the agent’s effective reach for the class (visible? class-filter permitted? canonical filter? per-agent filter? tenant-scoped?) plus the class-level metadata declared via ‘agent_fields` / `agent_methods` / `agent_large_fields`. Useful when an agent has 30 visible classes and a developer is debugging one specific refusal.

Parameters:

  • class_name (String, Symbol, Class)

    the Parse class to look up

Returns:

  • (Hash)

    per-class introspection envelope



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/parse/agent/describe.rb', line 47

def describe_for(class_name)
  cn = if class_name.is_a?(Class) && class_name.respond_to?(:parse_class)
      class_name.parse_class
    else
      class_name.to_s
    end
  {
    class_name:              cn,
    accessible:              describe_class_accessibility(cn),
    agent_fields:            class_field_allowlist(cn),
    agent_canonical_filter:  Parse::Agent::MetadataRegistry.canonical_filter(cn),
    per_agent_filter:        respond_to?(:filter_for) ? filter_for(cn) : nil,
    tenant_scope:            class_tenant_scope(cn),
    large_fields:            class_large_fields(cn),
    agent_methods:           class_agent_method_names(cn),
  }
end

#would_permit?(tool_name, class_name: nil, op: nil, method_name: nil, **_kwargs) ⇒ Hash

Dispatch-gate simulator. Runs every accessibility check that the tool dispatcher would run, without actually invoking the tool. Lets a developer answer “why is this agent refusing this call?” in one line, without parsing the audit payload or tracing through the tool implementation.

TRACK-AGENT-8: mirrors the REAL dispatch gates in Parse::Agent#execute and Tools.assert_class_accessible!. The simulator now checks:

* tool filter (`tools:` kwarg / `tool_filter_*` sets) and
  permission-tier membership
* env-gate (`PARSE_AGENT_ALLOW_WRITE_TOOLS` /
  `PARSE_AGENT_ALLOW_RAW_CRUD` for write tools;
  `PARSE_AGENT_ALLOW_SCHEMA_OPS` /
  `PARSE_AGENT_ALLOW_RAW_SCHEMA` for schema tools)
* `class_name` accessibility, including hidden-class +
  master-key-except, per-agent class allowlist, AND the
  CLP `op:` gate (forwarded when an `op:` is supplied)
* `master_atlas?` opt-in gate for `atlas_faceted_search`
* `method_filtered?` for `call_method` when a
  `method_name:` is supplied

Parameters:

  • tool_name (Symbol)

    the tool being checked

  • class_name (String, Symbol, Class, nil) (defaults to: nil)

    optional class scope for tools that take a ‘class_name:` argument

  • op (Symbol, nil) (defaults to: nil)

    optional CLP op (‘:find`, `:get`, `:count`, `:create`, `:update`, `:delete`, `:addField`) for class-level CLP checks. When omitted, only the class-visibility gate runs; CLP is not consulted.

  • method_name (Symbol, String, nil) (defaults to: nil)

    optional ‘agent_method` target for `call_method` simulation

Returns:

  • (Hash)

    ‘Boolean, reason: Symbol?, denied_at: Symbol?` `reason` and `denied_at` are populated only when `allowed: false`.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/parse/agent/describe.rb', line 99

def would_permit?(tool_name, class_name: nil, op: nil, method_name: nil, **_kwargs)
  tool_sym = tool_name.to_sym

  # Tool filter — present at the per-instance layer. Preserve
  # the historical `:tool_filtered` reason regardless of whether
  # the denial came from tier or instance filter, since the
  # describe consumer reads it as "this tool will be refused"
  # rather than as the dispatcher's split error_code.
  unless allowed_tools.include?(tool_sym)
    return { allowed: false, reason: :tool_filtered, denied_at: :allowed_tools }
  end

  # Env-gate for raw CRUD / schema-mutating tools. Mirrors the
  # gate in Parse::Agent#execute at line 1639-1662.
  if Parse::Agent::WRITE_GATED_TOOLS.include?(tool_sym) &&
     !(Parse::Agent.write_tools_enabled? && Parse::Agent.raw_crud_enabled?)
    return { allowed: false, reason: :write_env_gate_disabled,
             denied_at: :write_env_gate }
  end
  if Parse::Agent::SCHEMA_GATED_TOOLS.include?(tool_sym) &&
     !(Parse::Agent.schema_ops_enabled? && Parse::Agent.raw_schema_enabled?)
    return { allowed: false, reason: :schema_env_gate_disabled,
             denied_at: :schema_env_gate }
  end

  # atlas_faceted_search opt-in (master_atlas: true required —
  # see tools.rb:atlas_faceted_search). Mirrors the explicit
  # opt-in inside the tool body so the simulator doesn't
  # over-report :permitted for a session-bound agent.
  if tool_sym == :atlas_faceted_search &&
     !(respond_to?(:master_atlas?) && master_atlas?)
    return { allowed: false, reason: :master_atlas_required,
             denied_at: :master_atlas_gate }
  end

  # Class access gate — when the tool takes a class_name argument.
  # Includes CLP `op:` check when the caller supplied one,
  # mirroring assert_class_accessible!'s signature.
  if class_name
    cn = class_name.is_a?(Class) && class_name.respond_to?(:parse_class) ?
           class_name.parse_class : class_name.to_s
    begin
      Parse::Agent::Tools.assert_class_accessible!(cn, agent: self, op: op)
    rescue Parse::Agent::AccessDenied => e
      kind = e.respond_to?(:kind) && e.kind ? e.kind : :access_denied
      return { allowed: false, reason: kind, denied_at: :assert_class_accessible! }
    rescue Parse::Agent::ValidationError
      return { allowed: false, reason: :invalid_argument, denied_at: :assert_class_accessible! }
    end
  end

  # method_filtered? — mirror the call_method gate at tools.rb:3948.
  # Only fires when the caller supplied a method_name AND the
  # tool is call_method (the method-filter only narrows that tool).
  if tool_sym == :call_method && method_name && class_name
    cn = class_name.is_a?(Class) && class_name.respond_to?(:parse_class) ?
           class_name.parse_class : class_name.to_s
    if respond_to?(:method_filtered?) &&
       method_filtered?(method_name.to_sym, class_name: cn)
      return { allowed: false, reason: :method_filtered,
               denied_at: :method_filtered }
    end
  end

  { allowed: true }
end