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
251 changes: 251 additions & 0 deletions ai_tool_read/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

==============
AI Tool - Read
==============

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:fe66b1297c9c53a47b95b8407598bb5fab4d963c5e90afea0c614688365f21fc
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github
:target: https://github.com/OCA/ai/tree/16.0/ai_tool_read
:alt: OCA/ai
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_tool_read
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/ai&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

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**

.. contents::
:local:

Configuration
=============

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.

Usage
=====

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:

1. 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.

2. 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``.

3. 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:

.. code:: json

{
"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.

Known issues / Roadmap
======================

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.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/ai/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 <https://github.com/OCA/ai/issues/new?body=module:%20ai_tool_read%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Quartile

Contributors
------------

- `Quartile <https://www.quartile.co>`__:

- Toshikimi Shigenobu

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

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.

.. |maintainer-nobuQuartile| image:: https://github.com/nobuQuartile.png?size=40px
:target: https://github.com/nobuQuartile
:alt: nobuQuartile

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-nobuQuartile|

This module is part of the `OCA/ai <https://github.com/OCA/ai/tree/16.0/ai_tool_read>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions ai_tool_read/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
19 changes: 19 additions & 0 deletions ai_tool_read/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2026 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "AI Tool - Read",
"summary": "Generic ai_tool to read any model/field, with a per-field opt-out flag",
"version": "16.0.1.0.0",
"website": "https://github.com/OCA/ai",
"license": "AGPL-3",
"author": "Quartile,Odoo Community Association (OCA)",
"maintainers": ["nobuQuartile"],
"depends": [
"ai_tool",
],
"data": [
"views/ir_model_fields_views.xml",
"data/ai_tools.xml",
],
}
30 changes: 30 additions & 0 deletions ai_tool_read/data/ai_tools.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="list_models_tool" model="ai.tool">
<field name="name">list_models</field>
<field
name="description"
>List the models the current user is allowed to read, as {model, name} entries. Use the technical "model" value (e.g. res.partner) with the get_fields and search_read tools.</field>
<field name="model_id" ref="base.model_ir_model" />
<field name="function_name">_ai_list_models</field>
<field name="kind">generic</field>
</record>
<record id="get_fields_tool" model="ai.tool">
<field name="name">get_fields</field>
<field
name="description"
>Describe a model's readable fields (label, type, help, selection options and relation target) so their values can be interpreted. Returns "fields" (readable) and "hidden" (fields forbidden to AI).</field>
<field name="model_id" ref="base.model_ir_model" />
<field name="function_name">_ai_get_fields</field>
<field name="kind">generic</field>
</record>
<record id="search_read_tool" model="ai.tool">
<field name="name">search_read</field>
<field
name="description"
>Search and read records of a model. Accepts an optional Odoo domain such as [["is_company", "=", true]], an explicit field list, a limit (200 at most, and by default), an offset and an order. Without a field list, the stored fields are returned; ask for computed ones explicitly. "has_more" tells whether more records match: read the next page with the offset increased by the limit, keeping the same domain and order. Relational fields are returned as record IDs; read the related model to get the details of those records. Dates and times are in UTC. A requested field missing from the records is not available (forbidden to AI, binary, or not readable for the current user), so do not retry it; get_fields lists the readable fields, and the forbidden ones under "hidden". A field whose value cannot be computed for the current user is left out and listed in "inaccessible_fields"; do not retry it either.</field>
<field name="model_id" ref="base.model_ir_model" />
<field name="function_name">_ai_search_read</field>
<field name="kind">generic</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions ai_tool_read/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import ir_model_fields
from . import ir_model
Loading
Loading