---
name: "maison-access"
description: "Shape permissions on Maison: roles, policies and user groups, each available to one hotel or to all clients (roles and policies may be Maison only). A policy is a list of rules (feature + level) and is never applied to a client directly — a role applies each of its policies at all clients or at named clients (Applies to). Also the feature catalogue and effective-access views. Use when designing who may do what, troubleshooting a 403, or granting a team a bundle of permissions. Tools: add_client_policy_statement, add_client_role_rule, add_role_policy, assign_role, assign_role_to_group, attach_client_role_policy, attach_role_policy, create_client_group, create_client_policy, create_client_role, create_group, create_policy, create_role, delete_client_group, delete_client_policy, delete_group, delete_policy, detach_client_role_policy, detach_role_policy, disable_client_role, disable_role, get_client_group, get_client_policy, get_client_role, get_group, get_policy, get_role, link_client_group_role,…"
license: "Proprietary. Use with a Maison API key under the Maison terms of service."
metadata:
  author: "maison-labs"
  version: "1.0.0"
  category: "access"
  api-base: "https://maison-labs.com"
  openapi: "https://maison-labs.com/api/v1/openapi.json"
  docs: "https://docs.maison-labs.com/skills/maison-access/"
---

# Maison — Roles, policies and groups

Shape permissions on Maison: roles, policies and user groups, each available to one hotel or to all clients (roles and policies may be Maison only). A policy is a list of rules (feature + level) and is never applied to a client directly — a role applies each of its policies at all clients or at named clients (Applies to). Also the feature catalogue and effective-access views. Use when designing who may do what, troubleshooting a 403, or granting a team a bundle of permissions.

Part of the Maison public API. Read the `maison-public-api` skill first for authentication, discovery and transport details (`https://docs.maison-labs.com/skills/maison-public-api/SKILL.md`).

## When to use

- Set what a policy allows from a description ("can view conversations and analytics but change nothing"): `set_policy_rules` (Maison policy) or `set_client_policy_rules` (hotel policy) save the full rule list in one call.
- Create a reusable permission bundle for a hotel — `create_client_policy` with statements, attach it to a role with `attach_client_role_policy`.
- Explain a 403 — `get_client_user_access` (users skill) or `get_client_role` / `get_client_policy` effective access.
- Roles and groups available to all clients — `create_role` (clientId null), `create_group`, `update_group_roles`; apply a shared policy at chosen hotels with `attach_role_policy` and change it with `set_role_policy_applies_to`.
- Bring a retired role back — `restore_role`. Page a role's holders past `get_role`'s first 50 — `list_role_users`, `list_role_groups`.
- Maison policies end to end — `create_policy`, `set_policy_rules`, `attach_role_policy`; `update_policy` renames or re-describes; `delete_policy` once no role applies it.
- Rename a hotel group or drop it — `update_client_group`, `delete_client_group`.

## Before you start

1. Send `x-api-key: <key>` on every call; the key only reaches `/api/v1`.
2. `GET /api/v1/tools` (or MCP `tools/list`) returns the tools THIS key may run — if a tool below is missing there, you lack a grant; do not retry it.
3. Client-scoped tools take `clientId` (a hotel id). `whoami` lists the ids you can act on.

## Tools

| Tool | What it does | REST | Needs |
| --- | --- | --- | --- |
| `add_client_policy_statement` | Set one rule (a feature and a level — a rule carries no client; where the policy applies is each role's Applies to) in a hotel-owned policy. | `POST /api/v1/client/{clientId}/policy/{policyId}/statement` | policies WRITE (client) |
| `add_client_role_rule` | Set one rule (a feature and a level) in a hotel role's own policy; the role applies it at this hotel. | `POST /api/v1/client/{clientId}/role/{roleId}/rule` | maison-roles WRITE (client), policies WRITE (client) |
| `add_role_policy` | Set one rule (a feature and a level — a rule has no client; where it applies is the role's Applies to) in the role's own policy. | `POST /api/v1/role/{roleId}/policy` | maison-roles WRITE (global), policies WRITE (global) |
| `assign_role` | Give a user this role at one client (`clientId`) or at all clients (null). | `POST /api/v1/role/{roleId}/user` | maison-users WRITE (client) |
| `assign_role_to_group` | Give a group this role at one client (`clientId`) or at all clients (null) so its members inherit it. | `POST /api/v1/role/{roleId}/group` | maison-users WRITE (client) |
| `attach_client_role_policy` | Attach an existing policy to a hotel role; the role applies it at this hotel. | `POST /api/v1/client/{clientId}/role/{roleId}/policy` | maison-roles WRITE (client) |
| `attach_role_policy` | Attach an existing policy to a role and say where the role applies it (`appliesTo`: null = all clients, or client ids). | `POST /api/v1/role/{roleId}/policy/{policyId}/attach` | maison-roles WRITE (any) |
| `create_client_group` | Create a new access group owned by a hotel and optionally seed it with members and a role. | `POST /api/v1/client/{clientId}/group` | groups WRITE (client); composite of `update_client_group_members`, `link_client_group_role` |
| `create_client_policy` | Create a policy available to one hotel and optionally seed it with rules (each a feature and a level). | `POST /api/v1/client/{clientId}/policy` | policies WRITE (client); composite of `add_client_policy_statement` |
| `create_client_role` | Create a new role owned by a hotel and optionally seed it with rules (a feature and a level each; seeding also needs the policies grant at the hotel, checked before the role exists). | `POST /api/v1/client/{clientId}/role` | maison-roles WRITE (client); composite of `add_client_role_rule` |
| `create_group` | Create a user group available to all clients (needs the global groups grant) — Maison only with `visibilityMaisonOnly` — and optionally seed it with members (`update_group_members`) and role assignments (`update_group_roles`, which also need the users grant where each role is given). | `POST /api/v1/group` | groups WRITE (global); composite of `update_group_members`, `update_group_roles` |
| `create_policy` | Create a policy available to all clients (`visibilityMaisonOnly` makes it Maison only), with no rules yet: fill them with `set_policy_rules`, then apply it with `attach_role_policy`. | `POST /api/v1/policy` | policies WRITE (global) |
| `create_role` | Create a role available to one client (`clientId`) or to all clients (`clientId` null; `visibilityMaisonOnly` makes it Maison only), optionally seeded with rules on its own policy. | `POST /api/v1/role` | maison-roles WRITE (client); composite of `add_role_policy` |
| `delete_client_group` | Delete (soft) a group the hotel owns, with its role links. | `DELETE /api/v1/client/{clientId}/group/{groupId}` | groups WRITE (client); destructive |
| `delete_client_policy` | Soft-delete a hotel-owned policy. | `DELETE /api/v1/client/{clientId}/policy/{policyId}` | policies WRITE (client); destructive |
| `delete_group` | Soft-delete a group and cascade to members and role links; needs the groups grant where the group is available. | `DELETE /api/v1/group/{groupId}` | groups WRITE (any); destructive |
| `delete_policy` | Delete (soft) a policy available to all clients. | `DELETE /api/v1/policy/{policyId}` | policies WRITE (global); destructive |
| `detach_client_role_policy` | Stop a hotel role applying a policy. | `DELETE /api/v1/client/{clientId}/role/{roleId}/policy/{policyId}` | maison-roles WRITE (client); destructive |
| `detach_role_policy` | Stop a role applying a policy anywhere (every client it applied at). | `POST /api/v1/role/{roleId}/policy/{policyId}/detach` | maison-roles WRITE (any); destructive |
| `disable_client_role` | Soft-disable a hotel-owned role. | `POST /api/v1/client/{clientId}/role/{roleId}/disable` | maison-roles WRITE (client); destructive |
| `disable_role` | Retire a role (sets disabled_date); needs the roles grant where the role is available. | `POST /api/v1/role/{roleId}/disable` | maison-roles WRITE (any); destructive |
| `get_client_group` | Fetch a hotel group's members, candidate users, role assignments, and effective access summary. | `GET /api/v1/client/{clientId}/group/{groupId}` | groups READ (client), maison-users READ (client); read-only |
| `get_client_policy` | Fetch a hotel policy's rules (feature + level) and its effective access summary. | `GET /api/v1/client/{clientId}/policy/{policyId}` | policies READ (client); read-only |
| `get_client_role` | Fetch a hotel role's policies and its effective access summary. | `GET /api/v1/client/{clientId}/role/{roleId}` | maison-roles READ (client); read-only |
| `get_group` | Full detail of a group you can see, including members and role assignments. | `GET /api/v1/group/{groupId}` | groups READ (any); read-only |
| `get_policy` | Fetch one policy you can see with its rules (feature + level) and the roles that apply it. | `GET /api/v1/policy/{policyId}` | policies READ (any); read-only |
| `get_role` | Fetch a role's detail — its attached policies, each with `applies_to` (`all: true`, or the named `clients` where the role applies it) and rule count — its first page of direct users, and its first page of groups. | `GET /api/v1/role/{roleId}` | maison-roles READ (any); read-only |
| `link_client_group_role` | Create a user_group_role_link so a hotel-owned group inherits a role at this hotel. | `POST /api/v1/client/{clientId}/usergroup` | groups WRITE (client), maison-users WRITE (client) |
| `list_client_features_catalogue` | Features available for hotel-scoped roles and policies at a specific hotel, with their display labels. | `GET /api/v1/client/{clientId}/featurescatalogue` | policies READ (client); read-only |
| `list_client_groups` | The access groups available at a hotel: its own and those available to all clients — never another hotel's. | `GET /api/v1/client/{clientId}/groups` | groups READ (client); read-only |
| `list_client_policies` | The policies available at a hotel: its own and those available to all clients (never Maison-only ones, never another hotel's). | `GET /api/v1/client/{clientId}/policies` | policies READ (client); read-only |
| `list_client_roles` | Roles that can be assigned to users or groups at a specific hotel (excludes Maison-only and disabled roles). | `GET /api/v1/client/{clientId}/roles` | maison-roles READ (client); read-only |
| `list_features` | Read-only catalogue of all Maison features: id, key (`name`), display `label`, `purpose` (what it does and where), description and `maison_only`. | `GET /api/v1/features` | maison-roles READ (global); read-only |
| `list_groups` | Page through the user groups you can see, with optional search and sort (name, member count or creation): groups available to all clients and those of each client you read — never another client's; Maison-only groups (`visibility_maison_only`) only with the global groups read grant. | `GET /api/v1/groups` | groups READ (any); read-only |
| `list_policies` | Page through the policies you can see (search by name or description), with rule and role counts: policies available to all clients, Maison-only ones (with the global policies grant) and those of each client you read — never another client's. | `GET /api/v1/policies` | policies READ (any); read-only |
| `list_role_groups` | Page through the groups given a role (their members inherit it), each with the client the link is at. | `GET /api/v1/role/{roleId}/groups` | maison-roles READ (any); read-only |
| `list_role_users` | Page through the users holding a role directly, each with the client the link is at (null = all clients). | `GET /api/v1/role/{roleId}/users` | maison-roles READ (any); read-only |
| `list_roles` | Page through the roles you can see, with optional search, sort and filters (`scope`, `ownerClientId`, `includeRetired`): roles available to all clients, Maison-only roles (with the global roles grant) and the roles of each client you read — never another client's. | `GET /api/v1/roles` | maison-roles READ (any); read-only |
| `remove_client_policy_statement` | Soft-delete one statement from a hotel-owned policy. | `DELETE /api/v1/client/{clientId}/policy/{policyId}/statement/{stmtId}` | policies WRITE (client); destructive |
| `remove_client_role_rule` | Remove one rule (the id `add_client_role_rule` returned, or from `get_client_role`) from a hotel role's own policy. | `DELETE /api/v1/client/{clientId}/role/{roleId}/rule/{ruleId}` | maison-roles WRITE (client), policies WRITE (client); destructive |
| `remove_role_policy` | Soft-delete one rule from the role's own policy; needs the roles and policies grants where the role is available. | `DELETE /api/v1/role/{roleId}/policy/{policyId}` | maison-roles WRITE (any), policies WRITE (any); destructive |
| `restore_role` | Bring a retired role back (clears disabled_date) so it can be assigned again; its policies and links are as they were. | `POST /api/v1/role/{roleId}/restore` | maison-roles WRITE (any) |
| `set_client_policy_rules` | Save a hotel-owned policy's complete rule list in one call (the client Policies tab's rules table): each rule is a feature, or every feature, at READ, WRITE, EXECUTE or DENY. | `PUT /api/v1/client/{clientId}/policy/{policyId}/rules` | policies WRITE (client) |
| `set_policy_rules` | Save a Maison policy's complete rule list in one call (the policy page's rules table): each rule is a feature, or every feature, at READ, WRITE, EXECUTE or DENY. | `PUT /api/v1/policy/{policyId}/rules` | policies WRITE (global) |
| `set_role_policy_applies_to` | Replace where a role applies one of its policies (`appliesTo`: null = all clients, or client ids) in one call: clients outside the new set stop, missing ones start. | `PUT /api/v1/role/{roleId}/policy/{policyId}/appliesto` | maison-roles WRITE (any) |
| `unassign_role` | Take a role away from a user: every scope by default, or only one with `linkScope` (`global` for the link at all clients, or a client id). | `DELETE /api/v1/role/{roleId}/user/{userId}` | maison-users WRITE (any); destructive |
| `unlink_client_group` | Soft-delete all role links for a hotel-owned group at this hotel. | `DELETE /api/v1/client/{clientId}/usergroup/{groupId}` | groups WRITE (client); destructive |
| `update_client_group` | Rename or re-describe a group the hotel owns (`description` null clears it). | `PUT /api/v1/client/{clientId}/group/{groupId}` | groups WRITE (client) |
| `update_client_group_members` | Add or remove members in a hotel-owned access group. | `PUT /api/v1/client/{clientId}/group/{groupId}/members` | groups WRITE (client) |
| `update_client_policy` | Patch a hotel-owned policy's name or description. | `PUT /api/v1/client/{clientId}/policy/{policyId}` | policies WRITE (client) |
| `update_client_role` | Patch a hotel-owned role's name or description. | `PUT /api/v1/client/{clientId}/role/{roleId}` | maison-roles WRITE (client) |
| `update_group` | Patch a group's name, description or Maison-only flag (`visibilityMaisonOnly`, which needs the global groups grant and a group available to all clients — 400 on a hotel's group); needs the groups grant where the group is available. | `PUT /api/v1/group/{groupId}` | groups WRITE (any) |
| `update_group_members` | Add or remove members of a group; needs the groups grant where the group is available (and the global roles grant for a group carrying a Maison-only role). | `PUT /api/v1/group/{groupId}/members` | groups WRITE (any) |
| `update_group_roles` | Add or remove role assignments of a group (each at a client, or at all clients with clientId null); needs the groups grant where the group is available, and the users grant where each role is given. | `PUT /api/v1/group/{groupId}/roles` | groups WRITE (any), maison-users WRITE (any) |
| `update_policy` | Rename or re-describe a policy available to all clients, or set / clear its Maison-only flag. | `PUT /api/v1/policy/{policyId}` | policies WRITE (global) |
| `update_role` | Patch a role's name, description or Maison-only flag (`visibilityMaisonOnly`, which needs the global roles grant). | `PUT /api/v1/role/{roleId}` | maison-roles WRITE (any) |

Full input fields for every tool: [references/tools.md](references/tools.md).

## Recommended workflows

### Set a policy's rules from what the person describes

1. `get_policy` (or `get_client_policy`) for the current rules; `list_features` (or `list_client_features_catalogue`) for each feature's id, label and purpose.
2. Map the words to levels: view → READ, change or manage → WRITE, run, send or act → EXECUTE, never or block → DENY. Name the specific features; the every-feature rule (featureId null) only when they mean everything.
3. Ambiguous (which features, which level)? Show the rules as a short Feature / Level list and ask before saving.
4. `set_policy_rules` (or `set_client_policy_rules`) with the FULL list: the current rules you keep plus the changes, one rule per feature. A rule left out is removed.

### Grant a hotel team a defined set of features

1. `list_client_features_catalogue` to see which features the hotel's plan allows for roles.
2. `create_client_policy` with `statements: [{ featureId, permission }]` (or `create_client_role` with inline rules; add or remove one later with `add_client_role_rule` / `remove_client_role_rule`); a rule names no client.
3. `create_client_group` with `memberIds` and `roleId`, or assign per user with `set_client_user_roles`.
4. Verify with `get_client_group` / `get_client_role` effective access.

## Tips

- Levels: READ < WRITE < EXECUTE; DENY wins over any grant at the same scope. A policy holds at most one rule per feature (and at most one every-feature rule), at exactly one of READ, WRITE, EXECUTE or DENY; a higher level includes the lower ones (WRITE already includes READ).
- Nobody can grant a feature level they do not hold themselves on that hotel (no-escalation rule) — a 403 while adding a statement usually means that.
- A rule granting a Maison-only feature (flagged maison_only in `list_features`), an every-feature rule, and attaching a policy that grants a Maison-only feature all need `allow-maison-only` WRITE held globally and by name — an ALL wildcard never counts (403 names `allow-maison-only`). A DENY rule needs nothing extra.

## Pitfalls

- The rules tools replace the whole list, and one rule beyond your own access refuses the whole save (403 names the feature and level; nothing changed). One feature at two levels in the list is a 400 (nothing saved).
- `add_role_policy`, `add_client_role_rule` and `add_client_policy_statement` set the feature's one rule: a different level replaces the current rule, the same level changes nothing.
- Each kind has one grant, at the hotel for a row available to that hotel and GLOBAL for a row available to all clients (or Maison only): roles → `maison-roles` (also attaching policies to a role), groups → `groups`, policies → `policies` (and their rules), giving or revoking a role or a membership → `maison-users`. `clients` WRITE grants nothing in access management. Read with the same feature at READ.
- A role's own inline rules (`add_role_policy`, `create_role` with `policies`) need GLOBAL `policies` WRITE; a hotel's rules (`set_client_policy_rules`, `add_client_policy_statement`) need `policies` WRITE on the hotel.
- A policy available to one hotel applies only there; a role available to one hotel applies its policies there only (`appliesTo` null). A rule input naming a client, region, department or tag is a validation error — scope lives on the role, never on a rule.
- Disabling a role is soft (`disable_role`); the role's users lose the permissions immediately.
- `delete_client_group` is 409 while the group has members (pending invitations included) and `delete_policy` is 409 while a role applies it: empty or detach first.
- `unassign_role` without `linkScope` removes the role at every scope; pass `linkScope: "global"` or a client id to remove one link.

## Calling conventions

Same tool, three transports — pick whichever your runtime already speaks. Always send `x-api-key`.

```bash
# REST
curl -X POST "https://maison-labs.com/api/v1/client/<clientId>/policy/<policyId>/statement" -H "x-api-key: $MAISON_API_KEY" -H "content-type: application/json" -d '{ "permission": … }'
```

```json
// MCP (https://maison-labs.com/mcp, Streamable HTTP): tools/call
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "add_client_policy_statement", "arguments": { "permission": … } } }
```

```json
// A2A (https://maison-labs.com/a2a): tasks/send with a data part
{ "jsonrpc": "2.0", "id": 1, "method": "tasks/send", "params": { "message": { "role": "user", "parts": [{ "type": "data", "data": { "tool": "add_client_policy_statement", "input": { "permission": … } } }] } } }
```

Errors are the same everywhere: `401` no/invalid key; `403 forbidden` with `details.missing` (the grants you lack — fall back to the composite's parts or stop); `403 not_entitled` (the hotel's plan lacks the feature); `404`, `409`, `413 too_large`, `503`. Inputs that fail validation return `400 invalid_input` with the Zod issues — fix and retry once.
