> ## 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.

```typescript theme={null}
import { Grantex } from '@grantex/sdk';
import { salesforceManifest } from '@grantex/sdk/manifests/salesforce';

const grantex = new Grantex({ apiKey: 'gx_...', audience: 'https://api.merchant.example' });
grantex.loadManifest(salesforceManifest);

const result = await grantex.enforce({
  grantToken: token,
  connector: 'salesforce',
  tool: 'delete_contact',
});

if (!result.allowed) {
  console.log(result.reason); // "write scope does not cover delete operations on salesforce"
}
```

***

## enforce()

Check whether a grant token permits a tool call.

```typescript theme={null}
enforce(options: EnforceOptions): Promise<EnforceResult>
```

### Parameters: `EnforceOptions`

<ParamField body="grantToken" type="string" required>
  The JWT grant token issued by Grantex. Decoded and verified inline.
</ParamField>

<ParamField body="connector" type="string" required>
  The connector name to check against (e.g., `"salesforce"`, `"s3"`, `"jira"`). Must match a loaded manifest.
</ParamField>

<ParamField body="tool" type="string" required>
  The tool name to check (e.g., `"delete_contact"`, `"create_lead"`). Must be declared in the connector's manifest.
</ParamField>

<ParamField body="amount" type="number">
  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.8.0 a capped scope denies a call without it (`amount_missing`).
</ParamField>

<ParamField body="audience" type="string">
  The grant token audience this call expects; overrides the client's `audience`. See [Grant token audience](#grant-token-audience). From version 0.8.0.
</ParamField>

### Response: `EnforceResult`

<ResponseField name="allowed" type="boolean">
  `true` if the tool call is permitted by the token's scopes.
</ResponseField>

<ResponseField name="reason" type="string">
  Human-readable reason when `allowed` is `false`. Empty string when allowed.
</ResponseField>

<ResponseField name="grantId" type="string">
  The grant ID extracted from the JWT `grnt` (or `jti`) claim.
</ResponseField>

<ResponseField name="agentDid" type="string">
  The agent DID extracted from the JWT `agt` claim.
</ResponseField>

<ResponseField name="scopes" type="string[]">
  All scopes from the JWT `scp` claim.
</ResponseField>

<ResponseField name="permission" type="string">
  The resolved permission level for this tool from the manifest (`"read"`, `"write"`, `"delete"`, or `"admin"`).
</ResponseField>

<ResponseField name="connector" type="string">
  The connector name that was checked.
</ResponseField>

<ResponseField name="tool" type="string">
  The tool name that was checked.
</ResponseField>

<ResponseField name="wouldDeny" type="WouldDeny">
  In caps or decisions warn mode, the first denial that was let through (`reason_code`, `sub_reason`, `reason`, `details`). Absent when there is none.
</ResponseField>

<ResponseField name="wouldDenyAll" type="readonly WouldDeny[]">
  In caps or decisions warn mode, every denial that was let through, in step order (decision, amount cap, call caps, decision consumption); `wouldDeny` is its first entry. Absent when there is none.
</ResponseField>

### Example

```typescript theme={null}
const result = await grantex.enforce({
  grantToken: 'eyJhbGciOiJSUzI1NiIs...',
  connector: 'salesforce',
  tool: 'create_lead',
});

if (result.allowed) {
  console.log(`Allowed: ${result.tool} on ${result.connector}`);
  console.log(`Grant: ${result.grantId}, Agent: ${result.agentDid}`);
} else {
  console.log(`Denied: ${result.reason}`);
}
```

### Capped Scopes

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

```typescript theme={null}
const result = await grantex.enforce({
  grantToken: token,
  connector: 'stripe',
  tool: 'create_payment_intent',
  amount: 750,
});
// If token scope is tool:stripe:write:*:capped:500
// result.allowed = false
// result.reasonCode = 'cap_exceeded', result.subReason = 'amount_cap'
```

**From version 0.8.0 (breaking):** a call without `amount` under a capped
scope is denied with `reasonCode` `cap_exceeded` and `subReason`
`amount_missing` (`details` carries `limit`). Earlier releases allowed the
missing amount without checking its cap. `capsMode: 'warn'` (on the client or per
call) keeps allowing it and reports the denial in `result.wouldDenyAll`
(and in `result.wouldDeny` when it is the first warning of the call, so a
call that also lacks a decision grant under `decisionsMode: 'warn'` shows the
decision there and `amount_missing` only in `wouldDenyAll`);
`capsMode: '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/sdk@0.8.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` (an array) and, for `audience_mismatch`, `expected_audience`.

Audience denials are not relaxed by `enforceMode: 'permissive'`: they stay
`allowed: false`, with the same `reasonCode`, `subReason` 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:

```typescript theme={null}
const grantex = new Grantex({ apiKey: 'gx_...', audience: 'https://api.merchant.example' });

const result = await grantex.enforce({ grantToken: token, connector: 'acme_kyb', tool: 'get_case' });
if (result.subReason === 'audience_mismatch') {
  console.log(result.details?.['token_audience']);
}

// A tool served under a second audience
await grantex.enforce({
  grantToken: token,
  connector: 'acme_kyb',
  tool: 'get_case',
  audience: 'https://tools.merchant.example',
});
```

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

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

***

## loadManifest()

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

```typescript theme={null}
loadManifest(manifest: ToolManifest): void
```

### Example

```typescript theme={null}
import { salesforceManifest } from '@grantex/sdk/manifests/salesforce';

grantex.loadManifest(salesforceManifest);
```

***

## loadManifests()

Load multiple tool manifests at once.

```typescript theme={null}
loadManifests(manifests: ToolManifest[]): void
```

### Example

```typescript theme={null}
import { salesforceManifest } from '@grantex/sdk/manifests/salesforce';
import { hubspotManifest } from '@grantex/sdk/manifests/hubspot';
import { jiraManifest } from '@grantex/sdk/manifests/jira';

grantex.loadManifests([salesforceManifest, hubspotManifest, jiraManifest]);
```

***

## ToolManifest

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

```typescript theme={null}
class ToolManifest {
  constructor(options: {
    connector: string;
    description?: string;
    version?: string;
    tools: Record<string, Permission>;
  });

  readonly connector: string;
  readonly description: string;
  readonly version: string;
  readonly toolCount: number;

  getPermission(toolName: string): Permission | undefined;
  addTool(toolName: string, permission: Permission): void;

  static fromJSON(json: {
    connector: string;
    description?: string;
    version?: string;
    tools: Record<string, string>;
  }): ToolManifest;
}
```

### Constructor

```typescript theme={null}
import { ToolManifest, Permission } from '@grantex/sdk';

const manifest = new 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,
  },
});
```

### getPermission()

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

```typescript theme={null}
const perm = manifest.getPermission('reserve_inventory');
// Permission.WRITE

const unknown = manifest.getPermission('nonexistent_tool');
// undefined
```

### addTool()

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

```typescript theme={null}
import { salesforceManifest } from '@grantex/sdk/manifests/salesforce';

salesforceManifest.addTool('bulk_delete_all', Permission.ADMIN);
salesforceManifest.addTool('export_all_contacts', Permission.READ);

console.log(salesforceManifest.toolCount); // 8 (6 built-in + 2 custom)
```

### fromJSON()

Create a manifest from a plain JSON object (e.g., loaded from a file):

```typescript theme={null}
import { ToolManifest } from '@grantex/sdk';
import manifest from './manifests/inventory-service.json';

const inventoryManifest = ToolManifest.fromJSON(manifest);
grantex.loadManifest(inventoryManifest);
```

***

## Permission

An enum representing the four permission levels in the hierarchy.

```typescript theme={null}
enum 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`.

***

## permissionCovers()

Check whether a granted permission level covers a required permission level.

```typescript theme={null}
function permissionCovers(granted: Permission, required: Permission): boolean;
```

### Example

```typescript theme={null}
import { permissionCovers, Permission } from '@grantex/sdk';

permissionCovers(Permission.WRITE, Permission.READ);   // true  — write covers read
permissionCovers(Permission.WRITE, Permission.DELETE);  // false — write does not cover delete
permissionCovers(Permission.ADMIN, Permission.DELETE);  // true  — admin covers everything
permissionCovers(Permission.READ, Permission.READ);     // true  — exact match
```

***

## wrapTool()

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

```typescript theme={null}
wrapTool(
  tool: StructuredTool,
  options: {
    connector: string;
    tool: string;
    grantToken: string | (() => string);
    extractAmount?: (input: unknown) => number | undefined | null | Promise<number | undefined | null>;
  },
): StructuredTool
```

`extractAmount` (from version 0.8.0) receives the tool's input 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 `undefined` or `null`)
denies every call with `amount_missing`. An extractor that throws refuses the
call before `enforce()` runs; a value that is not a finite number is denied
with `invalid_amount`.

### Example

```typescript theme={null}
const protectedTool = grantex.wrapTool(myLangChainTool, {
  connector: 'salesforce',
  tool: 'create_lead',
  grantToken: () => currentState.grant_token,
  extractAmount: (input) => (input as { amount: number }).amount,
});

// Use protectedTool in your LangChain agent chain.
// If the token lacks sufficient scope, the tool throws an error
// instead of executing the underlying function.
```

***

## enforceMiddleware()

Express middleware that enforces scope on every request to a route.

```typescript theme={null}
grantex.enforceMiddleware(options: {
  extractToken: (req: Request) => string | undefined;
  extractConnector: (req: Request) => string;
  extractTool: (req: Request) => string;
  extractAmount?: (req: Request) => number | undefined | null | Promise<number | undefined | null>;
}): RequestHandler
```

`extractAmount` (from version 0.8.0) returns the request's amount for a
capped scope. Without it, a capped scope answers 403 with `reason`
`cap_exceeded` and `subReason` `amount_missing`. An extractor that throws, or
returns something that is not a finite number, answers 403 with
`invalid_amount`.

### Example

```typescript theme={null}
app.use('/api/tools/:connector/:tool', grantex.enforceMiddleware({
  extractToken: (req) => req.headers.authorization?.replace('Bearer ', ''),
  extractConnector: (req) => req.params.connector,
  extractTool: (req) => req.params.tool,
  extractAmount: (req) => req.body.amount,
}));

// Requests with insufficient scope receive a 403 response automatically.
```

***

## Related

* [Scope Enforcement guide](/guides/scope-enforcement) — end-to-end walkthrough with framework integrations
* [Python SDK enforce()](/sdks/python/enforce) — Python 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.