Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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, which declares one permission (scaidrive:read:/policies/) and one required secret (stripe_read_only).

1. Create the binding#

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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
1
2
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
1
2
3
4
5
6
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
1
2
3
4
5
httpx.post(
    f"{HOST}/v1/modules/scaiskills/bindings/{binding['id']}/permissions/grant",
    headers=H,
    json={"permission": "scaidrive:read:/policies/"},
)
javascript
1
2
3
4
5
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
1
2
3
4
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.

Updated 2026-05-18 15:01:32 View source (.md) rev 12