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

# Enforce

> Check whether an agent's grant token permits a specific tool call. Load manifests, call enforce(), and wrap LangChain tools.

## Overview

The enforce API verifies that an agent's grant token includes sufficient scope to call a specific tool on a specific connector. It combines JWT verification with manifest-based permission resolution in a single call.

```python theme={null}
from grantex import Grantex
from grantex.manifests.salesforce import manifest as sf

grantex = Grantex(api_key="gx_...", audience="https://api.merchant.example")
grantex.load_manifest(sf)

result = grantex.enforce(
    grant_token=token,
    connector="salesforce",
    tool="delete_contact",
)
if not result.allowed:
    print(result.reason)  # "write scope does not cover delete operations on salesforce"
```

***

## enforce()

Check whether a grant token permits a tool call.

```python theme={null}
def enforce(
    self,
    grant_token: str,
    connector: str,
    tool: str,
    amount: float | None = None,
    *,  # the keyword-only arguments are from version 0.7.0
    case_id: str | None = None,
    cost_components: list[str] | None = None,
    reserve: bool = True,
    caps_mode: str | None = None,
    caps_tenant_id: str | None = None,
    decision_grants: Sequence[str] | None = None,
    decision_action: DecisionAction | Mapping[str, Any] | None = None,
    arguments: Mapping[str, Any] | None = None,
    case_version: str | None = None,
    decisions_mode: str | None = None,
    revocation_check: str | None = None,
    audience: str | None = None,
) -> EnforceResult
```

### Parameters

| Parameter | Type | Required | Description |
| - | - | - | - |
| `grant_token` | `str` | Yes | The JWT grant token issued by Grantex. Decoded and verified inline. |
| `connector` | `str` | Yes | The connector name to check against (e.g., `"salesforce"`, `"s3"`). Must match a loaded manifest. |
| `tool` | `str` | Yes | The tool name to check (e.g., `"delete_contact"`, `"create_lead"`). Must be declared in the connector's manifest. |
| `amount` | `float \| None` | No | The call's amount, for capped scopes. When the token includes a capped scope like `tool:stripe:write:*:capped:500`, pass the transaction amount to check against the cap. From version 0.7.0 a capped scope denies a call without it (`amount_missing`). |
| `audience` | `str \| None` | No | The grant token audience this call expects; overrides the client's `audience`. See [Grant token audience](#grant-token-audience). From version 0.7.0. |

### Response: `EnforceResult`

| Field | Type | Description |
| - | - | - |
| `allowed` | `bool` | `True` if the tool call is permitted by the token's scopes. |
| `reason` | `str` | Human-readable reason when `allowed` is `False`. Empty string when allowed. |
| `grant_id` | `str` | The grant ID extracted from the JWT `grnt` (or `jti`) claim. |
| `agent_did` | `str` | The agent DID extracted from the JWT `agt` claim. |
| `scopes` | `list[str]` | All scopes from the JWT `scp` claim. |
| `permission` | `str` | The resolved permission level for this tool from the manifest (`"read"`, `"write"`, `"delete"`, or `"admin"`). |
| `connector` | `str` | The connector name that was checked. |
| `tool` | `str` | The tool name that was checked. |
| `would_deny` | `dict \| None` | In caps or decisions warn mode, the first denial that was let through (`reason_code`, `sub_reason`, `reason`, `details`); `None` when there is none. |
| `would_deny_all` | `tuple[dict, ...]` | In caps or decisions warn mode, every denial that was let through, in step order (decision, amount cap, call caps, decision consumption); `would_deny` is its first entry. Empty when there is none. |

### Example

```python theme={null}
result = grantex.enforce(
    grant_token="eyJhbGciOiJSUzI1NiIs...",
    connector="salesforce",
    tool="create_lead",
)

if result.allowed:
    print(f"Allowed: {result.tool} on {result.connector}")
    print(f"Grant: {result.grant_id}, Agent: {result.agent_did}")
else:
    print(f"Denied: {result.reason}")
```

### Capped Scopes

When a token includes a capped scope, pass the `amount` to enforce against the cap:

```python theme={null}
result = grantex.enforce(
    grant_token=token,
    connector="stripe",
    tool="create_payment_intent",
    amount=750,
)
# If token scope is tool:stripe:write:*:capped:500
# result.allowed = False
# result.reason_code = "cap_exceeded", result.sub_reason = "amount_cap"
```

**From version 0.7.0 (breaking):** a call without `amount` under a capped
scope is denied with `reason_code` `cap_exceeded` and `sub_reason`
`amount_missing` (`details` carries `limit`). Earlier releases allowed the
missing amount without checking its cap. `caps_mode="warn"` (on the client or per
call) keeps allowing it and reports the denial in `result.would_deny_all`
(and in `result.would_deny` when it is the first warning of the call, so a
call that also lacks a decision grant under `decisions_mode="warn"` shows the
decision there and `amount_missing` only in `would_deny_all`);
`caps_mode="off"` skips it. The cap covers every tool of the connector, read
tools included, so pass an amount (`0` when there is none) for those too. See
[Amount caps](/concepts/caps-and-metering#amount-caps).

### Grant token audience

<Warning>
  Available in published `grantex==0.7.0`. This is a breaking change:
  a grant token that carries an `aud` claim is denied unless the client
  expects that audience. A token without `aud` is denied if the client expects
  an audience; only tokens and clients both lacking an audience keep their
  previous semantics. Online revocation is now the default.
</Warning>

A grant token requested with an `audience` carries it in the `aud` claim
([RFC 7519 section 4.1.3](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3)):
the token is only for that relying party. `enforce()` checks `aud` right after
the signature, before revocation and scopes:

| Token `aud` | Expected audience | Result |
| - | - | - |
| none | none | checked as before |
| a string or an array of strings | none | denied: `token_invalid` / `audience_unconfigured` |
| contains the expected audience | set | checked as before |
| does not contain it, or no `aud` | set | denied: `token_invalid` / `audience_mismatch` |

`aud` may be a string or an array of strings; it matches when the expected
audience is one of its values, compared as exact strings (no case folding, no
trailing-slash or prefix matching). The denial's `details` carry
`token_audience` (a list) and, for `audience_mismatch`, `expected_audience`.

Audience denials are not relaxed by `enforce_mode="permissive"`: they stay
`allowed=False`, with the same `reason_code`, `sub_reason` and `details`, in
every enforce mode, because the token was issued for another relying party (or
the client does not know its own audience).

Set the audience on the client, and override it for one call:

```python theme={null}
grantex = Grantex(api_key="gx_...", audience="https://api.merchant.example")

result = grantex.enforce(grant_token=token, connector="acme_kyb", tool="get_case")
if result.sub_reason == "audience_mismatch":
    print(result.details["token_audience"])

# A tool served under a second audience
result = grantex.enforce(
    grant_token=token,
    connector="acme_kyb",
    tool="get_case",
    audience="https://tools.merchant.example",
)
```

| Client option | Type | Default | Description |
| - | - | - | - |
| `audience` | `str \| None` | `None` | The audience `enforce()` expects. A non-empty string. |
| `audience_check` | `str` | `"on"` | `"on"` applies the check above. `"off"` ignores `aud`, as earlier releases did, and cannot be combined with `audience`. Any other value raises `ValueError`. |

To keep the earlier behaviour while you find each service's audience, create
the client with `audience_check="off"`; remove it once `audience` is set.

***

## load\_manifest()

Load a single tool manifest into the client. Must be called before `enforce()` for the corresponding connector.

```python theme={null}
def load_manifest(self, manifest: ToolManifest) -> None
```

### Example

```python theme={null}
from grantex.manifests.salesforce import manifest as sf

grantex.load_manifest(sf)
```

***

## load\_manifests()

Load multiple tool manifests at once.

```python theme={null}
def load_manifests(self, manifests: list[ToolManifest]) -> None
```

### Example

```python theme={null}
from grantex.manifests.salesforce import manifest as sf
from grantex.manifests.hubspot import manifest as hs
from grantex.manifests.jira import manifest as jira

grantex.load_manifests([sf, hs, jira])
```

***

## ToolManifest

A manifest declares the permission level required for each tool on a connector.

```python theme={null}
class ToolManifest:
    connector: str
    description: str
    version: str

    def __init__(
        self,
        connector: str,
        tools: dict[str, Permission],
        description: str = "",
        version: str = "1.0.0",
    ) -> None: ...

    @property
    def tool_count(self) -> int: ...

    def get_permission(self, tool_name: str) -> Permission | None: ...
    def add_tool(self, tool_name: str, permission: Permission) -> None: ...

    @classmethod
    def from_file(cls, path: str) -> "ToolManifest": ...

    @classmethod
    def from_dict(cls, data: dict) -> "ToolManifest": ...
```

### Constructor

```python theme={null}
from grantex import ToolManifest, Permission

manifest = ToolManifest(
    connector="inventory-service",
    description="Internal warehouse inventory API",
    version="1.0.0",
    tools={
        "get_stock_level":   Permission.READ,
        "reserve_inventory": Permission.WRITE,
        "force_stock_reset": Permission.ADMIN,
    },
)
```

### get\_permission()

Look up the required permission for a tool. Returns `None` if the tool is not in the manifest.

```python theme={null}
perm = manifest.get_permission("reserve_inventory")
# Permission.WRITE

unknown = manifest.get_permission("nonexistent_tool")
# None
```

### add\_tool()

Add a tool to an existing manifest. Useful for extending pre-built manifests with custom tools.

```python theme={null}
from grantex.manifests.salesforce import manifest as sf_manifest

sf_manifest.add_tool("bulk_delete_all", Permission.ADMIN)
sf_manifest.add_tool("export_all_contacts", Permission.READ)

print(sf_manifest.tool_count)  # 8 (6 built-in + 2 custom)
```

### from\_file()

Create a manifest from a JSON file on disk:

```python theme={null}
manifest = ToolManifest.from_file("./manifests/inventory-service.json")
grantex.load_manifest(manifest)
```

The JSON file format:

```json theme={null}
{
  "connector": "inventory-service",
  "version": "1.0.0",
  "description": "Internal warehouse inventory API",
  "tools": {
    "get_stock_level": "read",
    "reserve_inventory": "write",
    "force_stock_reset": "admin"
  }
}
```

### from\_dict()

Create a manifest from a Python dictionary:

```python theme={null}
manifest = ToolManifest.from_dict({
    "connector": "pricing-engine",
    "tools": {
        "get_price": "read",
        "set_price": "write",
        "reset_all_prices": "admin",
    },
})
```

***

## Permission

A class representing the four permission levels in the hierarchy.

```python theme={null}
class Permission:
    READ   = "read"    # Level 0
    WRITE  = "write"   # Level 1
    DELETE = "delete"  # Level 2
    ADMIN  = "admin"   # Level 3
```

Higher levels subsume all lower levels: `admin > delete > write > read`.

### covers()

Check whether this permission level covers a required permission level:

```python theme={null}
Permission.WRITE.covers(Permission.READ)    # True  -- write covers read
Permission.WRITE.covers(Permission.DELETE)   # False -- write does not cover delete
Permission.ADMIN.covers(Permission.DELETE)   # True  -- admin covers everything
Permission.READ.covers(Permission.READ)      # True  -- exact match
```

### is\_valid()

Check whether a string is a valid permission level:

```python theme={null}
Permission.is_valid("write")    # True
Permission.is_valid("execute")  # False
```

***

## wrap\_tool()

Wrap a LangChain `StructuredTool` so that enforcement runs automatically before every invocation.

```python theme={null}
def wrap_tool(
    self,
    tool: StructuredTool,
    connector: str,
    tool_name: str,
    grant_token: str | Callable[[], str],
    extract_amount: Callable[[Mapping[str, Any]], float | None] | None = None,
) -> StructuredTool
```

`extract_amount` (from version 0.7.0) receives the tool call's keyword
arguments and returns the call's amount, which is passed to `enforce()` as
`amount`. Under a capped scope, a wrapper without it (or an extractor returning
`None`) denies every call with `amount_missing`. An extractor that raises
refuses the call with `PermissionError` before `enforce()` runs; a value that
is not a finite number is denied with `invalid_amount`.

### Example

```python theme={null}
protected_tool = grantex.wrap_tool(
    my_langchain_tool,
    connector="salesforce",
    tool_name="create_lead",
    grant_token=lambda: current_state["grant_token"],
    extract_amount=lambda arguments: arguments["amount"],
)

# Use protected_tool in your LangChain agent chain.
# If the token lacks sufficient scope, the tool raises
# PermissionError instead of executing the underlying function.
```

***

## FastAPI Integration

Use `GrantexAuth` from `grantex_fastapi` as a FastAPI dependency for automatic enforcement on tool execution routes:

```python theme={null}
from grantex_fastapi import GrantexAuth

enforcer = GrantexAuth(grantex)

@app.post("/api/tools/{connector}/{tool}")
async def execute_tool(
    connector: str,
    tool: str,
    auth: EnforceResult = Depends(enforcer),
):
    # auth.allowed is guaranteed True here.
    # If the token lacked sufficient scope, a 403 was raised automatically.
    print(f"Executing {auth.tool} on {auth.connector} for grant {auth.grant_id}")
    ...
```

***

## Related

* [Scope Enforcement guide](/guides/scope-enforcement) — end-to-end walkthrough with framework integrations
* [TypeScript SDK enforce()](/sdks/typescript/enforce) — TypeScript API reference
* [Tool Manifests concept](/concepts/tool-manifests) — permission hierarchy and scope format
* [CLI enforce test](/cli/enforce) — dry-run enforcement from the command line

## Ownership

Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).


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