---
summary: "Walk a binding through the pending-grants gate \u2014 declared permissions,\
  \ required secret mappings, and the admin approval flow."
title: Bind a skill with grants
path: tutorials/bind-with-grants
status: published
---

# Bind a skill with grants

When a skill's manifest declares permissions or required secrets, binding it does not activate it. The binding row is created with `pending_grants = true` and the resolver filters it out. An admin with `scaiskills:grant` has to satisfy each declared item before the binding participates in any turn.

This walks through the full flow against the `pricing-policy` skill from [Publish a skill](./publish-a-skill), which declares one permission (`scaidrive:read:/policies/`) and one required secret (`stripe_read_only`).

## 1. Create the binding

```bash
SKILL_ID="$(curl -s "$SCAIGRID_HOST/v1/modules/scaiskills/skills/pricing-policy" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" | jq -r .data.id)"

curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/bindings" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"skill_id\": \"$SKILL_ID\",
    \"version\": \"0.1.0\",
    \"scope_type\": \"workspace\",
    \"scope_id\": \"$WORKSPACE_ID\",
    \"secret_mappings\": {
      \"stripe_read_only\": \"vault:/secrets/stripe-readonly-prod\"
    }
  }"
```

```python
import httpx, os
H = {"Authorization": f"Bearer {os.environ['SCAIGRID_API_KEY']}"}
HOST = os.environ["SCAIGRID_HOST"]

skill = httpx.get(f"{HOST}/v1/modules/scaiskills/skills/pricing-policy", headers=H).json()["data"]

binding = httpx.post(
    f"{HOST}/v1/modules/scaiskills/bindings",
    headers=H,
    json={
        "skill_id": skill["id"],
        "version": "0.1.0",
        "scope_type": "workspace",
        "scope_id": os.environ["WORKSPACE_ID"],
        "secret_mappings": {
            "stripe_read_only": "vault:/secrets/stripe-readonly-prod",
        },
    },
).json()["data"]
print(binding["id"], binding["pending_grants"])
```

```javascript
const H = { "Authorization": `Bearer ${process.env.SCAIGRID_API_KEY}` };
const HOST = process.env.SCAIGRID_HOST;

const skill = (await (await fetch(`${HOST}/v1/modules/scaiskills/skills/pricing-policy`, { headers: H })).json()).data;

const binding = await fetch(`${HOST}/v1/modules/scaiskills/bindings`, {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({
    skill_id: skill.id,
    version: "0.1.0",
    scope_type: "workspace",
    scope_id: process.env.WORKSPACE_ID,
    secret_mappings: { stripe_read_only: "vault:/secrets/stripe-readonly-prod" },
  }),
}).then(r => r.json());
console.log(binding.data.id, binding.data.pending_grants);
```

The secret mapping was supplied at bind time, so the secrets axis is satisfied. The permission axis isn't — `pending_grants` is `true` and the binding is invisible to resolve.

## 2. Inspect the gap

List the bindings for the scope to see what's pending:

```bash
curl "$SCAIGRID_HOST/v1/modules/scaiskills/bindings?scope_type=workspace&scope_id=$WORKSPACE_ID" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY"
```

The response shows the binding with `pending_grants: true`. The original manifest's permissions list is the source of truth — look it up via the skill detail endpoint, or read it off your local copy of the bundle.

## 3. Grant the permission

An admin with `scaiskills:grant` runs:

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/bindings/$BINDING_ID/permissions/grant" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "permission": "scaidrive:read:/policies/"
  }'
```

```python
httpx.post(
    f"{HOST}/v1/modules/scaiskills/bindings/{binding['id']}/permissions/grant",
    headers=H,
    json={"permission": "scaidrive:read:/policies/"},
)
```

```javascript
await fetch(`${HOST}/v1/modules/scaiskills/bindings/${binding.data.id}/permissions/grant`, {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({ permission: "scaidrive:read:/policies/" }),
});
```

Behind the scenes the grant row goes into `mod_scaiskills_permission_grants`, the binding's manifest is rechecked, and if every declared permission has a grant and every required secret has a mapping, `pending_grants` flips to `false`. The Redis cache for the scope is invalidated.

For skills with multiple declared permissions, call grant once per permission string. The grants are independent rows — revoking later means deleting one without affecting the others.

## 4. Confirm via resolve

```bash
curl -X POST "$SCAIGRID_HOST/v1/modules/scaiskills/resolve" \
  -H "Authorization: Bearer $SCAIGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"scope_type\": \"workspace\", \"workspace_id\": \"$WORKSPACE_ID\"}"
```

The `pricing-policy` slug should now appear in the `skills` array.

## What happens when the skill bumps a version

If the binding was created with a floating ref (`^0.1`), and a new `0.2.0` is published that declares a *new* permission the existing binding hasn't been granted, the next resolve sees the version roll forward, the manifest's permission list no longer matches the grants, and `pending_grants` flips back to `true`. The binding falls out of results until an admin grants the new permission.

Pinned bindings (`0.1.0`) are immune — the lockfile and the resolved manifest are frozen.

## What happens when a required secret is missing

There is no per-secret grant endpoint. Required secrets are supplied at bind time via `secret_mappings`. If you missed one, the binding stays in `pending_grants` and the only correction is to delete it and create a fresh one with the complete mapping. The grant flow is for permissions only.

## What happens at runtime

ScaiSkills does not enforce the granted permissions itself. It only enforces that the bind-time approval gate was cleared. The actual permission check happens in whatever module the permission targets — ScaiDrive enforces `scaidrive:read:/policies/`, the network egress policy enforces `network:*`, and so on. ScaiSkills' role is the approval ledger, not the policy engine.

## Done

The binding is active, the grants are auditable, and the supply-chain gate is in place for future version rolls. Iterate on the skill's body and references without touching the binding; touch the binding only to change scopes, change the version ref, or revoke.
