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

# Tool Access Policies

> Decide which members, teams and roles can call which services and tools

A policy is one statement, a **grant**: a **subject** (the whole organization, a team, or a role) is **allowed** or **denied** a **service** or a single **tool**. Together, an organization's grants decide what each member's agents can call.

## Enforcement

Enforcement is on by default. A call the policy refuses answers `403` with `Your organization does not have access to this tool` and writes a `tool.denied` row to the [audit log](/organizations/audit-log) with the reason. The check runs on every path a tool can be executed from: single and batch tool calls over the API, the MCP server (which calls the same API) and hosted workflows. It also runs at discovery, so a member's search results and service tool lists leave out what they could not call.

Self-hosted deployments can change the mode with `ORG_TOOL_AUTHZ_MODE`:

| Mode | Behaviour |
| - | - |
| `enforce` (default) | Refuses with `403` and audits the denial. |
| `shadow` | Evaluates the same policy, audits what would have been denied (`tool.denied` with `"shadow": true` in `metadata`) and lets the call through. A rollout aid while a new set of grants is being checked, never a production setting. |
| `off` | Skips the check. |

## How evaluation works

The subjects that match a member are the organization (`org` / `default`), their role, and every team they belong to. The grants for those subjects are composed like this:

* **No grants means ungoverned.** Every member keeps full access to the organization's tools.
* **The first grant switches the organization to deny-by-default.** From then on, a member can only use what a grant allows.
* **Explicit deny wins.** A deny on the tool, or on its service, for any subject that includes the member refuses the call, whatever else allows it.
* **Owners and admins skip the allow-lists.** They are not subject to deny-by-default, so a mistaken allow cannot lock the people who can fix it out. An explicit deny whose subject includes them (the organization, their role, one of their teams) still applies to their calls, but never to their ability to edit the policies, which depends on their role alone.
* **Scope.** A service-level grant covers every tool in that service. A tool-level grant covers one tool.
* **The public catalog can be denied, never allow-listed.** Allow grants govern the organization's own internal, local-only and private tools; they never restrict the catalog, so the first allow grant does not hide the marketplace from members. A deny on a public catalog service or tool applies to every call made in the organization's context (a `danube-org-id` header naming it, or one of its keys) and is refused with `org_policy:explicit_deny`; the same call made outside the organization's context is not affected. To limit what one key may call in the catalog, restrict the key itself with `allowed_services` / `allowed_tools`.

A member sees the effect before calling anything: a tool the policy would refuse is missing from their search results and from the service's tool list, a denied catalog tool included when they search in the organization's context. The owners and admins who edit policies still see everything, apart from an explicit deny whose subject includes them.

## Subjects

| Subject type | `subject_id` | Applies to |
| - | - | - |
| `org` | `default` | every member of the organization |
| `team` | a team id of this organization | every member of that team |
| `role` | `owner`, `admin`, `member`, or `viewer` | every member holding that role |

### Team grants

A team grant needs a team of this organization (`400` otherwise) with people in it: add them under **Teams** on the Organization page or with `POST /v1/organizations/{org_id}/teams/{team_id}/members` (`{"email": "..."}` or `{"user_id": "..."}`, plus `"role": "lead"` if you want). A membership change takes effect on the next call. Deleting a team removes every grant that targeted it, so the policies page never lists a grant for a team that no longer exists.

## Create a policy

<Steps>
  <Step title="Open Access">
    Go to [Dashboard > Access](https://danubeai.com/dashboard/organization/access). Admins and owners only.
  </Step>

  <Step title="Pick the subject and the effect">
    Choose the organization, a team, or a role, and whether the grant allows or denies. A public catalog service or tool can only be denied.
  </Step>

  <Step title="Pick the scope">
    Choose a service (all of its tools) or one tool. The tool picker lists the organization's own services first and the rest of the catalog after them; once a service is chosen it searches that service's tools by name and description and shows the first eight matches.
  </Step>

  <Step title="Check the audit log">
    After the first grant exists, denied calls appear in the [audit log](/organizations/audit-log) with status `denied` and a `denial_reason`. Use it to confirm the policy does what you intended before relying on it.
  </Step>
</Steps>

## Example

The finance team may use every tool of the organization's internal Billing service, but nobody outside the admins may void an invoice:

```json theme={null}
[
  {"subject_type": "team", "subject_id": "<finance-team-id>", "effect": "allow", "service_id": "<billing-service-id>"},
  {"subject_type": "role", "subject_id": "member", "effect": "deny", "tool_id": "<billing-void-invoice-tool-id>"}
]
```

Admins keep the void tool because the deny targets the `member` role, not the whole organization. An `org` / `default` deny would reach the admins too. Members outside the finance team see none of the Billing tools, since the organization is now governed and nothing allows them.

## Denial reasons

Every refused call is audited as `tool.denied` with one of these `denial_reason` values.

| `denial_reason` | Meaning |
| - | - |
| `org_policy:no_grant` | The organization is governed and nothing allows this member the tool or its service |
| `org_policy:explicit_deny` | A deny on the tool or its service matched one of the member's subjects, in the organization that owns the tool or, for a public catalog tool, in the organization the call ran in |
| `not_org_member` | The tool belongs to an organization the caller is not a member of |
| `platform_private` | An unlisted catalog row parked by the platform; only a public listing makes a catalog row callable |
| `untenanted_private` | A non-public tool with no owning organization |
| `resolve_error`, `platform_unresolved` | Ownership could not be resolved; the check fails closed and the call is refused |
| `api_key_allowlist` | The key's own `allowed_services` / `allowed_tools` restriction, evaluated before the organization policy |
| `spend_cap:per_call`, `spend_cap:per_day` | The key's own spend caps |
| `org_cap:rate`, `org_cap:daily_spend` | The organization caps below |
| `missing_credentials` | The call needed a credential nobody had stored |

## Organization caps

Two caps under **Organization > Settings > Limits** (owner or admin; `rate_limit_per_minute` and `daily_spend_cap_cents` on `PATCH /v1/organizations/{org_id}/settings`) apply to every call made in the organization's context, whichever key made it, on top of the per-key caps. A call runs in the organization's context when it carries a `danube-org-id` header naming it or uses one of its keys; a personal organization is never capped here.

| Cap | Refusal | Audit |
| - | - | - |
| Calls per minute | `429`, `Retry-After` header, body `code` and `error_type` `usage_limit_reached`, `fault: caller`, message `This organization's limit of N calls per minute is reached; retry in S s` | `org_cap:rate` with `limit_per_minute` and `retry_after` |
| Daily spend | `403`, same `code`, `error_type` and `fault`, message `This organization's daily spend cap of N cents would be exceeded (spent M cents today)` | `org_cap:daily_spend` with `today_spend_cents`, `cap_cents` and `tool_price_cents` |

The per-minute window is a sliding 60 seconds; allowed calls count toward it, refused ones do not, so a client that keeps retrying does not push its own window out. The daily figure is the sum of paid-call costs since midnight UTC over every key that has belonged to the organization, revoked ones included, plus the price of the call being gated. Free tools count against the rate cap only.

## Endpoints

| Method | Path | Body |
| - | - | - |
| `GET` | `/v1/organizations/{org_id}/grants` | |
| `POST` | `/v1/organizations/{org_id}/grants` | `subject_type`, `subject_id`, `effect`, and one of `service_id` or `tool_id` |
| `DELETE` | `/v1/organizations/{org_id}/grants/{grant_id}` | |

Admin or owner. Validation rules: a grant needs a `service_id` or a `tool_id`; organization-wide grants must use `subject_id: "default"`; role grants must name one of the four roles; team grants must name a team of this organization. An identical grant answers `409`. The list stamps `service_name` and `tool_name` on each row so a grant reads as what it targets, not a uuid.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.