A tool manifest is a declarative mapping from tool names to permission levels for a specific connector. It tells Grantex exactly which permission is required to call each tool, so enforcement does not have to guess from tool name keywords.
{
"connector": "salesforce",
"version": "1.0.0",
"description": "Salesforce CRM connector",
"tools": {
"create_lead": "write",
"update_opportunity": "write",
"query": "read",
"create_task": "write",
"get_account": "read",
"list_opportunities": "read"
}
}
When an agent calls create_lead, Grantex looks up the manifest, sees that create_lead requires write permission, and checks whether the agent’s grant token includes a scope that covers write on the salesforce connector.
The Permission Hierarchy
Grantex defines four permission levels in a strict hierarchy. Higher levels subsume all lower levels:
admin (level 3)
└── delete (level 2)
└── write (level 1)
└── read (level 0)
| Level | Permission | Typical Operations |
|---|
| 0 | read | Query, list, get, search, fetch, check, download |
| 1 | write | Create, update, send, post, upload, apply |
| 2 | delete | Delete, remove, void, terminate, revoke, cancel |
| 3 | admin | Run payroll, period close, force reset, purge |
Coverage Rules
A scope grants access to all tools at or below its permission level:
| Granted Scope | READ Tools | WRITE Tools | DELETE Tools | ADMIN Tools |
|---|
tool:X:read | Allowed | Denied | Denied | Denied |
tool:X:write | Allowed | Allowed | Denied | Denied |
tool:X:delete | Allowed | Allowed | Allowed | Denied |
tool:X:admin | Allowed | Allowed | Allowed | Allowed |
Example: An agent with tool:salesforce:write:* can call query (read) and create_lead (write) but cannot call delete_contact (delete) or run_period_close (admin).
Tool enforcement scopes follow the format:
tool:{connector}:{permission}:{resource}[:capped:{N}]
| Part | Required | Description |
|---|
tool | Yes | Fixed prefix identifying this as a tool scope |
connector | Yes | The connector name (e.g., salesforce, s3, stripe) |
permission | Yes | The maximum permission level granted (read, write, delete, admin) |
resource | Yes | The resource pattern (* for all tools on this connector) |
capped:N | No | Optional spending cap per operation (e.g., capped:500) |
Examples
| Scope | Meaning |
|---|
tool:salesforce:write:* | Write access to all Salesforce tools (covers read + write) |
tool:s3:read:* | Read-only access to all S3 tools |
tool:stripe:write:*:capped:500 | Write access to Stripe tools, capped at 500 per operation |
tool:jira:admin:* | Full admin access to all Jira tools |
tool:okta:delete:* | Delete access to all Okta tools (covers read + write + delete) |
How enforce() Uses Manifests
When you call enforce(), Grantex performs these steps:
- Decode the JWT — extract
scp (scopes), grnt/jti (grant ID), agt (agent DID)
- Look up the manifest — find the loaded manifest for the given connector
- Resolve the permission — look up the tool name in the manifest to get its required permission level
- Check coverage — determine if any scope in the token covers the required permission on this connector
- Check caps — if the scope is capped and an amount was provided, verify the amount is within the cap
- Return the result —
allowed: true or allowed: false with a reason
result = grantex.enforce(
grant_token=token, # JWT with scp: ["tool:salesforce:write:*"]
connector="salesforce", # Look up salesforce manifest
tool="delete_contact", # Manifest says: delete permission required
)
# Token grants write, tool requires delete -> DENIED
# result.allowed = False
# result.reason = "write scope does not permit delete operations on salesforce"
Defining Manifests
Grantex enforces permissions on any connector you define — not just the ones we ship. You own the manifest, you own the permission mapping.
Custom Manifests
Define a manifest for any tool, API, or internal service. No dependency on Grantex to add support:
- Inline — define in code with
ToolManifest(...):
from grantex import ToolManifest, Permission
grantex.load_manifest(ToolManifest(
connector="my-internal-crm",
description="Internal CRM API",
tools={
"search_contacts": Permission.READ,
"create_deal": Permission.WRITE,
"delete_account": Permission.DELETE,
"manage_roles": Permission.ADMIN,
},
))
- JSON file — load from disk with
ToolManifest.from_file("./manifests/my-service.json")
- Directory — load all manifests from a folder with
grantex.load_manifests_from_dir("./manifests/")
- Extend — add tools to any manifest with
manifest.add_tool("new_tool", Permission.WRITE)
- CLI generate — auto-generate manifests from source code with
grantex manifest generate agent_tools.py
The enforce() engine, permission hierarchy, and JWT scope resolution work identically for custom and pre-built manifests. There is no difference at runtime.
Pre-Built Manifests
As a convenience, Grantex ships 53 pre-built manifests covering 339 tools across five categories:
| Category | Connectors | Tools | Examples |
|---|
| Finance | 11 | 78 | Stripe, SAP, QuickBooks, NetSuite |
| HR | 8 | 56 | Darwinbox, Okta, DocuSign, Greenhouse |
| Marketing | 16 | 94 | Salesforce, HubSpot, Mailchimp, Google Ads |
| Ops | 7 | 46 | Jira, Confluence, ServiceNow, Zendesk |
| Comms | 11 | 65 | Gmail, Slack, GitHub, S3, Twilio |
Import and load them directly:
from grantex.manifests.salesforce import manifest as sf
from grantex.manifests.stripe import manifest as stripe
grantex.load_manifests([sf, stripe])
These are a starting point, not a boundary. Mix pre-built and custom manifests freely — most production deployments use both.
Fail-Closed Default
If enforce() is called with a connector or tool that has no manifest loaded, the call is denied by default:
result = grantex.enforce(
grant_token=token,
connector="unknown-service",
tool="do_something",
)
# result.allowed = False
# result.reason = "No manifest loaded for connector 'unknown-service'. Load a manifest first."
This fail-closed behavior ensures that agents cannot bypass enforcement by calling tools that were never declared. Every tool must have an explicit permission entry in a loaded manifest.
For development and testing, you can switch to permissive mode:
grantex = Grantex(
api_key=key,
enforce_mode="strict", # default -- deny unknown tools
# enforce_mode="permissive", # dev only -- allow unknown tools with warning
)
Never use enforce_mode="permissive" in production. It defeats the purpose of scope enforcement.
When storing manifests as files (for git, CI, or team sharing), use this JSON format:
{
"connector": "inventory-service",
"version": "1.0.0",
"description": "Internal warehouse inventory API",
"tools": {
"get_stock_level": "read",
"reserve_inventory": "write",
"release_reservation": "write",
"adjust_stock": "write",
"force_stock_reset": "admin"
}
}
| Field | Type | Required | Description |
|---|
connector | string | Yes | Unique connector identifier |
version | string | No | Semantic version of the manifest (default "1.0.0") |
description | string | No | Human-readable description |
tools | object | Yes | Map of tool name to permission level ("read", "write", "delete", "admin") |