Behaviour for resolving visibility rules for a given data source.
This is a supported extension point. The default implementation
(Lotus.Visibility.Resolvers.Static) reads from static application
configuration, which is all most applications need. Alternative
implementations can load rules from registries, databases, feature
flags, or per-tenant configuration — for example to change rules
without restarting the application, or to scope rules to the current
user or tenant.
Configure a custom resolver via the :visibility_resolver config key:
config :lotus,
visibility_resolver: MyApp.VisibilityResolverScope
Every callback receives an opaque scope term as the second argument.
Callers pass scope via the :scope option on discovery functions
(e.g. Lotus.list_tables("postgres", scope: %{role: :admin})). When
the caller does not pass a scope, it is nil.
Scope is also hashed into the discovery cache key, so different scopes produce independent cached entries. Keep scope low-cardinality (per-role, per-tenant) for good cache hit rates.
The default Static resolver ignores scope — it returns the same
config-based rules regardless.
The rule formats returned by each callback are the same as those consumed by the static resolver — see the Visibility guide for the full syntax, and the Custom Resolvers guide for contracts, minimal examples, and testing guidance.
Summary
Callbacks
Return the compiled matcher for a source.
Whether the caller in scope may use the source at all.
Callbacks
@callback matcher_for(source_name :: String.t(), scope :: term()) :: Lotus.Visibility.Matcher.t()
Return the compiled matcher for a source.
Optional. Without it, core compiles the three rule callbacks with
Lotus.Visibility.compile/2 on every result and every discovery call. A
resolver that keeps rules in a store implements this to compile once when
a rule set is written and hand back the compiled value. The matcher must
include the built-in denies of the source's adapter: pass the adapter to
Lotus.Visibility.compile/2 as :adapter.
Whether the caller in scope may use the source at all.
Optional. Without it, every configured source is visible, and the rule
callbacks decide what is visible inside it. Return false and Lotus
refuses to run statements against the source for that scope, hides
everything in it from discovery, and leaves it out of the data sources
the AI assistant and Lotus.MCP list.
def source_visible?("billing", %{role: role}), do: role == :finance
def source_visible?(_source, _scope), do: true