Scope enforcement
Every tool call is checked against the caller’s token once, before the tool body runs, using the same scope groups as the REST API (see Scopes):READ-class tools require a CRM read (or manage) scope.WRITEandCOST_BEARING-class tools require a CRM manage scope.
viewer-role credential that only carries read scopes gets rejected on any write tool, with no silent downgrade. Because the check is derived from the tool’s declared class rather than a second, independently-set flag, it can’t drift out of sync with what the tool actually does.
Org isolation is structural, not per-tool code
A tool’s arguments never include an organization id, and a client-suppliedorganization_id-shaped field in the call arguments is simply ignored if you try to pass one — the caller’s organization comes entirely from the credential (fixed on an ak_ key, or supplied via the X-Anyreach-Org header for a pat_ token and re-verified against Logto on every connection). Every downstream data access is additionally scoped by Postgres row-level security keyed off that same organization claim. There is no tool argument that can widen a call’s reach to another organization’s data.
Name and ID resolution
Most tools that take a record reference (a deal, account, or contact) accept either its UUID or a plain name, and resolve it server-side:- An exact, case-insensitive name match resolves immediately.
- Multiple plausible matches return a structured
ambiguouserror with up to 8 candidates, rather than guessing which one you meant.
ambiguous error, re-issue the call with the UUID from the candidate list.
Idempotency
Exactly five tools accept an optionalidempotency_key, and they are the ones that create a record or start a paid job: create_account, create_contact, create_deal, log_activity, and cx_start_run. Retrying the same call with the same key returns the original result instead of repeating the effect; if you omit the key, one is derived by hashing the normalized call arguments, so an exact retry is still caught.
This is a best-effort, in-process cache (entries expire after 24 hours), not a distributed guarantee. It exists to stop the realistic failure mode — an agent retrying a call within one conversation — not to guarantee exactly-once semantics across a restart or a genuinely concurrent duplicate request. For a Customer Intelligence run, a retry that evades this cache means spending provider money twice.
Superadmin-gated tools
A handful of Customer Intelligence tools and fields are restricted to AnyCRM’s own platform operators, independent of any scope a customer’s credential can carry:cx_list_runs,cx_get_run,cx_start_run, andget_jobredact cost/spend figures for anyone who isn’t a superadmin, rather than refusing the call outright.cx_get_result_metricsrefuses non-superadmins entirely.cx_set_manual_demoandcx_restore_cx_reportare superadmin-only.cx_edit_cx_report/cx_preview_cx_reportgate a few specific fields (competitive analysis, brand data) to superadmins while leaving the rest of the report editable by anyone with CRM manage scope.
Limits
Error shape
Every tool failure — a scope check, a validation error, a timeout, an oversized result, an ambiguous reference, anything — comes back as a structured object rather than a transport-level failure, so a model can readerror.code and decide what to do next:
not_found is also what you get for a record that exists but belongs to a different organization — row-level security makes the two cases indistinguishable on purpose, the same way the REST API never confirms a cross-org record exists via a 403 (see Errors & Pagination).