Skip to content

[ADD] skills: odoo-guidelines, odoo-review, odoo-security - #5250

Open
fvdsn wants to merge 1 commit into
master-js-guidelines-gedfrom
master-workspace-skills-fva
Open

[ADD] skills: odoo-guidelines, odoo-review, odoo-security#5250
fvdsn wants to merge 1 commit into
master-js-guidelines-gedfrom
master-workspace-skills-fva

Conversation

@fvdsn

@fvdsn fvdsn commented Aug 28, 2026

Copy link
Copy Markdown

Stacked on odoo#285015 (targets its branch so only these commits show; rebase to master once it merges).

Adds three agent skills to skills/, following the structure introduced there:

  • odoo-guidelines — the house rules for addon code, one numbered file per domain (module structure, manifest, Python/ORM, fields, controllers, XML, QWeb reports, access rights, SCSS, performance, tests), dispatched by a routing table keyed on the files being touched. Trap-shaped rules (translations interpolation, view inheritance, batch ORM calls) get the full treatment with examples; the rest stay one-line bullets. Authoring conventions in AUTHORING.md.
  • odoo-review — a thin review-process skill: per-changed-file dispatch to the matching guidelines (including odoo-js-guidelines), stable-vs-master policy, version-trap warning, explicit completion criterion.
  • odoo-security — a security-audit skill routed by a sweep table (16 grep patterns → pitfall sections): access control (ir.access), sudo, SQL/domain injection, XSS, CSRF, eval, file access, and friends.

All content was verified in two passes against today's master: first against the documentation, then against the source itself. The source pass overruled the docs in several places (ir.access unification, t-raw and the restricted pickle no longer existing, CSRF being http-type-only, test-tag defaults) — a list of the documentation bugs found along the way is being forwarded to the docs team separately.

@robodoo

robodoo commented Aug 28, 2026

Copy link
Copy Markdown

This PR targets the un-managed branch odoo-dev/odoo:master-js-guidelines-ged, it needs to be retargeted before it can be merged.

@fvdsn
fvdsn force-pushed the master-js-guidelines-ged branch from 27fbaf2 to f91f605 Compare September 3, 2026 12:05
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch 2 times, most recently from 4c97733 to c951129 Compare September 3, 2026 12:20
@fvdsn
fvdsn force-pushed the master-js-guidelines-ged branch from f91f605 to fd61148 Compare September 3, 2026 13:14
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch from c951129 to aee3371 Compare September 3, 2026 13:15
@fvdsn
fvdsn force-pushed the master-js-guidelines-ged branch 2 times, most recently from 62cdc41 to 97ebb86 Compare September 3, 2026 13:43
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch from aee3371 to cbcfb5c Compare September 3, 2026 13:43
@fvdsn
fvdsn force-pushed the master-js-guidelines-ged branch from 97ebb86 to f5321c6 Compare September 3, 2026 13:47
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch from cbcfb5c to 91ccb5d Compare September 3, 2026 13:48
- Standard directories: `models/`, `views/`, `controllers/`, `data/`,
`security/`, `static/`, and optional `wizard/`, `report/`, `tests/`,
`populate/`.
- Split models by main model, one file per main model; each inherited model in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a main model?
The rule is just one model class per file.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, there is no such a thing as a "main model".
The rule is: one model is one class. Then one class (or model) per file.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded.

- `populate/` holds `populate.blueprint` XML records (`populate_demo.xml`,
`benchmarks.xml`); an `__init__.py` makes it an importable package for
custom populate generators.
- File names use only `[a-z0-9_]`. Don't link external data by URL (images,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filenames are python packages (follow that naming).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded.

- Model `_name`: dotted, prefixed by module, **singular** (`sale.order`, not
`sale.orders`). Transient/wizard: `<base_model>.<action>` (avoiding the word
"wizard" is a soft preference — core ships many `.wizard` names). SQL-view report model: `<base_model>.report.<action>`.
- Python classes use PascalCase (`class AccountInvoice(models.Model)`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is mostly python dependent, isn't the LLM already aware of python standards like PEP8?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the two PEP 8 lines (class and variable casing); the rest of the file is Odoo-specific.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept a one-line pointer: PEP 8 applies, ruff.toml at the repo root is the reference for what is enforced.

@@ -0,0 +1,37 @@
# ORM idioms & correctness

- Use recordset methods (`filtered`, `mapped`, `sorted`) for readability and

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Use recordset methods (`filtered`, `mapped`, `sorted`) for readability and
- Use recordset methods (`filtered`, `grouped`, `mapped`, `sorted`) for readability and

but you probably shouldn't prefer them in all cases. Use when it makes sense.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, with your wording.

need correct dependencies and no side effects.
- `@api.onchange` is UI-only — never rely on it for data integrity; enforce
invariants with `@api.constrains` or SQL constraints declared as model
attributes (`_x_check = models.Constraint("CHECK (...)", "msg")`; the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also define indexes similarly. For example models.UniqueIndex(...).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

minimal. In *master*, apply guidelines to new code, or to existing code only
when a file is under major change (do a separate *move* commit first).
- **Version traps.** Odoo's ORM and view/template syntax change between major
versions (`attrs=`, `<tree>`, `name_get`, `read_group` are all gone from

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_group is back, but it's for RPC calls :) (you can ignore this comment)

Comment thread skills/odoo-security/SKILL.md Outdated
| --- | --- |
| `sudo(`, `with_user(`, `with_company(` | [Don't over-sudo](#dont-over-sudo) |
| `cr.execute`, `SQL(`, `.format`/`%`/f-string near SQL | [Use the ORM; parameterize SQL](#use-the-orm-parameterize-sql) |
| domain built from request/user input | [Domain injection](#domain-injection) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also from field input. Users can set whatever value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread skills/odoo-security/SKILL.md Outdated
- A field's `groups` attribute removes it from views and `fields_get` and
raises on explicit read/write. Use it for sensitive fields instead of
relying on the UI to hide them.
- **Passwords and API tokens**: restrict them with `groups="base.group_system"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or fields.NO_ACCESS

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Comment thread skills/odoo-security/SKILL.md Outdated

- Never use the cursor directly when the ORM can do it: raw SQL bypasses
access control, translations, field invalidation, and `active` handling.
Prefer `search`/`_read_group`/`browse(...).read(...)`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read should not be used in python code, just access the fields directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

- When you must write SQL, **never** interpolate with `+`/`%`/`.format`. Pass
values as **parameters** (psycopg2 formats them, including a tuple for
`IN %s`), or use the `odoo.tools.SQL` wrapper. For dynamic **identifiers**
(table/column names, which can't be parameters) use `SQL.identifier(name)`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also odoo.models.Query and a new API where you can do stuff like:

query = model._search([...])
query.select(SQL("LENGTH(%s)", query.table.some_field))  # select length(t.some_field) from ... t where ...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

@fvdsn
fvdsn force-pushed the master-js-guidelines-ged branch from f5321c6 to 915cd0c Compare September 3, 2026 14:04
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch 2 times, most recently from fa5cce5 to ed30662 Compare September 3, 2026 14:13
Comment on lines +16 to +18
- Public (non-`_`) model methods are RPC-callable; prefix internal helpers
with `_`, or decorate with `@api.private` when a public name must not be
exposed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would invert the rule: private by default, public when a name must be exposed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inverted.

Comment on lines +3 to +5
- Standard directories: `models/`, `views/`, `controllers/`, `data/`,
`security/`, `static/`, and optional `wizard/`, `report/`, `tests/`,
`populate/`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strange this is by type and not by featyre, the opposite of the JS guideline.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️

Comment on lines +7 to +8
its own file (`res_partner.py`). A single-model module's file matches the
module name.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single-model name: I disagree, this seems oddly specific. Let us allow to pick the best module name and best model name without having them to match.

What if a module contains 2 models and we remove one? Do we suddenly need to rename the other one?

What if a module starts with a single model but need a second one later?

This is just useless and counter-productive.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped.

`benchmarks.xml`); an `__init__.py` makes it an importable package for
custom populate generators.
- File names use only `[a-z0-9_]`. Don't link external data by URL (images,
libs) — copy it into the codebase.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No link to external data seems unrelated to module structure.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to odoo-web-guidelines as a new 0003 Assets convention file, together with the no-minified-libraries rule from the docs, since both are asset concerns.

custom populate generators.
- File names use only `[a-z0-9_]`. Don't link external data by URL (images,
libs) — copy it into the codebase.
- Permissions: directories 755, files 644 — no executable bit on source files.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait what, does this need to be specified? I don't think I've ever checked or changed the permission of a code file in my career, the default has always been good.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped (came from runbot checks fyi).

# Batch ORM calls

Make **one** ORM call for the whole collection; an ORM call inside a
`for` loop over records or vals is almost always a bug.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a "bug", just less efficient code.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

```python
# bad — one INSERT round-trip per record
for vals in vals_list:
self.env['sale.order.line'].create(vals)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we possibly use fake model names here?

I don't want to find this file whenever I grep for sale.order.line.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to a schematic library.book model.

Comment thread skills/odoo-review/SKILL.md Outdated
Comment on lines +38 to +39
The review is complete when every changed file has been checked against every
guideline that matches it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say this is only the start of the review, not the end.

Checking guidelines is necessary but certainly not sufficient to consider a review done.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and the overrides-outside-the-diff concern is now its own section.

Comment on lines +4 to +8
Security audit of Odoo addon code: access control (ir.access, field groups,
sudo), injection (SQL, domain, eval, XSS), untrusted public methods/RPC,
controller auth/CSRF, file access, deserialization, returning complex
objects, getattr/setattr, timing attacks. Use when auditing an addon, or
judging whether a specific construct (a sudo, raw SQL, a route, …) is safe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say markup should be in the description somewhere.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Comment thread skills/odoo-security/SKILL.md Outdated
# Security audit of Odoo code

Audit Odoo addons for the framework-specific ways access control and injection
go wrong. **Never weaken security to make a feature work.**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait what? We're asked to do that on a regular basis (we try our best to mitigate the potential issues, if anything).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rephrased: minimum access, narrowest scope, with a comment; a silent widening is the finding.

Comment on lines +8 to +10
- Class naming: avoid `id` selectors; prefix classes with `o_<module>` (just
`o_` for the webclient). Use the flat "grandchild" approach
(`o_element_entry`), not hyper-specific nested names.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts the line below, _ vs -

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified: classes use underscores, SCSS identifiers hyphens.

Comment thread skills/odoo-review/SKILL.md Outdated
performance, tests; its table maps file types to guideline files.
- [`odoo-web-guidelines`](../odoo-web-guidelines/SKILL.md) — everything
under `static/`: JavaScript, Owl templates, SCSS.
- If the diff touches controllers, ACLs or record rules, `sudo()`, raw SQL,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"ACLs or record rules" → only before saas-19.4
Instead I suggest "access rules" (ir.access records).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@fvdsn
fvdsn force-pushed the master-js-guidelines-ged branch from ee4d31d to 5503f0c Compare September 4, 2026 08:50
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch 7 times, most recently from 45bfdaa to 2070840 Compare September 4, 2026 10:33
Before this commit, the house rules for addon code and the way to
review or audit an addon against them only existed in the heads of
reviewers and in scattered documentation that lags the code.

After this commit, skills/ holds three agent skills alongside
odoo-web-guidelines, following the same structure:

- odoo-guidelines: the house rules for addon code outside static/,
  one numbered file per domain (module structure, manifest,
  Python/ORM, fields, controllers, XML views and data, QWeb reports,
  access rights, performance, tests), dispatched by a routing table
  keyed on the files being touched. Rules stay one-line bullets by
  default; the trap-shaped ones (translation interpolation, view
  inheritance, batch ORM calls) get the full treatment with a bad and
  a fixed example. Authoring conventions live in AUTHORING.md.
- odoo-review: a thin review-process skill, dispatching each changed
  file to the matching guidelines (including odoo-web-guidelines),
  with the stable-vs-master policy, a version-trap warning, and an
  explicit completion criterion.
- odoo-security: a security-audit skill routed by a sweep table (16
  grep patterns, each dispatched to its pitfall section): access
  control in ir.access vocabulary, sudo and x2many commands, SQL and
  domain injection, XSS through markup()/t-out, CSRF on http routes,
  safe_eval, file access, RPC surface, returning complex objects.

The SCSS conventions go to odoo-web-guidelines (0002), so the two
guideline skills split on static/ versus everything else.

Every claim was verified against the documentation and then against
the master source, which overrules the docs where they disagree:
ir.model.access and ir.rule are unified into ir.access, t-raw and the
restricted pickle no longer exist, CSRF validation only applies to
type='http' routes, standard,post_install is the default test tag,
_sql_constraints is ignored in favour of models.Constraint. Examples
are schematic only and never point at real files, since nothing checks
such references and a stale one misleads more than none.

A README.md at the root of skills/ explains what the directory holds,
where to copy it depending on the agent harness, and that the skills
must be installed together since they reference each other.
@fvdsn
fvdsn force-pushed the master-workspace-skills-fva branch from 2070840 to f2f3456 Compare September 4, 2026 11:02
@fvdsn fvdsn mentioned this pull request Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants