From 6892869bca99c33b0a909c353ece50c2bd6530c4 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 8 Sep 2026 14:05:19 -0400 Subject: [PATCH 1/9] docs(contextual-access): document group-based tool access Adds the public configuration page for the group_access extension, alongside the existing rate-limiting page: how visibility gating is enforced, tool matcher precedence, the three identity backends, Dashboard and API setup, the staleness model, and fail-closed behavior. Two things get more space than their size suggests, because both fail silently rather than loudly: - identity matching. The backend looks a caller up by the identity the request carries, so an Arcade user ID that does not equal the directory login matches nothing, and fail-closed turns that into tools quietly missing. - SailPoint's API client rights. `idn:access-profiles:read` and `idn:roles:read` sound correct and are not sufficient; they authorize reading definitions rather than assignments, so a client with only those authenticates and then fails every lookup. Also records that a tool no rule matches stays visible, so a mistyped matcher governs nothing instead of hiding everything. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../governance/contextual-access/_meta.tsx | 3 + .../contextual-access/group-access/page.mdx | 203 ++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 app/en/operate/governance/contextual-access/group-access/page.mdx diff --git a/app/en/operate/governance/contextual-access/_meta.tsx b/app/en/operate/governance/contextual-access/_meta.tsx index e41331ffd..9c26acccb 100644 --- a/app/en/operate/governance/contextual-access/_meta.tsx +++ b/app/en/operate/governance/contextual-access/_meta.tsx @@ -7,6 +7,9 @@ export const meta: MetaRecord = { "rate-limiting": { title: "Rate Limiting", }, + "group-access": { + title: "Group-Based Tool Access", + }, examples: { title: "Running an Extension", }, diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx new file mode 100644 index 000000000..553963bf9 --- /dev/null +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -0,0 +1,203 @@ +--- +title: "Group-Based Tool Access" +description: "Vary which tools a caller can see and use based on their identity provider group membership, enforced natively by the Arcade Engine" +--- + +import { Callout, Steps } from "nextra/components"; + +# Group-Based Tool Access + +As a platform operator, you can vary which tools a caller sees based on who they are. Group-based access is built into the Arcade Engine: you define rules that require membership in an identity provider group, and the Engine resolves membership and filters the tool list before the caller ever sees it. Like rate limits, this runs natively inside Arcade, so there is no server to build or host. + +Use it when one shared MCP gateway serves more than one team or customer and they should not all see the same tools. Without it, the alternatives are a separate gateway per access boundary or a custom webhook extension. + +## How enforcement works + +A group access configuration has two parts: an **identity backend** that holds your groups, and a set of **rules**. Each rule combines two things: + +- A **tool matcher** that selects which tools the rule applies to +- One or more **required groups** the caller must belong to + +Enforcement runs at the tool access hook point, so it applies to both discovery and execution: + +- A caller who does not satisfy a matched rule has the tool **removed from the response** rather than being shown a tool that returns an error. +- Naming that tool directly is also refused, so hiding is not the only enforcement. A caller who guesses a tool name gains nothing. + +Membership is resolved by querying your identity provider, then cached per user and per group for five minutes by default. + + + Multiple required groups on one rule are combined with OR: the caller needs + membership in any one of them, not all of them. + + +### Tool matchers + +| Matcher | Example | Applies to | +| --- | --- | --- | +| Exact | `Slack.SendMessage` | One fully qualified tool | +| Toolkit | `Slack.*` | Every tool in the toolkit | +| Global | `*` | Every tool | + +When several rules match the same call, only the most specific rule applies: an exact match beats a toolkit match, and a toolkit match beats the global match. The caller is evaluated against that one rule only. + +**A tool no rule matches stays visible.** This matters when you write a matcher by hand: a typo produces a rule that governs nothing, rather than a rule that hides everything. Use a global `*` rule when you want a default that covers tools you have not named. + +## Choosing an identity backend + +One configuration uses one backend. All three are read-only integrations that never write to your directory. + +| Backend | You provide | Access it needs | +| --- | --- | --- | +| Okta | Org URL and an API token | Read access to users and groups | +| Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | +| SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | + +With Okta and Entra ID, a rule's required groups are group names. With SailPoint, they are access profile or role names. + +### Matching a caller to a directory user + +The backend looks the caller up in your directory **by the identity the request carries**. In practice the Arcade user ID has to equal the directory login exactly. + +This is the most common source of surprise in a real deployment. A caller whose Arcade identity is an internal ID, or an email that differs in case or domain from the directory login, matches nothing. Because the hook fails closed, that surfaces as tools quietly missing for everyone rather than as an error someone can act on. Confirm that the identity your gateway sends is the same string your directory knows the person by before you turn a rule on. + +## SailPoint Identity Security Cloud + +SailPoint support has constraints the other two backends do not. + +**Identity Security Cloud only.** IdentityIQ is a separate product with a different API surface and is not supported. + +**Rules match access profile and role names.** Not group names, and not raw entitlements or identity attributes. A rule requiring `Finance-Analyst` is satisfied when the caller holds an access profile or a role by that name. + +**Only provisioned access counts.** Access that has been requested or approved but not yet provisioned does not satisfy a rule. This is deliberate: a rule should reflect what someone actually has. + +### Required API client rights + +Create a read-only API client in Identity Security Cloud using the OAuth 2.0 client credentials grant. No interactive user is needed. Grant it exactly: + +- `idn:identity-history:read` +- `idn:public-identities:read` + + + `idn:access-profiles:read` and `idn:roles:read` are **not sufficient**, even + though they sound like the correct ones. They authorize reading access + profile and role *definitions*, not which identity has been assigned them. A + client granted only those authenticates successfully and then fails every + membership lookup, which fail-closed behavior turns into every gated tool + disappearing for every caller. + + +### Identity matching in SailPoint + +SailPoint's access read is keyed on an internal identity ID rather than a login, so an email caller is resolved to that ID first. Two cases are reported as errors rather than as a clean denial, so that a mapping mistake stays diagnosable instead of looking like a working deny: + +- No identity matches the caller +- More than one identity matches the caller + +### Consider gating on the provisioned groups instead + +Many organizations use SailPoint to provision access into Okta or Entra ID. If that describes you, pointing a rule at the resulting Okta or Entra group is simpler to operate and closer to real time, because you skip a layer of propagation. Reach for the SailPoint backend when the access you want to gate on lives only in SailPoint. + +## Configure in the Dashboard + + + +### Create a group access extension + +Navigate to **Contextual Access** in the Arcade Dashboard, click **Add Extension**, and choose the group access type. + +### Pick a scope + +Bind the configuration to the organization to apply it across all projects, or to a single project. + +### Select a backend and enter credentials + +Choose Okta, Microsoft Entra ID, or SailPoint ISC, then fill in that backend's fields. Credentials are stored write-only: they are never returned, so the form cannot show you a stored secret later. + +### Add rules + +Each rule row takes a tool matcher and a comma-separated list of required groups. You can add up to 100 rules, each matcher can appear only once, and each rule accepts up to 50 groups. + +### Activate + +The **Active** toggle controls enforcement. Inactive configurations are kept but not enforced, so you can stage rules before turning them on. + + + +Rules and credentials can both be edited after creation. Leave a credential field blank when editing to keep the stored value. + + + A project can have one group access configuration at a time. To combine + more than one set of rules, put them in one configuration. + + +## Configure via the API + +Create a configuration with the plugins API. The example below hides every Slack tool from callers outside two Okta groups, restricts one specific tool to a narrower group, and leaves every other tool visible: + +```bash +curl -s -X POST "https://api.arcade.dev/v1/orgs/{org_id}/projects/{project_id}/plugins" \ + -H "Authorization: Bearer $ARCADE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "plugin_type": "group_access", + "name": "Team tool boundaries", + "group_access_config": { + "backend": "okta", + "okta": { + "org_url": "https://example.okta.com", + "api_token": "'"$OKTA_API_TOKEN"'" + }, + "rules": [ + { "match": "Slack.SendMessage", "required_groups": ["comms-team"] }, + { "match": "Slack.*", "required_groups": ["comms-team", "support-team"] } + ] + } + }' +``` + +Swap the `okta` block for `entra` or `sailpoint` to use another backend: + +```json +{ "backend": "entra", "entra": { "tenant_id": "...", "client_id": "...", "client_secret": "..." } } +``` + +```json +{ "backend": "sailpoint", "sailpoint": { "tenant_url": "https://acme.identitynow.com", "client_id": "...", "client_secret": "..." } } +``` + +To bind the configuration to the organization instead of a project, post to `/v1/orgs/{org_id}/plugins`. The [API reference](/references/api) documents the full plugins API, including listing, updating, and deleting. + +When you update a configuration, rules are replaced as a set rather than merged, because a rule has no identifier of its own. Send the complete list you want. Credentials behave the opposite way: omit one and the stored value is kept. + +## How quickly a change takes effect + +Effective delay is the membership cache lifetime plus however long your identity provider takes to converge on the change. Both directions are worth understanding: + +- **Granting access fails safe.** A caller stays without the tool until the change propagates. +- **Revoking access is bounded, not immediate.** A caller who is removed from a group can keep seeing a gated tool until the cached answer expires. + +Identity providers are themselves eventually consistent, and in testing a group removal took roughly ten seconds to appear in one provider's transitive membership data. If you need tighter propagation than the default five minutes, lower the cache lifetime on the hook. + +## When the platform cannot verify membership + +If membership cannot be determined, because the provider is unreachable or does not recognize the caller, the tool is **hidden**. A degraded provider must not silently stop enforcing the boundaries you rely on. + +For rules where availability matters more than the boundary, you can opt individual rules into the opposite behavior with `allow_on_unavailable`: + +```json +{ "match": "Slack.*", "required_groups": ["comms-team"], "allow_on_unavailable": true } +``` + + + Because unverifiable membership hides tools, a credential problem and a + revoked user look identical to the caller. If tools disappear unexpectedly + for everyone at once, check the backend credentials and the identity mapping + before looking at group membership. + + +## Next steps + +- [How hooks work](/operate/governance/contextual-access/how-hooks-work) - Where group access fits in the hook pipeline +- [Rate limiting](/operate/governance/contextual-access/rate-limiting) - Cap how often tools can be called +- [Build your own](/operate/governance/contextual-access/build-your-own) - Enforce custom policies from your own webhook server +- [API reference](/references/api) - Full plugins API documentation From b8e53c15892c9d0967ada477d1cba7136ad35030 Mon Sep 17 00:00:00 2001 From: "arcade-docs-bot[bot]" <321924871+arcade-docs-bot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:08:03 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=A4=96=20Regenerate=20LLMs.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/llms.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/llms.txt b/public/llms.txt index 29f0f7497..addc9a3df 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,4 +1,4 @@ - + # Arcade @@ -132,6 +132,7 @@ Arcade docs serve two audiences. Start with the path that matches your goal: - [Get Formatted Tool Definitions](https://docs.arcade.dev/en/build/tool-calling/custom-apps/get-tool-definitions): Documentation page - [Getting Your API Key](https://docs.arcade.dev/en/get-started/setup/api-keys): This documentation page guides users on how to obtain and manage their Arcade API key, detailing the steps for generating keys through both the Arcade dashboard and CLI. It emphasizes the importance of securely storing API keys, as they serve as administrator credentials that grant access to - [Governance](https://docs.arcade.dev/en/operate/governance): Documentation page +- [Group-Based Tool Access](https://docs.arcade.dev/en/operate/governance/contextual-access/group-access): Documentation page - [Handle Errors](https://docs.arcade.dev/en/build/create-tools/error-handling): Documentation page - [Hosting options overview](https://docs.arcade.dev/en/operate/deploy): Documentation page - [How Hooks Work](https://docs.arcade.dev/en/operate/governance/contextual-access/how-hooks-work): Documentation page From 3b4398686e88c711be8f89febfa84f5db6996693 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 8 Sep 2026 15:03:41 -0400 Subject: [PATCH 3/9] docs(contextual-access): how to name an Entra group in a rule The page said a required group is a group name for both Okta and Entra ID. That is no longer accurate for Entra: a rule may carry the group's object ID, and a display name is honoured only for a security group that is not a Microsoft 365 group. The reason is worth an operator's attention rather than a footnote. Entra does not require display names to be unique and many tenants let ordinary members create groups, so a display-name rule can be satisfied by someone who creates a group with that name and joins it. Restricting the match to ordinary security groups removes the surface most members can add to but is not a guarantee, so the page recommends the object ID for anything sensitive and says where to copy it from. Also notes that this does not apply to Okta or SailPoint, where creating a group, access profile, or role is an administrative action. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../contextual-access/group-access/page.mdx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 553963bf9..4cc8b8d53 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -52,7 +52,26 @@ One configuration uses one backend. All three are read-only integrations that ne | Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | | SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | -With Okta and Entra ID, a rule's required groups are group names. With SailPoint, they are access profile or role names. +With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are access profile or role names. + +### Naming an Entra group: name or object ID + +Entra does not require group display names to be unique. Its object ID is the only unique key, and many tenants let ordinary members create groups. A rule that requires the display name `finance-admins` could therefore be satisfied by someone who creates their own group with that name and joins it, without ever belonging to the group you meant. + +Two ways to name an Entra group in a rule: + +| Rule value | Matched against | Collision risk | +| --- | --- | --- | +| `finance-admins` | `displayName`, and only for a security group that is not a Microsoft 365 group | Possible, if your tenant lets members create security groups | +| `3f2504e0-4f89-11d3-9a0c-0305e82c3301` | the group's object ID | None. It is unique, and it follows the group through a rename | + +Restricting a display-name match to ordinary security groups removes the surface most members can add to, but it is not a guarantee. **For anything sensitive, put the group's object ID in the rule.** You can copy it from the group's overview page in Entra. + + + This does not apply to Okta or SailPoint. Group, access profile, and role + creation there are administrative actions, so an ordinary caller cannot + introduce a colliding name. + ### Matching a caller to a directory user From f1e4853a0c64914d96f0c88d0fe200afac5b12a5 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 8 Sep 2026 15:49:19 -0400 Subject: [PATCH 4/9] docs(contextual-access): a SailPoint access profile is named by its ID The page said rules match access profile and role names. That is right for a role and wrong for an access profile, and wrong in a way that would send an operator to configure something that cannot work. SailPoint's access-items response documents `displayName` as the role display name on a role, but as the identity's display name on an access profile, and it carries no access profile name at all. There is nothing for a profile-name rule to match, so the rule carries the profile's ID. The page now states which value each rule shape matches, says where to copy the ID from, and explains why we do not resolve names for you: it would need `idn:access-profiles:read`, which the required-rights section deliberately excludes. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../contextual-access/group-access/page.mdx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 4cc8b8d53..85b8b8471 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -52,7 +52,7 @@ One configuration uses one backend. All three are read-only integrations that ne | Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | | SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | -With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are access profile or role names. +With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are role names or access profile IDs, which is also covered below. ### Naming an Entra group: name or object ID @@ -85,7 +85,17 @@ SailPoint support has constraints the other two backends do not. **Identity Security Cloud only.** IdentityIQ is a separate product with a different API surface and is not supported. -**Rules match access profile and role names.** Not group names, and not raw entitlements or identity attributes. A rule requiring `Finance-Analyst` is satisfied when the caller holds an access profile or a role by that name. +**Rules match roles and access profiles, not groups.** Raw entitlements and identity attributes do not count either. How you name each one differs: + +| Rule value | Matched against | +| --- | --- | +| `Finance-Approver` | a role's name | +| a role's ID | that role | +| an access profile's ID | that access profile | + +**An access profile has to be named by its ID.** SailPoint's access-items response documents `displayName` as the *role* display name on a role, but as *the identity's* display name on an access profile, and it carries no access profile name at all, so a profile-name rule has nothing to match against. Copy the ID from the access profile's page in Identity Security Cloud. + +Resolving profile names to IDs for you would mean reading `/v3/access-profiles`, which needs `idn:access-profiles:read`. The section below deliberately excludes that right, so the rule carries the ID instead. **Only provisioned access counts.** Access that has been requested or approved but not yet provisioned does not satisfy a rule. This is deliberate: a rule should reflect what someone actually has. From dc35c0712d80ab9fcf4048c6da755b76d03f6f8a Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Wed, 9 Sep 2026 10:00:21 -0400 Subject: [PATCH 5/9] docs(contextual-access): a SailPoint role or access profile can be named or ID'd An earlier revision said an access profile had to be named by its ID, on the strength of SailPoint's SDK documenting `displayName` as the identity's display name on an access-profile row. Checked against a live tenant, that field carries the access profile's own name and varies from row to row, so profiles are nameable and requiring an ID was stricter than the product needs. Both kinds can now be named either way, with IDs recommended for anything sensitive because they cannot collide and survive a rename. The callout explains the one rule that keeps both tenant behaviours safe: a name equal to the caller's own identity display name is never a match. Also drops the note about `idn:access-profiles:read`, which is no longer relevant now that no name-to-ID resolution is needed. Refs PLT-3234. Co-Authored-By: Claude Opus 5 --- .../contextual-access/group-access/page.mdx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 85b8b8471..6a72cfcb3 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -52,7 +52,7 @@ One configuration uses one backend. All three are read-only integrations that ne | Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | | SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | -With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are role names or access profile IDs, which is also covered below. +With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are role or access profile names, or their IDs, which is also covered below. ### Naming an Entra group: name or object ID @@ -85,17 +85,23 @@ SailPoint support has constraints the other two backends do not. **Identity Security Cloud only.** IdentityIQ is a separate product with a different API surface and is not supported. -**Rules match roles and access profiles, not groups.** Raw entitlements and identity attributes do not count either. How you name each one differs: +**Rules match roles and access profiles, not groups.** Raw entitlements and identity attributes do not count either. Either kind can be named two ways: | Rule value | Matched against | | --- | --- | -| `Finance-Approver` | a role's name | -| a role's ID | that role | -| an access profile's ID | that access profile | +| `Finance-Approver` | the name of a role or an access profile | +| an ID | the role or access profile with that ID | -**An access profile has to be named by its ID.** SailPoint's access-items response documents `displayName` as the *role* display name on a role, but as *the identity's* display name on an access profile, and it carries no access profile name at all, so a profile-name rule has nothing to match against. Copy the ID from the access profile's page in Identity Security Cloud. +Names are easier to read in a rule. An ID is exact, cannot collide, and follows the item through a rename, so it is the better choice for anything sensitive. You can copy it from the item's page in Identity Security Cloud. -Resolving profile names to IDs for you would mean reading `/v3/access-profiles`, which needs `idn:access-profiles:read`. The section below deliberately excludes that right, so the rule carries the ID instead. + + One rule for both kinds: a name that matches the caller's *own* identity + display name is never treated as a match. Some tenants report an access + item's `displayName` as the identity's name rather than the item's, and + without that rule a person whose name happened to equal a rule's value would + satisfy it by holding any access at all. On tenants that report item names, + which is what we see in practice, this costs nothing. + **Only provisioned access counts.** Access that has been requested or approved but not yet provisioned does not satisfy a rule. This is deliberate: a rule should reflect what someone actually has. From 3030c28b0618a5e1e9b733b33fb118c846ff8b95 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Mon, 14 Sep 2026 15:24:26 -0400 Subject: [PATCH 6/9] docs(contextual-access): mark group-based tool access as Early Access The feature ships off by default behind a per-organization flag, so the page has to say so: without it a reader follows the Dashboard steps and finds no group access type in the picker, or calls the API and gets a 403, with nothing telling them why. Co-Authored-By: Claude Opus 5 --- .../governance/contextual-access/group-access/page.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 6a72cfcb3..60345b5d1 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -7,6 +7,13 @@ import { Callout, Steps } from "nextra/components"; # Group-Based Tool Access + + **Early Access.** Group-based tool access is off by default and turned on per + organization. Contact Arcade support to have it enabled for yours. While it is + off, the group access extension type is not offered in the Dashboard and the + API refuses to create one. + + As a platform operator, you can vary which tools a caller sees based on who they are. Group-based access is built into the Arcade Engine: you define rules that require membership in an identity provider group, and the Engine resolves membership and filters the tool list before the caller ever sees it. Like rate limits, this runs natively inside Arcade, so there is no server to build or host. Use it when one shared MCP gateway serves more than one team or customer and they should not all see the same tools. Without it, the alternatives are a separate gateway per access boundary or a custom webhook extension. From 62be0246555c8ac0b6b8efe0dded8c2458f778ac Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Tue, 22 Sep 2026 19:56:20 -0400 Subject: [PATCH 7/9] docs(group-access): stop presenting the caller identity as an email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page described caller matching in terms of an email differing "in case or domain from the directory login", which reads as though Arcade expects an email and compares it. It does neither: the value is passed to the directory untouched, and the directory decides what it matches against. Adds a table of what each backend actually does with it, including that SailPoint tries the identity's email or its alias — which of those carries a login is a property of how a directory was populated, not of SailPoint. Also documents two things operators could previously only discover by experiment: - group names match exactly, including case, while tool matchers do not. A mis-cased matcher governs nothing, whereas a loosely matched group widens who satisfies the rule, so the asymmetry is deliberate. Group IDs fold case, because an ID is opaque. - every backend accepts a name or an ID for a required group. Okta previously took only a name, so the page said so; it now takes either. --- .../contextual-access/group-access/page.mdx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 60345b5d1..0a1442ff4 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -45,6 +45,8 @@ Membership is resolved by querying your identity provider, then cached per user | Toolkit | `Slack.*` | Every tool in the toolkit | | Global | `*` | Every tool | +Group names are matched **exactly, including case**, while tool matchers are not. A rule requiring `Finance-Admins` is not satisfied by membership in `finance-admins`. The asymmetry is deliberate: a mis-cased tool matcher governs nothing, whereas a loosely matched group would widen who satisfies the rule. Group IDs are matched without regard to case, since an ID is opaque. + When several rules match the same call, only the most specific rule applies: an exact match beats a toolkit match, and a toolkit match beats the global match. The caller is evaluated against that one rule only. **A tool no rule matches stays visible.** This matters when you write a matcher by hand: a typo produces a rule that governs nothing, rather than a rule that hides everything. Use a global `*` rule when you want a default that covers tools you have not named. @@ -59,7 +61,7 @@ One configuration uses one backend. All three are read-only integrations that ne | Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | | SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | -With Okta, a rule's required groups are group names. With Entra ID they are group names or group object IDs, which is a distinction worth reading below. With SailPoint, they are role or access profile names, or their IDs, which is also covered below. +Every backend accepts either a name or an ID for a required group: an Okta group name or group ID, an Entra ID group name or object ID, a SailPoint role or access profile name or its ID. Names are easier to read in a rule; an ID is exact, cannot collide, and follows the item through a rename, so it is the better choice for anything sensitive. The Entra distinction is worth reading below, because a display name there is less safe than it looks. ### Naming an Entra group: name or object ID @@ -82,9 +84,17 @@ Restricting a display-name match to ordinary security groups removes the surface ### Matching a caller to a directory user -The backend looks the caller up in your directory **by the identity the request carries**. In practice the Arcade user ID has to equal the directory login exactly. +The backend looks the caller up in your directory **by the identity the request carries**. Arcade does not interpret that value or assume it is an email address: it is passed to your directory as-is, and your directory decides what it matches against. + +What each backend does with it differs: + +| Backend | How the caller is resolved | +| --- | --- | +| Okta | The value is used directly in the user lookup, so it must be an identifier Okta accepts for that user | +| Microsoft Entra ID | The same, against Microsoft Graph | +| SailPoint | Looked up against the identity's `email` **or** `alias`, because SailPoint's access read needs an internal identity ID first | -This is the most common source of surprise in a real deployment. A caller whose Arcade identity is an internal ID, or an email that differs in case or domain from the directory login, matches nothing. Because the hook fails closed, that surfaces as tools quietly missing for everyone rather than as an error someone can act on. Confirm that the identity your gateway sends is the same string your directory knows the person by before you turn a rule on. +This is the most common source of surprise in a real deployment. A caller whose Arcade identity is an internal ID, or a value that differs in case or domain from what the directory knows, matches nothing. Because the hook fails closed, that surfaces as tools quietly missing for everyone rather than as an error someone can act on. Confirm that the identity your gateway sends is one your directory recognizes before you turn a rule on. ## SailPoint Identity Security Cloud @@ -130,7 +140,7 @@ Create a read-only API client in Identity Security Cloud using the OAuth 2.0 cli ### Identity matching in SailPoint -SailPoint's access read is keyed on an internal identity ID rather than a login, so an email caller is resolved to that ID first. Two cases are reported as errors rather than as a clean denial, so that a mapping mistake stays diagnosable instead of looking like a working deny: +SailPoint's access read is keyed on an internal identity ID rather than a login, so the caller is resolved to that ID first, matching the identity's `email` or its `alias`. Which of those carries the login is a property of how your directory was populated rather than of SailPoint, which is why both are tried. Two cases are reported as errors rather than as a clean denial, so that a mapping mistake stays diagnosable instead of looking like a working deny: - No identity matches the caller - More than one identity matches the caller From 0f79fac0038029935d3c9c1f7dad17699adf5572 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:36:43 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=A4=96=20Regenerate=20LLMs.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/llms.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/llms.txt b/public/llms.txt index 92b51fe98..3927ee19e 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,4 +1,4 @@ - + # Arcade @@ -133,6 +133,7 @@ Arcade docs serve two audiences. Start with the path that matches your goal: - [Get Formatted Tool Definitions](https://docs.arcade.dev/en/build/tool-calling/custom-apps/get-tool-definitions): Documentation page - [Getting Your API Key](https://docs.arcade.dev/en/get-started/setup/api-keys): This documentation page guides users on how to obtain and manage their Arcade API key, detailing the steps for generating keys through both the Arcade dashboard and CLI. It emphasizes the importance of securely storing API keys, as they serve as administrator credentials that grant access to - [Governance](https://docs.arcade.dev/en/operate/governance): Documentation page +- [Group-Based Tool Access](https://docs.arcade.dev/en/operate/governance/contextual-access/group-access): Documentation page - [Handle Errors](https://docs.arcade.dev/en/build/create-tools/error-handling): Documentation page - [Hosting options overview](https://docs.arcade.dev/en/operate/deploy): Documentation page - [How Hooks Work](https://docs.arcade.dev/en/operate/governance/contextual-access/how-hooks-work): Documentation page From 3c32629931013e14201c53febe11c92de7b62568 Mon Sep 17 00:00:00 2001 From: Andrew Connor Date: Wed, 23 Sep 2026 17:40:44 -0400 Subject: [PATCH 9/9] docs(group-access): tone pass, and correct permissions and editing behavior Rewrite in the voice of the neighboring rate-limiting page: state behavior plainly, drop the rationale asides and internal implementation detail, and cut the page by about a quarter. Corrections: - Add Okta and Entra ID permission sections. Entra needs GroupMember.Read.All; User.Read.All alone returns groups without names, so name rules never match. - SailPoint needs exactly two rights; access-profile and role reads are not needed. - Remove the claim that the membership cache TTL can be lowered. It is fixed at five minutes (configurability tracked in PLT-3702). - Editing now requires the secret again when the backend, a URL, the tenant ID or the client ID changes. - Document the Dashboard toggle for allow_on_unavailable. - Drop the unverified timing claim and the provisioned-groups recommendation. Refs PLT-3234. Co-Authored-By: Claude Opus 5.5 --- .../contextual-access/group-access/page.mdx | 171 +++++++----------- 1 file changed, 68 insertions(+), 103 deletions(-) diff --git a/app/en/operate/governance/contextual-access/group-access/page.mdx b/app/en/operate/governance/contextual-access/group-access/page.mdx index 0a1442ff4..3d9c4e2eb 100644 --- a/app/en/operate/governance/contextual-access/group-access/page.mdx +++ b/app/en/operate/governance/contextual-access/group-access/page.mdx @@ -14,28 +14,20 @@ import { Callout, Steps } from "nextra/components"; API refuses to create one. -As a platform operator, you can vary which tools a caller sees based on who they are. Group-based access is built into the Arcade Engine: you define rules that require membership in an identity provider group, and the Engine resolves membership and filters the tool list before the caller ever sees it. Like rate limits, this runs natively inside Arcade, so there is no server to build or host. +As a platform operator, you can vary which tools a caller sees based on their identity provider groups. Group-based access is built into the Arcade Engine: you define rules that require membership in a group, and the Engine checks membership and filters the tool list before the caller sees it. Like [rate limits](/operate/governance/contextual-access/rate-limiting), it runs natively inside Arcade, so there is no server to build or host. -Use it when one shared MCP gateway serves more than one team or customer and they should not all see the same tools. Without it, the alternatives are a separate gateway per access boundary or a custom webhook extension. +Use it when one MCP gateway serves more than one team or customer and they should not all see the same tools, instead of running a separate gateway for each. ## How enforcement works -A group access configuration has two parts: an **identity backend** that holds your groups, and a set of **rules**. Each rule combines two things: +A group access configuration has an **identity backend** that holds your groups, and a set of **rules**. Each rule combines: - A **tool matcher** that selects which tools the rule applies to -- One or more **required groups** the caller must belong to +- One or more **required groups**. A caller in any one of them satisfies the rule. -Enforcement runs at the tool access hook point, so it applies to both discovery and execution: +Enforcement runs at the tool access hook point, so it covers both discovery and execution. A caller who does not satisfy a rule does not see the tool in their tool list, and a call to it by name is refused. -- A caller who does not satisfy a matched rule has the tool **removed from the response** rather than being shown a tool that returns an error. -- Naming that tool directly is also refused, so hiding is not the only enforcement. A caller who guesses a tool name gains nothing. - -Membership is resolved by querying your identity provider, then cached per user and per group for five minutes by default. - - - Multiple required groups on one rule are combined with OR: the caller needs - membership in any one of them, not all of them. - +The Engine checks membership with your identity provider and caches the answer per user and per group for five minutes. ### Tool matchers @@ -45,110 +37,86 @@ Membership is resolved by querying your identity provider, then cached per user | Toolkit | `Slack.*` | Every tool in the toolkit | | Global | `*` | Every tool | -Group names are matched **exactly, including case**, while tool matchers are not. A rule requiring `Finance-Admins` is not satisfied by membership in `finance-admins`. The asymmetry is deliberate: a mis-cased tool matcher governs nothing, whereas a loosely matched group would widen who satisfies the rule. Group IDs are matched without regard to case, since an ID is opaque. +When more than one rule matches a tool, only the most specific rule applies: an exact match beats a toolkit match, and a toolkit match beats the global match. The caller is checked against that one rule only. -When several rules match the same call, only the most specific rule applies: an exact match beats a toolkit match, and a toolkit match beats the global match. The caller is evaluated against that one rule only. +A tool that no rule matches stays visible to everyone. Add a global `*` rule to set a default for tools you have not named. -**A tool no rule matches stays visible.** This matters when you write a matcher by hand: a typo produces a rule that governs nothing, rather than a rule that hides everything. Use a global `*` rule when you want a default that covers tools you have not named. +Tool matchers are not case-sensitive. Group names are: a rule requiring `Finance-Admins` is not satisfied by membership in `finance-admins`. Group IDs are not case-sensitive. -## Choosing an identity backend +## Choose an identity backend -One configuration uses one backend. All three are read-only integrations that never write to your directory. +Each configuration uses one backend. All three integrations are read-only and never write to your directory. | Backend | You provide | Access it needs | | --- | --- | --- | -| Okta | Org URL and an API token | Read access to users and groups | -| Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | The `GroupMember.Read.All` application permission, with administrator consent granted | -| SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See the SailPoint section below, because the rights that sound correct are not the ones that work | - -Every backend accepts either a name or an ID for a required group: an Okta group name or group ID, an Entra ID group name or object ID, a SailPoint role or access profile name or its ID. Names are easier to read in a rule; an ID is exact, cannot collide, and follows the item through a rename, so it is the better choice for anything sensitive. The Entra distinction is worth reading below, because a display name there is less safe than it looks. +| Okta | Org URL and an API token | See [Okta API token](#okta-api-token) | +| Microsoft Entra ID | Directory (tenant) ID, application (client) ID, client secret | See [Entra ID app permissions](#entra-id-app-permissions) | +| SailPoint Identity Security Cloud | Tenant URL, client ID, client secret | See [Required API client rights](#required-api-client-rights) | -### Naming an Entra group: name or object ID +A required group can be a name or an ID in every backend: an Okta group name or ID, an Entra ID group name or object ID, or a SailPoint role or access profile name or ID. An ID is exact and survives a rename, so use IDs for sensitive rules. -Entra does not require group display names to be unique. Its object ID is the only unique key, and many tenants let ordinary members create groups. A rule that requires the display name `finance-admins` could therefore be satisfied by someone who creates their own group with that name and joins it, without ever belonging to the group you meant. +### Okta API token -Two ways to name an Entra group in a rule: +An Okta API token has the same permissions as the administrator who creates it. Create the token from an account whose administrator role can read users and groups, such as a service account with the Read-Only Administrator role. If that account's role changes, the token's permissions change with it. -| Rule value | Matched against | Collision risk | -| --- | --- | --- | -| `finance-admins` | `displayName`, and only for a security group that is not a Microsoft 365 group | Possible, if your tenant lets members create security groups | -| `3f2504e0-4f89-11d3-9a0c-0305e82c3301` | the group's object ID | None. It is unique, and it follows the group through a rename | +### Entra ID app permissions -Restricting a display-name match to ordinary security groups removes the surface most members can add to, but it is not a guarantee. **For anything sensitive, put the group's object ID in the rule.** You can copy it from the group's overview page in Entra. +Register an app in Entra ID and give it the `GroupMember.Read.All` **application** permission (not delegated), then grant administrator consent. Until consent is granted, the app can still sign in but every membership lookup fails. - - This does not apply to Okta or SailPoint. Group, access profile, and role - creation there are administrative actions, so an ordinary caller cannot - introduce a colliding name. + + `User.Read.All` alone is not enough. Without permission to read groups, + Microsoft Graph returns a user's groups with their IDs but not their names or + types, so every rule that names a group by display name matches nothing. -### Matching a caller to a directory user - -The backend looks the caller up in your directory **by the identity the request carries**. Arcade does not interpret that value or assume it is an email address: it is passed to your directory as-is, and your directory decides what it matches against. +### Entra ID group names -What each backend does with it differs: +Entra ID does not require group display names to be unique, and many tenants let members create their own groups. A rule that names `finance-admins` could be satisfied by someone who creates a group with that name and joins it. -| Backend | How the caller is resolved | +| Rule value | Matched against | | --- | --- | -| Okta | The value is used directly in the user lookup, so it must be an identifier Okta accepts for that user | -| Microsoft Entra ID | The same, against Microsoft Graph | -| SailPoint | Looked up against the identity's `email` **or** `alias`, because SailPoint's access read needs an internal identity ID first | +| `finance-admins` | The group's display name, for security groups only (not Microsoft 365 groups) | +| `3f2504e0-4f89-11d3-9a0c-0305e82c3301` | The group's object ID | -This is the most common source of surprise in a real deployment. A caller whose Arcade identity is an internal ID, or a value that differs in case or domain from what the directory knows, matches nothing. Because the hook fails closed, that surfaces as tools quietly missing for everyone rather than as an error someone can act on. Confirm that the identity your gateway sends is one your directory recognizes before you turn a rule on. +For sensitive rules, use the object ID. You can copy it from the group's overview page in Entra ID. This does not apply to Okta or SailPoint, where only administrators can create groups, roles, and access profiles. -## SailPoint Identity Security Cloud +### Match callers to directory users + +The backend looks up the caller in your directory using the user ID the request carries. Arcade passes that value to your directory unchanged and does not assume it is an email address. -SailPoint support has constraints the other two backends do not. +| Backend | How the caller is looked up | +| --- | --- | +| Okta | The user ID must be an identifier Okta accepts for that user | +| Microsoft Entra ID | The user ID must be an identifier Microsoft Graph accepts for that user | +| SailPoint | The user ID is matched against each identity's `email` and `alias` | -**Identity Security Cloud only.** IdentityIQ is a separate product with a different API surface and is not supported. +A caller whose user ID your directory does not recognize matches no groups, so every gated tool is hidden from them. Before you turn on a rule, confirm that the user IDs your gateway sends are ones your directory knows. -**Rules match roles and access profiles, not groups.** Raw entitlements and identity attributes do not count either. Either kind can be named two ways: +## SailPoint Identity Security Cloud -| Rule value | Matched against | -| --- | --- | -| `Finance-Approver` | the name of a role or an access profile | -| an ID | the role or access profile with that ID | +SailPoint support covers Identity Security Cloud only. IdentityIQ is not supported. -Names are easier to read in a rule. An ID is exact, cannot collide, and follows the item through a rename, so it is the better choice for anything sensitive. You can copy it from the item's page in Identity Security Cloud. +Rules match **roles and access profiles**. Entitlements and identity attributes do not count toward a rule. Name a role or access profile by its name, or by its ID from the item's page in Identity Security Cloud. - - One rule for both kinds: a name that matches the caller's *own* identity - display name is never treated as a match. Some tenants report an access - item's `displayName` as the identity's name rather than the item's, and - without that rule a person whose name happened to equal a rule's value would - satisfy it by holding any access at all. On tenants that report item names, - which is what we see in practice, this costs nothing. - +Only provisioned access counts. Access that has been requested or approved but not yet provisioned does not satisfy a rule. -**Only provisioned access counts.** Access that has been requested or approved but not yet provisioned does not satisfy a rule. This is deliberate: a rule should reflect what someone actually has. +A caller is matched to an identity by `email` or `alias`. If no identity matches, or more than one does, membership cannot be verified and the caller's gated tools are hidden. ### Required API client rights -Create a read-only API client in Identity Security Cloud using the OAuth 2.0 client credentials grant. No interactive user is needed. Grant it exactly: +Create a read-only API client in Identity Security Cloud using the OAuth 2.0 client credentials grant. It needs exactly two rights: - `idn:identity-history:read` - `idn:public-identities:read` - `idn:access-profiles:read` and `idn:roles:read` are **not sufficient**, even - though they sound like the correct ones. They authorize reading access - profile and role *definitions*, not which identity has been assigned them. A - client granted only those authenticates successfully and then fails every - membership lookup, which fail-closed behavior turns into every gated tool - disappearing for every caller. + `idn:access-profiles:read` and `idn:roles:read` are not needed, and they + cannot replace the two rights above. They allow reading role and access + profile definitions, not who holds them. A client with only those rights + authenticates, then fails every membership lookup, and every gated tool is + hidden from every caller. -### Identity matching in SailPoint - -SailPoint's access read is keyed on an internal identity ID rather than a login, so the caller is resolved to that ID first, matching the identity's `email` or its `alias`. Which of those carries the login is a property of how your directory was populated rather than of SailPoint, which is why both are tried. Two cases are reported as errors rather than as a clean denial, so that a mapping mistake stays diagnosable instead of looking like a working deny: - -- No identity matches the caller -- More than one identity matches the caller - -### Consider gating on the provisioned groups instead - -Many organizations use SailPoint to provision access into Okta or Entra ID. If that describes you, pointing a rule at the resulting Okta or Entra group is simpler to operate and closer to real time, because you skip a layer of propagation. Reach for the SailPoint backend when the access you want to gate on lives only in SailPoint. - ## Configure in the Dashboard @@ -163,7 +131,7 @@ Bind the configuration to the organization to apply it across all projects, or t ### Select a backend and enter credentials -Choose Okta, Microsoft Entra ID, or SailPoint ISC, then fill in that backend's fields. Credentials are stored write-only: they are never returned, so the form cannot show you a stored secret later. +Choose Okta, Microsoft Entra ID, or SailPoint ISC, then fill in that backend's fields. Secrets are write-only and are never shown again after you save. ### Add rules @@ -175,16 +143,16 @@ The **Active** toggle controls enforcement. Inactive configurations are kept but -Rules and credentials can both be edited after creation. Leave a credential field blank when editing to keep the stored value. +You can edit rules and credentials after you create the extension. When you edit, leave a secret blank to keep the stored one. If you change the backend, org URL, tenant URL, tenant ID, or client ID, you must enter the secret again. - A project can have one group access configuration at a time. To combine - more than one set of rules, put them in one configuration. + A project can have one group access configuration at a time. To apply more + than one set of rules, put them in the same configuration. ## Configure via the API -Create a configuration with the plugins API. The example below hides every Slack tool from callers outside two Okta groups, restricts one specific tool to a narrower group, and leaves every other tool visible: +Create a configuration with the plugins API. The example below hides every Slack tool from callers outside two Okta groups, limits `Slack.SendMessage` to one group, and leaves every other tool visible: ```bash curl -s -X POST "https://api.arcade.dev/v1/orgs/{org_id}/projects/{project_id}/plugins" \ @@ -207,44 +175,41 @@ curl -s -X POST "https://api.arcade.dev/v1/orgs/{org_id}/projects/{project_id}/p }' ``` -Swap the `okta` block for `entra` or `sailpoint` to use another backend: +To use another backend, replace the `okta` block with an `entra` or `sailpoint` block: ```json -{ "backend": "entra", "entra": { "tenant_id": "...", "client_id": "...", "client_secret": "..." } } +{ "backend": "entra", "entra": { "tenant_id": "11111111-2222-3333-4444-555555555555", "client_id": "66666666-7777-8888-9999-000000000000", "client_secret": "..." } } ``` ```json -{ "backend": "sailpoint", "sailpoint": { "tenant_url": "https://acme.identitynow.com", "client_id": "...", "client_secret": "..." } } +{ "backend": "sailpoint", "sailpoint": { "tenant_url": "https://acme.identitynow.com", "client_id": "2c9180835d2e5168015d32f890ca1581", "client_secret": "..." } } ``` To bind the configuration to the organization instead of a project, post to `/v1/orgs/{org_id}/plugins`. The [API reference](/references/api) documents the full plugins API, including listing, updating, and deleting. -When you update a configuration, rules are replaced as a set rather than merged, because a rule has no identifier of its own. Send the complete list you want. Credentials behave the opposite way: omit one and the stored value is kept. - -## How quickly a change takes effect +When you update a configuration, the rules you send replace the existing rules, so send the complete list. A credential you leave out keeps its stored value, unless the update changes the backend, a URL, the tenant ID, or the client ID, in which case the credential is required. -Effective delay is the membership cache lifetime plus however long your identity provider takes to converge on the change. Both directions are worth understanding: +## How quickly changes take effect -- **Granting access fails safe.** A caller stays without the tool until the change propagates. -- **Revoking access is bounded, not immediate.** A caller who is removed from a group can keep seeing a gated tool until the cached answer expires. +A membership change reaches Arcade after your identity provider applies it and the cached answer expires, which takes up to five minutes: -Identity providers are themselves eventually consistent, and in testing a group removal took roughly ten seconds to appear in one provider's transitive membership data. If you need tighter propagation than the default five minutes, lower the cache lifetime on the hook. +- **Granting access:** the caller does not see the tool until the change reaches Arcade. +- **Revoking access:** the caller can keep seeing the tool until the cached answer expires. -## When the platform cannot verify membership +## When membership cannot be verified -If membership cannot be determined, because the provider is unreachable or does not recognize the caller, the tool is **hidden**. A degraded provider must not silently stop enforcing the boundaries you rely on. +If the Engine cannot determine membership, because the identity provider is unreachable or does not recognize the caller, the tool is **hidden**. This keeps an outage from silently removing the boundaries you set. -For rules where availability matters more than the boundary, you can opt individual rules into the opposite behavior with `allow_on_unavailable`: +To show the tool instead when membership cannot be verified, turn on **Show the tool when membership cannot be verified** for the rule in the Dashboard, or set `allow_on_unavailable` on the rule in the API: ```json { "match": "Slack.*", "required_groups": ["comms-team"], "allow_on_unavailable": true } ``` - Because unverifiable membership hides tools, a credential problem and a - revoked user look identical to the caller. If tools disappear unexpectedly - for everyone at once, check the backend credentials and the identity mapping - before looking at group membership. + A credential problem and a removed user look the same to the caller: the tool + is hidden. If tools disappear for everyone at once, check the backend + credentials and the user ID mapping before you check group membership. ## Next steps