Skip to content

2.1.0: execute_action and execute_method, with a forced permission prompt for irreversible tools - #53

Merged
matty-drexler merged 1 commit into
mainfrom
release-2.1.0
Aug 8, 2026
Merged

2.1.0: execute_action and execute_method, with a forced permission prompt for irreversible tools#53
matty-drexler merged 1 commit into
mainfrom
release-2.1.0

Conversation

@matty-drexler

Copy link
Copy Markdown
Contributor

What this changes

Adds two tools — execute_action (run an ir.actions.server) and execute_method (press a workflow button) — behind a shared ExecutionGuard, and makes unlink_record and execute_action prompt a human on every call regardless of Claude Code's permission mode.

Why

Brass-Monkey could read server actions but never run them, and had no way to press a workflow button. An agent wanting to confirm a sales order had to fake it by writing the state field — which sets a string and skips every side effect Odoo attaches to the real transition, leaving the database inconsistent.

The two tools are split deliberately rather than merged. Model methods are declared by the model and can be validated against live views before the call; server actions are mutable data rows that can change between introspection and execution. Different validation models, different tools.

execute_action

  • Expands the whole child_ids tree and classifies on the union, so a multi wrapper containing one Python step is treated as Python.
  • Blocks code / webhook / mail_post / sms behind acknowledge_unsafe — arbitrary execution, and outbound egress to third parties.
  • Snapshots only the declarative states; where it cannot, the audit entry records why rather than logging an empty object.
  • Returns any follow-up action as inert data. Never chains.

execute_method

  • Hard, non-overridable deny-list on ORM primitives, so it cannot become a bypass around the guards on write_record / create_record / unlink_record.
  • action_ / button_ / toggle_ naming convention; anything else needs acknowledge_unsafe.
  • Verifies the method is bound to a <button type="object"> in the combined view arch, which covers Studio-added buttons.
  • Snapshots workflow state markers around the call.

The permission-mode hole this also closes

acknowledge_unsafe assumes a human reads the refusal. Claude Code's auto and bypassPermissions modes remove the prompt entirely, so the agent could read its own refusal and immediately retry with the flag set — nobody ever sees the Python. unlink_record and execute_action now advertise _meta["anthropic/requiresUserInteraction"], which forces the prompt in every permission mode and suppresses "don't ask again".

Deliberately not set on write_record, create_record or execute_method: it prompts on every call, and applying it to routine data work would make auto mode unusable. Limits — Claude Code only, v2.1.199+, static, and headless --permission-prompt-tool converts allow to deny — are written up in docs/execution-safety.md.

How it was verified

  • npm test passes locally — 98 tests, 16 files
  • npm run build succeeds; bundle regenerated
  • tools/list exercised over stdio against the built bundle: 25 tools, requiresUserInteraction present on exactly unlink_record and execute_action
  • Tested against a live Odoo instance — NOT DONE. The run_run fallback and the view-arch regex are precisely the parts mocks cannot validate. Needs an MCP Inspector pass before tagging.
  • Docs / skills/ guidance updated to match (odoo-dev, odoo-data-ops, new docs/execution-safety.md)

Checklist

  • dist/ is included — same-repo maintainer release path, which contributor-guard.yml explicitly permits
  • DCO sign-off not required for same-repo PRs per the same workflow
  • No credentials, live URLs, database names, or real customer data in the diff
  • Both schema systems updated for the new tools — static JSON in src/tools/schemas.ts and co-located Zod
  • Both new tools require an explicit justification and write to all three audit channels
  • Tests added — tests/execution-tools.test.ts (14 cases) plus _meta assertions in the integration test

Also in this change

package-lock.json had drifted from package.json on two counts: the version (still 2.0.1) and the engines block added by the dependency roundup. Regenerated. Worth noting scripts/sync-version.mjs does not cover the lockfile — a gap that will recur next release.

Separately, GitHub is now reporting 2 vulnerabilities on main (1 high, 1 low). Unrelated to this PR, but worth a look.

…guard

Brass-Monkey could read server actions but never run them, and had no way to
press a workflow button — so an agent wanting to confirm a sales order had to
fake it by writing the `state` field, skipping every side effect Odoo attaches
to the real transition.

Two tools, split deliberately rather than merged. Model methods are declared by
the model and can be validated against live views before the call; server
actions are mutable data rows that can change between introspection and
execution. Different validation models, different tools.

execute_action (ir.actions.server):
  - expands the whole child_ids tree and classifies on the union, so a `multi`
    wrapper containing one Python step is treated as Python
  - blocks code/webhook/mail_post/sms behind acknowledge_unsafe — arbitrary
    execution and outbound egress to third parties
  - snapshots only the declarative states, and records why when it cannot
  - returns any follow-up action as inert data; never chains

execute_method (workflow buttons):
  - hard, non-overridable deny-list on ORM primitives, so it cannot become a
    bypass around the guards on write_record/create_record/unlink_record
  - action_/button_/toggle_ naming convention, else acknowledge_unsafe
  - verifies the method is bound to a <button type="object"> in the combined
    view arch, which covers Studio-added buttons too
  - snapshots workflow state markers around the call

Shared ExecutionGuard owns justification, target resolution, snapshot capture
and the triple audit write, so the two tools cannot drift apart on safety.

Also closes a hole the new tools would otherwise have: acknowledge_unsafe
assumes a human reads the refusal, but Claude Code's auto and bypassPermissions
modes remove the prompt entirely, letting the agent retry its own refusal with
the flag set. unlink_record and execute_action now advertise
_meta["anthropic/requiresUserInteraction"], which forces the prompt in every
permission mode. Deliberately not set on the routine write tools, which would
make auto mode unusable. Limits are documented in docs/execution-safety.md.

Version synced to 2.1.0 across the four manifests; package-lock had drifted
from package.json on both version and the engines block added by the dependency
roundup, and is now regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHB8rQL4x7Vog8D1sret5X
@matty-drexler
matty-drexler merged commit 57211ce into main Aug 8, 2026
3 checks passed
@matty-drexler
matty-drexler deleted the release-2.1.0 branch August 8, 2026 04:33
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.

1 participant