AI Tool - Read

Adds ai_tool tools to let an AI agent read any model and field it is
allowed to access, while keeping personal or otherwise sensitive fields
out of the results.
A flag AI Read Forbidden (ai_no_read) is added to fields. The
tools never return nor describe a forbidden field, nor any field
exposing its value: related fields, including those delegated through
_inherits, and computed fields depending on it. Flagging
res.partner.email thus also hides email_formatted,
email_normalized and res.users.email.
Three tools are provided:
- list_models: lists the models the current user is allowed to read.
- get_fields: describes a model’s readable fields (label, type,
help, selection options and relation target), and lists the forbidden
ones under hidden.
- search_read: searches and reads records of a model. It returns the
stored fields unless a field list is given, relational fields as
record IDs, and dates in UTC. Results are paged with limit and
offset, and has_more tells whether more records match. A field
whose computation is refused for the user is left out and listed in
inaccessible_fields, and a record the search finds but the user
cannot read (as ir.attachment allows) is left out without notice,
as record rules would.
The tools run with the calling user’s own access rights (no sudo),
so Odoo ACLs, record rules and field groups still apply. Connect the
agent as a user with no more rights than it needs: an administrator sees
every field restricted by groups.
Besides forbidden fields, the results leave out binary fields, fields
pointing to a model the user cannot read (reading them would fail), and
display names computed on the fly, including the one Odoo adds to
many2one values: they are built from other records’ fields, which may be
forbidden there.
The flag keeps values out of the results, not out of searches: see
Known issues.
Table of contents
To forbid a field, tick AI Read Forbidden on it. As the
administrator, either:
- Go to Settings > Technical > Database Structure > Fields, find the
field and tick the flag (the AI Read Forbidden filter and list
column help you review what is already flagged), or
- Open the model in Settings > Technical > Database Structure > Models
and tick the flag on the line of the field, in the Fields tab.
The flag applies to base fields as well as custom ones, and takes effect
immediately. A flag set on a mixin’s field, such as email_normalized
on mail.thread.blacklist, applies to every model inheriting it.
A forbidden field takes along the fields that expose its value through
their definition, on any model: related fields, the fields a model
delegates through _inherits, and computed fields that depend on it.
The hidden list returned by the get_fields tool shows the result
on a model. A value copied into another record when that record is
written is not followed (see Known issues): flag such fields as well.
This errs on the safe side: a computed field is hidden as soon as it
depends on a forbidden field, even when its value does not contain it.
Forbidding res.partner.email also hides is_blacklisted, a yes/no
computed from the address, and hr.employee.mobile_phone, computed by
the same method as the work email. For the same reason, flag the fields
that hold the sensitive values themselves (email, phone, address), not
fields that many others are computed from, such as parent_id or
company_id: those would hide far more than intended.
Fields restricted to a user group (groups= in their definition) are
already left out of the results for a user outside that group, without
any flag, and are not reported in the hidden list either. AI Read
Forbidden is only needed for fields that the user is otherwise allowed
to read.
The tools are registered as ai.tool records and are callable through
any ai_tool integration: ai_oca_mcp, for instance, exposes them
to MCP clients such as Claude Desktop. This module provides the tools
themselves; how they are reached depends on the transport module
installed.
A typical agent workflow:
Call list_models to discover the models the bound user may read.
Each entry gives the technical model name (e.g. res.partner)
to pass to the other tools.
Call get_fields with model to learn what each readable field
means (label, type, help, selection options, relation target), and
which fields are listed under hidden.
Call search_read to fetch records:
- model (required): the technical model name.
- domain (optional): an Odoo domain as a list, e.g.
[["is_company", "=", true]].
- fields (optional): an explicit field list; the stored readable
fields are returned otherwise. An empty list returns ids only.
- limit (optional): page size, 200 by default and at most; a
value of 0 or less, or past the cap, is treated as the maximum.
- offset (optional): skip that many matching records.
- order (optional): e.g. "name asc".
Relational fields come back as record ids, dates and datetimes as
ISO-8601 strings in UTC. has_more tells whether another page
exists under the same domain and order; fields the call could not
read are listed in inaccessible_fields, while forbidden or
restricted fields are simply absent from the records.
Example search_read arguments:
{
"model": "res.partner",
"domain": [["is_company", "=", true]],
"fields": ["name", "email", "country_id"],
"limit": 20,
"order": "name asc"
}
Every call executes with the access rights of the calling user — the
API-key owner when reached through ai_oca_mcp — so Odoo ACLs, record
rules and field groups apply as usual. Connect the agent as a
dedicated user holding only the rights it needs.
The tools keep forbidden values out of their results, not out of the
searches an agent runs: they are not meant to stop an agent that probes
for hidden values on purpose. Each case below still lets a forbidden
value through:
- Filtering on a forbidden field.
[("email", "ilike", "@example.com")] lists the contacts whose
address matches, and repeating it on longer fragments rebuilds a whole
address. The same works on fields restricted by groups=, which
Odoo does not check when searching, and on search-only fields:
phone_mobile_search rebuilds a phone number even when phone is
forbidden.
- Searching a relation by text.
[("partner_id", "ilike", "@example.com")] searches contacts by
name, email included, and finds the orders of the contacts whose
address matches.
- Sorting. Ordering on a forbidden field, or on a many2one (sorted
by the related record’s display name), reveals the order of the hidden
values.
- Copied values. A value copied into another record is not linked to
its source: the author’s address in a chatter message
(mail.message.email_from) is returned unless that field is flagged
too.
One more limit, which leaks nothing: a requested field that is
forbidden, binary or restricted is simply missing from the records, with
no reason given; get_fields tells them apart.
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
feedback.
Do not contact contributors directly about support or help with technical issues.
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
Current maintainer:

This module is part of the OCA/ai project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.