> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prisme.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Access Manager Module

> Manage service account tokens, product bindings, and access checks at runtime

The **access-manager** module provides four groups of functions :

1. **Service account management** — Create, rotate, delete service accounts and issue JWT tokens. Restricted to **privileged workspaces**.
2. **Org API key management** — Create, list, rotate, revoke org API keys scoped to the privileged workspace. Restricted to **privileged workspaces**.
3. **Product bindings** — CRUD operations on the `product_bindings` collection to link resources to users, orgs, or groups. Available to **any workspace**.
4. **Access checking** — High-level `checkAccess` function that combines permissions, scopes, and bindings to determine if a caller can access a resource. Available to **any workspace**.

<Warning>
  Service-account and org-API-key functions (`getServiceAccountToken`, `createServiceAccount`, `rotateServiceAccountSecret`, `deleteServiceAccount`, `createOrgApiKey`, `listOrgApiKeys`, `rotateOrgApiKey`, `deleteOrgApiKey`) are only available to **privileged workspaces**. A workspace is privileged when **either** of the following holds:

  * It is listed in the **`PRIVILEGED_WORKSPACES` environment variable** (operator-curated, slug-keyed — applies platform-wide), **or**
  * An organization has granted it privileges via `PUT /v2/orgs/{orgSlug}/workspaces/{workspaceIdOrSlug}/privileges` (per-org grant, ID-keyed — applies only inside that org's session).

  Binding and access-check functions are available to all workspaces.
</Warning>

## Service Account Functions

### getServiceAccountToken — Get a JWT token for a service account

```yaml theme={null}
- run:
    module: access-manager
    function: getServiceAccountToken
    parameters:
      orgSlug: "{{orgSlug}}"
      serviceAccountSlug: "{{saSlug}}"
      create: true
      expiresIn: 3600
    output: tokenResult
```

| Parameter            | Type    | Required | Description                                                                                                                                              |
| -------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orgSlug`            | string  | yes      | Organization slug                                                                                                                                        |
| `serviceAccountSlug` | string  | yes      | Service account slug                                                                                                                                     |
| `create`             | boolean | no       | When `true`, creates the service account if it doesn't exist and rotates the secret if needed. When `false` or omitted, only works with a cached secret. |
| `name`               | string  | no       | Display name for the service account (used on creation)                                                                                                  |
| `roleSlug`           | string  | no       | Role to assign. Must be in the workspace's `serviceAccounts.allowedRoleSlugs`. Defaults to `serviceAccounts.defaultRoleSlug`.                            |
| `expiresIn`          | number  | no       | Token TTL in seconds                                                                                                                                     |

Returns the token response including `accessToken`, `tokenType`, `expiresAt`, `permissions`, and `scopes`.

### createServiceAccount — Create a new service account

```yaml theme={null}
- run:
    module: access-manager
    function: createServiceAccount
    parameters:
      orgSlug: "{{orgSlug}}"
      serviceAccountSlug: "{{saSlug}}"
      name: "My Agent"
      roleSlug: "agent-standard"
    output: result
```

| Parameter            | Type   | Required | Description                                      |
| -------------------- | ------ | -------- | ------------------------------------------------ |
| `orgSlug`            | string | yes      | Organization slug                                |
| `serviceAccountSlug` | string | yes      | Service account slug                             |
| `name`               | string | no       | Display name                                     |
| `roleSlug`           | string | no       | Role to assign (validated against allowed roles) |

Returns the created service account including `slug` and `clientSecret`. If the service account already exists, returns `{ slug }` without error.

### rotateServiceAccountSecret — Rotate a service account's client secret

```yaml theme={null}
- run:
    module: access-manager
    function: rotateServiceAccountSecret
    parameters:
      orgSlug: "{{orgSlug}}"
      serviceAccountSlug: "{{saSlug}}"
    output: result
```

| Parameter            | Type   | Required | Description          |
| -------------------- | ------ | -------- | -------------------- |
| `orgSlug`            | string | yes      | Organization slug    |
| `serviceAccountSlug` | string | yes      | Service account slug |

Returns the new `clientSecret`.

### deleteServiceAccount — Delete a service account

```yaml theme={null}
- run:
    module: access-manager
    function: deleteServiceAccount
    parameters:
      orgSlug: "{{orgSlug}}"
      serviceAccountSlug: "{{saSlug}}"
    output: result
```

| Parameter            | Type   | Required | Description          |
| -------------------- | ------ | -------- | -------------------- |
| `orgSlug`            | string | yes      | Organization slug    |
| `serviceAccountSlug` | string | yes      | Service account slug |

## Cache behavior

The module caches client secrets in memory after creation or rotation. This cache is automatically invalidated when:

* A service account is **deleted** (clears secret + permissions cache)
* A service account secret is **rotated** (clears secret cache)
* A service account is **updated** (clears permissions cache)

Cache invalidation is event-driven and applies to all runtime instances simultaneously.

## Service Account Example

```yaml theme={null}
slug: get-agent-token
name: Get Agent Token
do:
  # Create (or reuse) a service account and get a JWT token
  - run:
      module: access-manager
      function: getServiceAccountToken
      parameters:
        orgSlug: "{{orgSlug}}"
        serviceAccountSlug: "agent-{{agentId}}"
        name: "Agent {{agentId}}"
        create: true
        expiresIn: 3600
      output: tokenResult

  # Use the token to call an API
  - fetch:
      url: "{{config.apiUrl}}/resources"
      method: GET
      headers:
        Authorization: "Bearer {{tokenResult.accessToken}}"
      output: resources
```

***

## Org API Key Functions

These functions let a privileged workspace mint and manage **org-level API keys** on behalf of its users — typically to expose a programmatic key tied to a single product resource (an agent, a workflow, etc.) without granting the user direct `orgs:apikeys:manage` permission.

### Security model

The runtime enforces strict isolation so a privileged workspace can only see and manage keys *it owns*:

* **`ownerType` is force-prefixed** with the calling workspace slug (e.g. the DSUL passes `agent`, the runtime stores `agent-factory:agent`). The DSUL **cannot** spoof another workspace's `ownerType`.
* **`listOrgApiKeys`** force-prefixes the `ownerType` filter the same way, so listings only ever return keys minted by the calling workspace.
* **`rotateOrgApiKey` / `deleteOrgApiKey`** fetch the target key first via `GET /v2/orgs/:orgSlug/api-keys/:keyId` and refuse the operation unless the existing `ownerType` starts with `${workspaceSlug}:`.
* **`permissions` and `scopes`** are forwarded verbatim to api-gateway. The DSUL is responsible for prefixing them; the runtime then validates each value against the privileged-workspace allowlist (see config below) — so a workspace can declare permissions/scopes of *other* workspaces only if the operator has explicitly listed them.

### Configuration

Add `serviceAccounts` and/or `apiKeys` blocks to the workspace's entry in `PRIVILEGED_WORKSPACES`:

```json theme={null}
{
  "agent-factory": {
    "serviceAccounts": {
      "defaultRoleSlug": "agent-standard",
      "allowedRoleSlugs": ["agent-standard", "agent-admin"]
    },
    "apiKeys": {
      "allowedPermissions": [
        "agent-factory:agents:read",
        "agent-factory:agents:write"
      ],
      "allowedScopes": ["agent-factory:agents:*"]
    }
  }
}
```

The same shape is accepted by the per-org grant endpoint as the `privileges.accessManager` body.

| Field                              | Type       | Description                                                                                                                                                                                  |
| ---------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serviceAccounts.defaultRoleSlug`  | `string`   | Role assigned to service accounts when the DSUL omits `roleSlug`. Required only if the DSUL calls `createServiceAccount` or `getServiceAccountToken` without a `roleSlug`.                   |
| `serviceAccounts.allowedRoleSlugs` | `string[]` | Allowlist of role slugs the workspace may assign. Empty / omitted means only `defaultRoleSlug` is accepted.                                                                                  |
| `apiKeys.allowedPermissions`       | `string[]` | Fully-qualified permission patterns the workspace may mint. Trailing `*` is supported (e.g. `agent-factory:agents:*`). The runtime rejects any permission not matching at least one pattern. |
| `apiKeys.allowedScopes`            | `string[]` | Fully-qualified scope patterns the workspace may attach to its API keys. Same glob semantics as `allowedPermissions`.                                                                        |

If `serviceAccounts` is omitted, the workspace cannot mint service-account tokens. If `apiKeys` is omitted, the workspace cannot create API keys.

### createOrgApiKey — Mint a new API key

```yaml theme={null}
- run:
    module: access-manager
    function: createOrgApiKey
    parameters:
      orgSlug: "{{auth.orgSlug}}"
      slug: "agent-{{agentId}}-{{run.correlationId}}"
      name: "API Key – {{agent.name}}"
      permissions:
        - agent-factory:agents:read
        - agent-factory:agents:write
      scopes:
        - agent-factory:agents:{{agentId}}
      ownerType: agent
      ownerId: "{{agentId}}"
      expiresAt: "2026-12-31T23:59:59Z"
    output: created
```

| Parameter     | Type      | Required | Description                                                                                      |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------ |
| `orgSlug`     | string    | yes      | Organization slug owning the key                                                                 |
| `slug`        | string    | yes      | Unique short id of the key within the org                                                        |
| `name`        | string    | yes      | Display name                                                                                     |
| `permissions` | string\[] | yes      | Fully-qualified permissions (DSUL prefixes them; runtime validates against `allowedPermissions`) |
| `scopes`      | string\[] | no       | Fully-qualified scopes (validated against `allowedScopes`)                                       |
| `ownerType`   | string    | yes      | **Bare** owner type (e.g. `agent`). The runtime force-prefixes it with the workspace slug.       |
| `ownerId`     | string    | no       | Owner identifier (typically the resource id)                                                     |
| `expiresAt`   | string    | no       | ISO-8601 expiration date                                                                         |

Returns `{ id, slug, apiKey, name, permissions, expiresAt }`. **The raw `apiKey` is shown only on creation — store it immediately.**

### listOrgApiKeys — List the workspace's API keys

```yaml theme={null}
- run:
    module: access-manager
    function: listOrgApiKeys
    parameters:
      orgSlug: "{{auth.orgSlug}}"
      ownerType: agent
      ownerId: "{{agentId}}"
      limit: 50
    output: keys
```

| Parameter   | Type   | Required | Description                                                                                                                               |
| ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `orgSlug`   | string | yes      | Organization slug                                                                                                                         |
| `ownerType` | string | yes      | **Bare** owner type. The runtime force-prefixes with the workspace slug; api-gateway has no wildcard support, so this is an exact filter. |
| `ownerId`   | string | no       | Restrict to keys for a single owner                                                                                                       |
| `limit`     | number | no       | Page size (default 50)                                                                                                                    |
| `page`      | number | no       | Page index (1-based)                                                                                                                      |

Returns `{ results: OrgApiKey[], total }`. Keys minted by *other* workspaces in the same org are never returned.

### rotateOrgApiKey — Regenerate a key's secret

```yaml theme={null}
- run:
    module: access-manager
    function: rotateOrgApiKey
    parameters:
      orgSlug: "{{auth.orgSlug}}"
      keyId: "{{keyId}}"
      expiresAt: "2027-01-01T00:00:00Z"
    output: rotated
```

| Parameter   | Type   | Required | Description                                        |
| ----------- | ------ | -------- | -------------------------------------------------- |
| `orgSlug`   | string | yes      | Organization slug                                  |
| `keyId`     | string | yes      | Slug/id of the key to rotate                       |
| `expiresAt` | string | no       | New expiration (keeps the previous one if omitted) |

Returns the same shape as `createOrgApiKey` (with the **new** raw `apiKey`). Throws if the key's `ownerType` doesn't start with `${workspaceSlug}:`.

### deleteOrgApiKey — Revoke a key

```yaml theme={null}
- run:
    module: access-manager
    function: deleteOrgApiKey
    parameters:
      orgSlug: "{{auth.orgSlug}}"
      keyId: "{{keyId}}"
    output: deleted
```

| Parameter | Type   | Required | Description                  |
| --------- | ------ | -------- | ---------------------------- |
| `orgSlug` | string | yes      | Organization slug            |
| `keyId`   | string | yes      | Slug/id of the key to delete |

Returns `{ success: true }`. Like `rotateOrgApiKey`, refuses if the key isn't owned by the calling workspace.

### Org API Key Example

End-to-end automation that mints, lists and revokes an API key tied to a specific agent (single endpoint exposed via webhook in the agent-factory workspace):

```yaml theme={null}
slug: v1/agents/agent_id/api-keys
name: API/v1/Agents/agent_id/Api-keys
when:
  endpoint: v1/agents/:agent_id/api-keys
do:
  - _auth:
      agent_id: '{{pathParams.agent_id}}'
      resource: agents
      action: share
      output: auth
  - conditions:
      '{{method}} = "POST"':
        # Prefix each requested permission with this workspace's slug so the
        # runtime allowlist (PRIVILEGED_WORKSPACES.agent-factory.apiKeys) accepts it.
        - set:
            name: _prefixed_permissions
            value: []
        - repeat:
            'on': '{{body.permissions}}'
            do:
              - set:
                  name: _prefixed_permissions[]
                  value: agent-factory:{{item}}
        - run:
            module: access-manager
            function: createOrgApiKey
            parameters:
              orgSlug: '{{auth.orgSlug}}'
              slug: '{{body.slug}}'
              name: '{{body.name}}'
              permissions: '{{_prefixed_permissions}}'
              scopes:
                - agent-factory:agents:{{pathParams.agent_id}}
              ownerType: agent
              ownerId: '{{pathParams.agent_id}}'
              expiresAt: '{{body.expiresAt}}'
            output: result
      '{{method}} = "GET"':
        - run:
            module: access-manager
            function: listOrgApiKeys
            parameters:
              orgSlug: '{{auth.orgSlug}}'
              ownerType: agent
              ownerId: '{{pathParams.agent_id}}'
            output: result
output: '{{result}}'
```

***

## Product Bindings Functions

Product bindings associate resources (agents, workflows, etc.) to principals (users, orgs, groups) within a workspace. All binding functions automatically scope queries to the caller's `workspaceId` — it cannot be overridden.

### findBindings — Query bindings

```yaml theme={null}
- run:
    module: access-manager
    function: findBindings
    parameters:
      query:
        resourceType: agents
        principalType: user
        principalId: "{{userId}}"
      options:
        pagination:
          limit: 50
          page: 0
        sort:
          createdAt: desc
    output: bindings
```

| Parameter            | Type   | Required | Description                                                                                                            |
| -------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `query`              | object | yes      | Filter fields matching `ProductBinding` (e.g. `resourceType`, `resourceId`, `principalType`, `principalId`, `orgSlug`) |
| `options.pagination` | object | no       | `{ page, skip, limit }`                                                                                                |
| `options.sort`       | object | no       | Sort fields (e.g. `{ createdAt: 'desc' }`)                                                                             |
| `options.fields`     | array  | no       | Fields to return                                                                                                       |

Returns an array of binding documents.

### findAndCountBindings — Query bindings with total count

```yaml theme={null}
- run:
    module: access-manager
    function: findAndCountBindings
    parameters:
      query:
        resourceType: agents
      options:
        pagination:
          limit: 20
          page: 0
    output: result
# result.items = [...], result.total = 42
```

Same parameters as `findBindings`. Returns `{ items: [...], total: number }`.

### countBindings — Count matching bindings

```yaml theme={null}
- run:
    module: access-manager
    function: countBindings
    parameters:
      query:
        resourceType: agents
        orgSlug: "{{orgSlug}}"
    output: count
```

| Parameter | Type   | Required | Description   |
| --------- | ------ | -------- | ------------- |
| `query`   | object | yes      | Filter fields |

Returns a number.

### insertBinding — Create a binding

```yaml theme={null}
- run:
    module: access-manager
    function: insertBinding
    parameters:
      data:
        resourceType: agents
        resourceId: "{{agentId}}"
        principalType: user
        principalId: "{{targetUserId}}"
        orgSlug: "{{orgSlug}}"
        grantedBy: "{{userId}}"
        email: "{{targetEmail}}"
    output: result
# result.acknowledged = true, result.insertedId = "..."
```

| Parameter            | Type           | Required | Description                                                                                                                                                                                     |
| -------------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.resourceType`  | string         | yes      | Resource type (e.g. `agents`, `workflows`)                                                                                                                                                      |
| `data.resourceId`    | string         | yes      | Resource identifier                                                                                                                                                                             |
| `data.principalType` | string         | yes      | `user`, `org`, or `group`                                                                                                                                                                       |
| `data.principalId`   | string         | yes      | Principal identifier                                                                                                                                                                            |
| `data.orgSlug`       | string         | yes      | Organization slug                                                                                                                                                                               |
| `data.grantedBy`     | string         | yes      | User who granted the binding                                                                                                                                                                    |
| `data.email`         | string         | no       | Email for user bindings                                                                                                                                                                         |
| `data.roleSlug`      | string \| null | no       | Optional role slug controlling which actions this binding grants. See [Role-based bindings](#role-based-bindings). When `null` or omitted, the binding grants every action **except `delete`**. |

The `workspaceId` and `workspaceSlug` are set automatically from the caller's context. Emits a `runtime.bindings.created` event.

A unique constraint prevents duplicate bindings for the same `(workspaceId, resourceType, resourceId, principalType, principalId)`.

### updateBinding — Update an existing binding

```yaml theme={null}
- run:
    module: access-manager
    function: updateBinding
    parameters:
      query:
        resourceType: agents
        resourceId: "{{agentId}}"
        principalType: user
        principalId: "{{targetUserId}}"
      data:
        roleSlug: editor
    output: result
# result.matchedCount = 1, result.modifiedCount = 1
```

| Parameter       | Type           | Required | Description                                                                                                  |
| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `query`         | object         | yes      | Filter to match the binding(s) to update                                                                     |
| `data.roleSlug` | string \| null | no       | New role slug to apply. Pass `null` to clear the role (binding will then grant all actions except `delete`). |

Only `roleSlug` can be updated through this function — other fields are immutable. Emits a `runtime.bindings.updated` event when at least one binding is modified.

### deleteOneBinding — Delete a single binding

```yaml theme={null}
- run:
    module: access-manager
    function: deleteOneBinding
    parameters:
      query:
        resourceType: agents
        resourceId: "{{agentId}}"
        principalType: user
        principalId: "{{targetUserId}}"
    output: result
# result.deletedCount = 1
```

| Parameter | Type   | Required | Description                           |
| --------- | ------ | -------- | ------------------------------------- |
| `query`   | object | yes      | Filter to match the binding to delete |

Emits a `runtime.bindings.deleted` event if a binding was deleted.

### deleteManyBindings — Delete multiple bindings

```yaml theme={null}
- run:
    module: access-manager
    function: deleteManyBindings
    parameters:
      query:
        resourceType: agents
        resourceId: "{{agentId}}"
    output: result
# result.deletedCount = 5
```

| Parameter | Type   | Required | Description                        |
| --------- | ------ | -------- | ---------------------------------- |
| `query`   | object | yes      | Filter to match bindings to delete |

Emits a `runtime.bindings.deleted.many` event if any bindings were deleted.

### Workspace cleanup

When a workspace is deleted, all its bindings are automatically removed.

***

## Access Check Function

The `checkAccess` function provides a high-level access control check that combines three sources: **permissions** (from `run.permissions`), **scopes** (from `run.scopes`), and **bindings** (from the `product_bindings` collection). This replaces the need for complex DSUL-based permission checking.

### checkAccess — Check resource access

```yaml theme={null}
- run:
    module: access-manager
    function: checkAccess
    parameters:
      resourceType: agents
      resourceId: "{{agentId}}"
      action: read
    output: access
```

All parameters are optional. When called without `resourceType`/`action`, it only checks authentication and returns `isWorkspaceAdmin`.

| Parameter      | Type    | Required | Description                                                                                                                                                                                                                                                 |
| -------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resourceType` | string  | no       | Resource type (e.g. `agents`, `workflows`). Must be set together with `action`.                                                                                                                                                                             |
| `resourceId`   | string  | no       | Specific resource ID. Requires `resourceType`.                                                                                                                                                                                                              |
| `action`       | string  | no       | Action to check (typically `read`, `write`, `share`, `delete`, `manage`, but any string is accepted — must match the keys in `roles[*].permissions`). Must be set together with `resourceType`.                                                             |
| `list`         | boolean | no       | When `true`, returns the list of granted resource IDs instead of a single grant                                                                                                                                                                             |
| `roles`        | object  | no       | Role definitions for binding role enforcement. `Record<string, { name?: string, permissions: string[] }>`. **Required as soon as any matching binding has a `roleSlug`** — otherwise `checkAccess` throws. See [Role-based bindings](#role-based-bindings). |

### Return value

| Field              | Type      | Description                                                                                                                                                                                                                                                                                |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `granted`          | boolean   | Whether access is granted                                                                                                                                                                                                                                                                  |
| `reason`           | string    | Why access was granted: `permission`, `wildcard-scope`, `scope`, or `binding:{principalType}` (e.g. `binding:user`, `binding:org`, `binding:group`). When the matching binding carries a `roleSlug`, the reason becomes `binding:{principalType}:{roleSlug}` (e.g. `binding:user:editor`). |
| `grantedIds`       | string\[] | Only in list mode — merged set of IDs from scopes and bindings                                                                                                                                                                                                                             |
| `hasWildcardScope` | boolean   | `true` if the caller has wildcard scope (sees all resources of this type)                                                                                                                                                                                                                  |
| `isWorkspaceAdmin` | boolean   | `true` if the caller has `*` or `{workspaceSlug}` manage permission                                                                                                                                                                                                                        |
| `error`            | object    | Only when `granted` is `false` — contains `error` (code) and `message` (human-readable)                                                                                                                                                                                                    |

The `error` object follows the same shape as `_auth` automations:

| `error.error`  | `error.message`                                                               | When                                                   |
| -------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ |
| `Unauthorized` | `Authentication required`                                                     | No authenticated user (no `userId` and no org API key) |
| `Forbidden`    | `Access denied: missing permission '{workspaceSlug}:{resourceType}:{action}'` | User is authenticated but has no matching permission   |

### Resolution order

0. **Authentication** — The caller must have a `userId` (user session) or an `orgSlug` (org API key). If neither is present, returns `Unauthorized` immediately.

1. **Permissions** — Checked from `run.permissions` (set by the token's role). The function checks in order:
   * `*` with `manage` → workspace admin
   * `{workspaceSlug}` with `manage` → workspace admin
   * `{workspaceSlug}:{resourceType}` with `manage` or `{action}` → access
   * If no permission matches → returns `Forbidden` immediately (scopes and bindings are not checked)

2. **Scopes** — Parsed from `run.scopes` (only if permission was granted):
   * `*`, `{workspaceSlug}:*`, or `{workspaceSlug}:{resourceType}:*` → wildcard scope (all resources)
   * `{workspaceSlug}:{resourceType}:{id}` → adds `id` to scoped IDs

3. **Bindings** — Looked up in `product_bindings` for the caller's identity (userId, org, groups). Only checked for single resource mode when the scope doesn't match. Each matching binding is then evaluated against the requested action through [role-based bindings](#role-based-bindings).

### Role-based bindings

Bindings can carry a `roleSlug` to restrict which actions they grant. The role definitions themselves live **outside** the binding — they must be passed to `checkAccess` via the `roles` parameter (typically loaded from `config.roles`):

```yaml theme={null}
- run:
    module: access-manager
    function: checkAccess
    parameters:
      resourceType: agents
      resourceId: "{{agentId}}"
      action: write
      roles:
        owner:
          name: Owner
          permissions: [read, write, share, delete]
        admin:
          name: Admin
          permissions: [read, write, share]
        editor:
          name: Editor
          permissions: [read, write]
        reader:
          name: Reader
          permissions: [read]
    output: access
```

Resolution rules applied to each candidate binding:

| Binding `roleSlug`  | `roles` parameter         | Behavior                                                                                                                                                 |
| ------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `null` / unset      | (any)                     | Binding grants **every action except `delete`**. Used for legacy bindings or "full collaborator" semantics.                                              |
| set (e.g. `editor`) | provided, slug exists     | Action must be in `roles[slug].permissions` — otherwise the binding is ignored and `checkAccess` continues to the next binding (or returns `Forbidden`). |
| set                 | not provided **at all**   | `checkAccess` **throws** — passing `roles` is required as soon as any binding could be role-typed.                                                       |
| set                 | provided but slug missing | The binding is ignored (treated as having no permissions).                                                                                               |

`checkAccess` only stops at the **first** binding that grants the requested action. If a user has multiple bindings (e.g. `reader` and `editor`), the most permissive applicable one wins.

When the binding is granted, the `reason` field reflects the role: `binding:user:editor`, `binding:org:admin`, etc.

<Tip>
  Keep your role catalog in `config.roles` so it can be referenced consistently from every endpoint that calls `checkAccess` (and from `insertBinding` / `updateBinding` for validation).
</Tip>

### Modes

**Auth-only** (no `resourceType`, no `action`):

* Returns `{ granted: true, isWorkspaceAdmin }` — just confirms the caller is authenticated

**Unauthenticated caller:**

* Returns `{ granted: false, error: { error: 'Unauthorized', message: 'Authentication required' } }`

**Single resource** (`resourceId` provided):

* If wildcard scope → `{ granted: true, reason: 'wildcard-scope', hasWildcardScope: true, isWorkspaceAdmin }`
* If scoped ID match → `{ granted: true, reason: 'scope', hasWildcardScope: false, isWorkspaceAdmin }`
* If binding found and the binding's `roleSlug` allows the action → `{ granted: true, reason: 'binding:{principalType}' or 'binding:{principalType}:{roleSlug}', hasWildcardScope: false, isWorkspaceAdmin }`
* Otherwise → `{ granted: false, hasWildcardScope: false, error: { error: 'Forbidden', message: '...' } }`

**List mode** (`list: true`, no `resourceId`):

* If wildcard scope → `{ granted: true, grantedIds: [], hasWildcardScope: true }` (caller sees everything)
* Otherwise → `{ granted: true, grantedIds: [...], hasWildcardScope: false }` (merged+deduplicated scoped IDs + binding IDs)

**Permission-only** (`resourceType` + `action`, no `resourceId`, no `list`):

* Returns `{ granted: true, reason: 'permission', hasWildcardScope, isWorkspaceAdmin }`

### Access check examples

#### Guard a specific resource

```yaml theme={null}
- run:
    module: access-manager
    function: checkAccess
    parameters:
      resourceType: agents
      resourceId: "{{agentId}}"
      action: read
    output: access

# access.error is set when granted=false (UNAUTHORIZED or FORBIDDEN)
- conditions:
    "{{access.error}}":
        - break:
            automation: true
            output:
              error: "{{access.error.error}}"
              message: "{{access.error.message}}"
```

#### Check auth + admin status only

```yaml theme={null}
- run:
    module: access-manager
    function: checkAccess
    output: access

# access.granted = true if authenticated
# access.isWorkspaceAdmin = true if user has * or workspace-level manage
```

#### List all accessible resources

```yaml theme={null}
- run:
    module: access-manager
    function: checkAccess
    parameters:
      resourceType: agents
      action: read
      list: true
    output: access

# If access.hasWildcardScope is true, query all agents without filter
# Otherwise, filter agents by access.grantedIds
- conditions:
    "{{access.hasWildcardScope}}":
        - fetch:
            url: "{{config.apiUrl}}/agents"
            output: agents
    default:
        - fetch:
            url: "{{config.apiUrl}}/agents?ids={{access.grantedIds | join ','}}"
            output: agents
```
