API reference
All endpoints are mounted at /v1/modules/scaiskills/ and authenticate with the standard ScaiGrid bearer token. Responses use ScaiGrid's standard envelope ({ "data": ... } for success, { "error": ... } for failures). Permission checks happen per-endpoint against the keys documented in Permissions.
Skills#
POST /skills#
Register a new skill identity. Requires scaiskills:publish.
| Field | Required | Notes |
|---|---|---|
slug |
yes | Lowercase letters, digits, hyphens. Pattern: ^[a-z][a-z0-9-]{2,63}$. Globally unique. |
visibility |
no | private (default) or public. |
description |
no | Free text, up to 500 chars. |
1 2 3 4 | |
Returns { "data": { "id", "slug", "owner_workspace_id", "visibility", "description", "created_at" } }.
GET /skills#
List skills visible to the caller's workspace — own skills plus all public skills across the deployment. Requires scaiskills:view.
GET /skills/{slug}#
Fetch one skill's metadata and full version list. Requires scaiskills:view. Private skills owned by another workspace return SCAISKILLS_SKILL_NOT_FOUND, not 403.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
DELETE /skills/{slug}#
Hard-delete the skill, every version, every binding, and every grant. Requires scaiskills:manage. Workspace must own the skill. Returns { "deleted": true, "slug": "..." }.
Versions#
POST /skills/{slug}/versions#
Publish a new version. Requires scaiskills:publish and ownership of the skill. Multipart upload with one field, bundle, containing the .tar.gz.
1 2 3 | |
The validator checks:
- Bundle is a valid gzip-compressed tar.
SKILL.mdis present at the root with valid YAML frontmatter.- Manifest
nameequals the URL slug. - Manifest
versionis strictly greater than every previously published semver for this skill. - References paths don't escape the bundle root.
- Secrets entries match the v1.1 schema.
On success returns the new version row including content_hash, storage_uri, and status: "published".
On validation failure returns a non-standard error envelope with success: false and a context.errors array:
1 2 3 4 5 6 7 8 9 10 11 12 | |
POST /skills/{slug}/versions/{semver}/yank#
Mark a version as yanked. Requires scaiskills:publish and ownership. Existing bindings pinned to the version keep working; new bindings against the exact semver fail with SCAISKILLS_YANKED_VERSION; floating refs (latest, ^x.y) skip yanked versions.
Returns { "semver": "0.2.0", "status": "yanked" }.
Bindings#
POST /bindings#
Create a binding with eager dependency resolution. Requires scaiskills:bind.
| Field | Required | Notes |
|---|---|---|
skill_id |
yes | The skill's id (not slug). |
version |
yes | 0.1.0 (exact), latest, ^1.2, ~1.2, >=1.0. The leading @ is optional. |
scope_type |
yes | workspace, channel, user, or core. |
scope_id |
yes | Id under the scope type. |
secret_mappings |
no | { "secret_name": "vault_path" }. Required entries for any required: true secret in the manifest. |
What happens server-side:
- Resolve the version ref against published, non-yanked versions.
- Walk
manifest.requires.skillsdepth-first, resolve each dependency, detect cycles, build the lockfile. - Check granted permissions vs. declared permissions, mapped secrets vs. required secrets, set
pending_grants. - Persist the binding row with
resolved_deps_jsonandsecret_mappings_json. - Invalidate the Redis cache for the scope.
Returns the full binding row.
GET /bindings?scope_type=&scope_id=#
List bindings for a given scope. Requires scaiskills:view. Both query parameters are required.
DELETE /bindings/{binding_id}#
Delete a binding and cascade its grants. Requires scaiskills:bind. Invalidates the scope's Redis cache. Returns { "deleted": true } (or { "deleted": false } if the id wasn't found — idempotent).
POST /bindings/{binding_id}/permissions/grant#
Grant one declared permission on a binding. Requires scaiskills:grant.
1 2 3 | |
After the grant, the binding's manifest is rechecked; if every declared permission is granted and every required secret is mapped, pending_grants flips to false. Scope cache is invalidated.
Returns { "id", "binding_id", "permission_string", "granted_at" }.
Resolve#
POST /resolve#
Return the lightweight manifest list for a scope. Requires scaiskills:view. Called by the runtime — ScaiWave per turn (scope_type: channel | user), ScaiCore at boot (scope_type: core).
1 2 3 4 5 6 | |
The ancillary workspace_id, channel_id, user_id, and core_id fields are merged into the candidate set; scope_type is the primary scope and dictates the Redis cache key.
Returns:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Active, non-pending_grants bindings only. Deduplicated by skill_id with precedence core > user > channel > workspace. Cached in Redis for 60 seconds; every binding write for the scope invalidates.
MCP tools#
The progressive-disclosure surface the LLM calls during a turn. Each call logs a row in mod_scaiskills_invocations for metering.
POST /mcp/skills/list#
The lightweight manifest list for the caller's scope. Identical body shape to /resolve; identical output shape. Requires scaiskills:view.
POST /mcp/skills/search#
Semantic search over bound skills. Query parameter query plus the same scope body as /mcp/skills/list. Requires scaiskills:view.
1 2 3 4 | |
Two-stage pipeline: a ScaiMatrix vector search over the indexed manifests, intersected against the caller's resolved set so out-of-scope skills cannot leak. On ScaiMatrix failure, falls back to a substring filter over the resolved set. Each match includes score and match_excerpt.
POST /mcp/skills/view/{slug}#
Return the full SKILL.md body (default) or a file under references/. Requires scaiskills:view and an active binding for the slug in the caller's scope — otherwise SCAISKILLS_SKILL_NOT_FOUND.
Query parameter path selects an alternative file: references/refund-policy.md (or just refund-policy.md — the references/ prefix is added if missing). Absolute paths and .. segments are rejected.
Returns:
1 2 3 4 5 6 | |
The YAML frontmatter is stripped from SKILL.md so the LLM only sees the body.
Errors#
ScaiSkills-specific codes:
| Code | HTTP | Meaning |
|---|---|---|
SCAISKILLS_SKILL_NOT_FOUND |
404 | Slug doesn't exist or isn't visible to the caller. |
SCAISKILLS_VERSION_NOT_FOUND |
404 | No version with this semver for the skill. |
SCAISKILLS_BINDING_NOT_FOUND |
404 | Binding id doesn't exist. |
SCAISKILLS_SLUG_CONFLICT |
409 | Slug already taken (globally). |
SCAISKILLS_VERSION_CONFLICT |
409 | Version exists or is not strictly greater than the previous version. |
SCAISKILLS_VALIDATION_FAILED |
422 | Bundle or manifest didn't validate. context.errors enumerates. |
SCAISKILLS_DEPENDENCY_CYCLE |
422 | requires.skills formed a cycle during bind resolution. |
SCAISKILLS_UNRESOLVABLE_DEPENDENCY |
422 | A dependency ref didn't match any published version. |
SCAISKILLS_PENDING_GRANTS |
409 | Binding has ungranted permissions or unmapped required secrets when an active path was attempted. |
SCAISKILLS_YANKED_VERSION |
410 | Tried to bind to a yanked version. |
SCAISKILLS_BUNDLE_TOO_LARGE |
413 | Bundle exceeded the size cap. |
SCAISKILLS_STORAGE_ERROR |
500 | S3 put/get failed. |
All errors follow ScaiGrid's standard envelope with code, message, optional details, and a request id in meta.