Odoo Community Association

AI Tool - Read

Beta License: AGPL-3 OCA/ai Translate me on Weblate Try me on Runboat

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:

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

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:

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

Credits

Authors

  • Quartile

Contributors

Maintainers

This module is maintained by the OCA.

Odoo Community Association

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:

nobuQuartile

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.