Skills API¶
Base URL: /api/v1
Authentication¶
The Skills API uses dual authentication — the auth method depends on the endpoint:
| Endpoint type | Auth method | Credential | Required scope |
|---|---|---|---|
Sync-down reads (GET /skills, GET /skills/{id}/content) | Session cookie or Bearer token | jarvis_registry_session cookie / Authorization: Bearer <token> | skills-read |
All other reads (GET /skills/{id}, GET /skills/{id}/files/{path}) | Session cookie only | jarvis_registry_session cookie | skills-read |
Writes (POST, PATCH, DELETE) | Session cookie only | jarvis_registry_session cookie + X-Jarvis-CSRF header | skills-write |
Sync-down reads accept Bearer tokens so that CLI agents can pull skill content using managed-agent tokens without a browser session. Write endpoints are session-only to prevent proxy token replay.
Content type: Content-Type: application/json
Access Control¶
Skills use the same ACL system as MCP Servers and A2A Agents:
- Resource type:
skill(inRegistryResourceTypeenum) - Permission bits: VIEW (1), EDIT (3), DELETE / OWNER (15)
- Seed roles:
skill_viewer(permBits=1),skill_editor(permBits=3),skill_owner(permBits=15) - On create: the author is automatically granted OWNER permission (atomic transaction)
- On delete: all ACL entries for the skill are removed (atomic transaction)
List endpoints return only skills the user has VIEW access to. Individual operations check the required permission level (VIEW for reads, EDIT for updates/toggle, DELETE for deletion).
Data Sharing with Jarvis Chat¶
Skills and SkillFiles are stored in shared MongoDB collections (skills, skillfiles). Registry uses Beanie's use_state_management = True so that save() emits incremental $set updates, preserving Chat-only fields that are not modelled on ExtendedSkill.
Key distinctions:
| Field | Purpose |
|---|---|
createdByRegistry: bool | Only true for skills created through this API. Registry refuses to delete Chat-created skills (409). |
source (on SkillFile) | "registry-inline" means content is stored in body: bytes. Other values (Chat sources) return available: false. |
enabled: bool | Document-level toggle. Chat skills default to enabled; Registry skills can be toggled via the toggle endpoint. |
Endpoints¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET | /skills | Dual | List skills (ACL-filtered) |
POST | /skills | Session + CSRF | Create a skill |
GET | /skills/{skill_id} | Session | Get skill detail with file metadata |
GET | /skills/{skill_id}/content | Dual | Get skill content for CLI sync-down |
GET | /skills/{skill_id}/files/{file_path} | Session | Get individual file content |
PUT | /skills/{skill_id}/files/{file_path} | Session + CSRF | Create or replace a supporting file |
DELETE | /skills/{skill_id}/files/{file_path} | Session + CSRF | Delete a supporting file |
PATCH | /skills/{skill_id} | Session + CSRF | Update a skill (metadata only; not files) |
DELETE | /skills/{skill_id} | Session + CSRF | Delete a skill |
POST | /skills/{skill_id}/toggle | Session + CSRF | Toggle skill enabled state |
1. List Skills¶
GET /skills
Returns metadata for all skills the authenticated user has VIEW access to, ordered by updatedAt ascending.
Query Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | boolean | — | Filter by enabled state |
fileCount | integer (≥ 0) | — | Filter by file count |
Request Example¶
Response¶
200 OK
{
"skills": [
{
"id": "<skill-objectid>",
"name": "mongoose-to-beanie",
"displayTitle": "Mongoose to Beanie",
"description": "Convert Mongoose schemas to Beanie models",
"category": "development",
"tags": ["python", "mongodb"],
"path": "mongoose-to-beanie",
"version": 3,
"fileCount": 0,
"alwaysApply": false,
"enabled": true,
"author": "<user-objectid>",
"authorName": "Jane Doe",
"source": "inline",
"sourceMetadata": null,
"createdByRegistry": true,
"permissions": {
"VIEW": true,
"EDIT": true,
"DELETE": true,
"SHARE": true
},
"updatedAt": "2026-08-05T10:01:00Z",
"deletedAt": null
}
]
}
Response Fields¶
| Field | Type | Description |
|---|---|---|
skills | array | ACL-filtered skill metadata |
skills[].id | string | MongoDB ObjectID |
skills[].name | string | Unique name (kebab-case, ^[a-z0-9]+(?:-[a-z0-9]+)*$) |
skills[].displayTitle | string or null | Human-readable display title |
skills[].description | string | Skill description |
skills[].category | string | Skill category |
skills[].tags | string[] | Tags |
skills[].path | string | Local directory name; falls back to name |
skills[].version | integer | Increments on each update |
skills[].fileCount | integer | Number of supporting files |
skills[].alwaysApply | boolean | Whether the skill is always applied |
skills[].enabled | boolean | Whether the skill is active |
skills[].author | string | Author's MongoDB user ID |
skills[].authorName | string | Author's display name |
skills[].source | string | Source type ("inline", etc.) |
skills[].sourceMetadata | object or null | Source-specific metadata |
skills[].createdByRegistry | boolean | true only for skills created through this API; false for Chat-created skills |
skills[].permissions | object | Caller's permissions (VIEW, EDIT, DELETE, SHARE) |
skills[].updatedAt | datetime | Last update time |
skills[].deletedAt | datetime or null | Soft-delete tombstone |
2. Create Skill¶
POST /skills
Creates a new skill. The authenticated user is automatically granted OWNER permission. A duplicate check is performed on (name, author) — returns 409 on conflict.
Request Body¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Kebab-case name, max 64 chars. Pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$ |
displayTitle | string | No | null | Display title, max 128 chars |
description | string | Yes | — | Description, max 1024 chars |
body | string | No | "" | Markdown body, max 100,000 chars |
category | string | No | "" | Category, max 128 chars |
tags | string[] | No | [] | Tags |
alwaysApply | boolean | No | false | Always-apply flag |
userInvocable | boolean | No | true | Whether users can invoke directly |
disableModelInvocation | boolean | No | false | Disable model invocation |
allowedTools | string[] or null | No | null | Tool whitelist; null = unrestricted |
files | SkillFileInput[] | No | [] | Supporting files created inline (see below); omit for a single-SKILL.md skill |
SkillFileInput¶
| Field | Type | Required | Description |
|---|---|---|---|
relativePath | string | Yes | POSIX path relative to the skill directory. Server-validated: non-empty, no absolute paths, no ../. segments, no backslashes, normalized (no ./, //, trailing /), not SKILL.md. |
content | string | Exactly-one | UTF-8 text content. Provide either content or body, not both. |
body | string | Exactly-one | Base64-encoded binary content. |
mimeType | string | No | Defaults to mimetypes.guess_type(relativePath) or application/octet-stream. |
isExecutable | boolean | No | Default false. The CLI applies chmod +x on sync when true. |
isBinary | boolean or null | No | Optional. The server always recomputes it from the actual bytes; if the client sends a value, it must match the server's determination or the request is rejected (422). |
Server-enforced limits (all return 422 on breach): - Single file: 5 MiB (MAX_SKILL_FILE_SIZE), applied to the decoded byte count. - Total per skill: 10 MiB (MAX_SKILL_FILES_TOTAL_SIZE). - Count per skill: 50 (MAX_SKILL_FILE_COUNT). - Duplicate relativePath within a single request is rejected.
Wire size note. Binary payloads travel as base64 in JSON, which expands the raw bytes by ~1.33×. A single 5 MiB binary is therefore ~6.7 MiB on the wire, and a full 10 MiB skill can approach ~13.3 MiB of JSON body. Front-facing proxies (nginx
client_max_body_size, ALB request-size limits, API gateway caps) must allow at least this much or clients will receive413 Payload Too Largeinstead of Registry's422.
Request Example¶
POST /api/v1/skills
Cookie: jarvis_registry_session=<token>
X-Jarvis-CSRF: <hmac>
Content-Type: application/json
{
"name": "mongoose-to-beanie",
"description": "Convert Mongoose schemas to Beanie models",
"body": "# Mongoose to Beanie\n\nFollow these instructions...\n",
"category": "development",
"tags": ["python", "mongodb"],
"files": [
{
"relativePath": "scripts/run_review.sh",
"content": "#!/usr/bin/env bash\nset -euo pipefail\n",
"mimeType": "text/x-shellscript",
"isExecutable": true
},
{
"relativePath": "assets/logo.png",
"body": "<base64-encoded>",
"mimeType": "image/png"
}
]
}
Response¶
201 Created — returns SkillDetailResponse (same shape as GET detail). The response files[] reflects the files just created.
Errors¶
| Status | Condition |
|---|---|
409 Conflict | A skill with this name already exists for this author |
422 Unprocessable Entity | Name pattern validation failed; file path invalid; per-file / total / count limit exceeded; duplicate relativePath; content/body neither-or-both; invalid base64; isBinary declaration disagrees with the actual bytes |
3. Get Skill Detail¶
GET /skills/{skill_id}
Returns full skill detail including file metadata (without file content) and the caller's permissions.
Path Parameters¶
| Parameter | Type | Description |
|---|---|---|
skill_id | ObjectID | Skill ID |
Response¶
200 OK
{
"id": "<skill-objectid>",
"name": "mongoose-to-beanie",
"displayTitle": "Mongoose to Beanie",
"description": "Convert Mongoose schemas to Beanie models",
"body": "# Mongoose to Beanie\n\nFollow these instructions...\n",
"frontmatter": {
"name": "mongoose-to-beanie",
"description": "Convert Mongoose schemas to Beanie models",
"alwaysApply": false
},
"category": "development",
"tags": ["python", "mongodb"],
"version": 3,
"fileCount": 1,
"enabled": true,
"alwaysApply": false,
"userInvocable": true,
"disableModelInvocation": false,
"allowedTools": null,
"author": "<user-objectid>",
"authorName": "Jane Doe",
"source": "inline",
"sourceMetadata": null,
"createdByRegistry": true,
"createdAt": "2026-08-01T12:00:00Z",
"updatedAt": "2026-08-05T10:01:00Z",
"files": [
{
"id": "<file-objectid>",
"relativePath": "references/guide.md",
"mimeType": "text/markdown",
"bytes": 2048,
"isBinary": false,
"isExecutable": false,
"source": "registry-inline"
}
],
"permissions": {
"VIEW": true,
"EDIT": true,
"DELETE": true,
"SHARE": true
}
}
4. Get Skill Content (Sync-Down)¶
GET /skills/{skill_id}/content
Returns the data required to reconstruct the local skill directory. This is the primary endpoint for CLI sync. Accepts both session cookie and Bearer token authentication.
Response¶
200 OK
{
"id": "<skill-objectid>",
"name": "mongoose-to-beanie",
"description": "Convert Mongoose schemas to Beanie models",
"body": "# Mongoose to Beanie\n\nFollow these instructions...\n",
"frontmatter": {
"name": "mongoose-to-beanie",
"description": "Convert Mongoose schemas to Beanie models"
},
"alwaysApply": false,
"disableModelInvocation": false,
"userInvocable": true,
"allowedTools": null,
"category": "development",
"createdByRegistry": true,
"files": [
{
"relativePath": "references/guide.md",
"content": "# Guide\n\nReference content here.",
"mimeType": "text/markdown",
"bytes": 2048,
"isBinary": false,
"isExecutable": false,
"source": "registry-inline"
},
{
"relativePath": "assets/icon.png",
"body": "<base64-encoded>",
"mimeType": "image/png",
"bytes": 4096,
"isBinary": true,
"isExecutable": false,
"source": "registry-inline"
}
]
}
Each entry in files[] is a SkillFileResponse: text registry-inline files carry content; binary registry-inline files carry body (base64) instead, with content omitted.
File Availability¶
Files with source: "registry-inline" include their content. Files created in Jarvis Chat (other source values) return available: false with a reason:
{
"relativePath": "data/config.json",
"mimeType": "application/json",
"bytes": 512,
"isBinary": false,
"source": "github",
"available": false,
"unavailableReason": "File content is not available in Registry because it was created in Jarvis Chat."
}
5. Get File Content¶
GET /skills/{skill_id}/files/{file_path}
Returns content for a single file. Registry-inline files return text content or base64-encoded binary. Chat-created files return available: false.
Path Parameters¶
| Parameter | Type | Description |
|---|---|---|
skill_id | ObjectID | Skill ID |
file_path | string | Relative file path (e.g., references/guide.md) |
Response¶
200 OK — Registry-inline text file:
{
"relativePath": "references/guide.md",
"content": "# Guide\n\nReference content here.",
"mimeType": "text/markdown",
"isBinary": false,
"available": true
}
200 OK — Registry-inline binary file:
{
"relativePath": "assets/icon.png",
"body": "<base64-encoded>",
"mimeType": "image/png",
"isBinary": true,
"available": true
}
200 OK — Chat-created file:
{
"relativePath": "data/config.json",
"mimeType": "application/json",
"isBinary": false,
"available": false,
"unavailableReason": "File content is not available in Registry because it was created in Jarvis Chat."
}
5b. Upsert File¶
PUT /skills/{skill_id}/files/{file_path}
Create or replace a single registry-inline supporting file. Supports incremental file management — the client sends only the file being changed, not the whole skill. Requires EDIT on the skill and createdByRegistry = true. Bumps the skill's version and refreshes updatedAt.
Request Body — SkillFileUpsertRequest¶
Same field set as SkillFileInput (see §2) without relativePath (comes from the URL): content or body (exactly one), optional mimeType, isExecutable, isBinary.
Response¶
201 Created— the file did not exist and was created.200 OK— the file existed and was replaced.
Body: SkillFileMetadataResponse.
{
"id": "<file-objectid>",
"relativePath": "scripts/run_review.sh",
"mimeType": "text/x-shellscript",
"bytes": 42,
"isBinary": false,
"isExecutable": true,
"source": "registry-inline"
}
Errors¶
| Status | Condition |
|---|---|
403 Forbidden | Caller lacks EDIT |
404 Not Found | Skill does not exist |
409 Conflict | Skill was created in Jarvis Chat (createdByRegistry = false); the target file exists but is not registry-inline (Chat/GitHub-owned); or the skill was modified concurrently and MongoDB rejected the write (retry the request) |
422 Unprocessable Entity | Any file-validation failure listed in §2 (path, size, count, total, base64, isBinary mismatch, content/body cardinality) |
5c. Delete File¶
DELETE /skills/{skill_id}/files/{file_path}
Remove a single registry-inline supporting file. Requires EDIT and createdByRegistry = true. Bumps the skill's version and refreshes updatedAt.
Response¶
204 No Content.
Errors¶
| Status | Condition |
|---|---|
404 Not Found | Skill or file does not exist |
409 Conflict | createdByRegistry = false; the file's source is not registry-inline; or the skill was modified concurrently and MongoDB rejected the write (retry the request) |
422 Unprocessable Entity | file_path fails relative-path validation |
6. Update Skill¶
PATCH /skills/{skill_id}
Partial update of metadata only. Supporting files are managed via PUT/DELETE /skills/{skill_id}/files/{file_path} (§5b, §5c) so callers only re-send the file being changed. Increments version and updates updatedAt. Frontmatter fields (name, description, alwaysApply, userInvocable, disableModelInvocation, allowedTools) are automatically synced to the frontmatter object.
Request Body¶
All fields are optional. Only include fields to update.
| Field | Type | Nullable | Description |
|---|---|---|---|
name | string | No | New name (triggers duplicate check) |
displayTitle | string | Yes | Display title |
description | string | No | Description |
body | string | No | Markdown body |
category | string | No | Category |
tags | string[] | No | Tags |
alwaysApply | boolean | No | Always-apply flag |
userInvocable | boolean | No | User-invocable flag |
disableModelInvocation | boolean | No | Disable model invocation |
allowedTools | string[] or null | Yes | Tool whitelist |
Sending null for non-nullable fields returns 422 Unprocessable Entity. An empty body (no fields) returns the current state without incrementing version.
Request Example¶
PATCH /api/v1/skills/<skill-objectid>
Cookie: jarvis_registry_session=<token>
X-Jarvis-CSRF: <hmac>
Content-Type: application/json
{
"description": "Updated description",
"category": "migration"
}
Response¶
200 OK — returns SkillDetailResponse.
Errors¶
| Status | Condition |
|---|---|
409 Conflict | New name conflicts with an existing skill |
422 Unprocessable Entity | Non-nullable field set to null |
7. Delete Skill¶
DELETE /skills/{skill_id}
Permanently deletes a skill and all its files. Only skills created in Registry (createdByRegistry: true) can be deleted. Chat-created skills return 409 Conflict.
The operation is atomic (MongoDB transaction): deletes skill files, the skill document, and all ACL entries.
Response¶
204 No Content
Errors¶
| Status | Condition |
|---|---|
404 Not Found | Skill does not exist or is already deleted |
409 Conflict | Skill was created in Jarvis Chat, not Registry |
8. Toggle Skill¶
POST /skills/{skill_id}/toggle
Enables or disables a skill. Requires EDIT permission.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | New enabled state |
Request Example¶
POST /api/v1/skills/<skill-objectid>/toggle
Cookie: jarvis_registry_session=<token>
X-Jarvis-CSRF: <hmac>
Content-Type: application/json
{
"enabled": false
}
Response¶
200 OK
Error Responses¶
All errors use the standard FastAPI response shape:
| Status | Condition |
|---|---|
401 Unauthorized | Authentication failed. Dual-auth endpoints include WWW-Authenticate: Bearer challenge. |
403 Forbidden | Insufficient permissions (ACL check failed) |
404 Not Found | Skill or file does not exist |
409 Conflict | Duplicate name or attempting to delete a Chat-created skill |
422 Unprocessable Entity | Validation error (name pattern, null on non-nullable field, invalid ObjectID) |
500 Internal Server Error | Unexpected server failure |
Local Directory Reconstruction¶
The CLI creates one directory per Skill:
SKILL.md is reconstructed by combining promoted fields with frontmatter and body:
---
name: mongoose-to-beanie
description: Convert Mongoose schemas to Beanie models
always-apply: false
disable-model-invocation: false
user-invocable: true
category: development
---
# Mongoose to Beanie
Follow these instructions...
When allowedTools is non-null, it maps to allowed-tools in the frontmatter. When isExecutable is true on a supporting file, the CLI must set the file's execute permission (chmod +x) after writing.
Clients should still normalize relativePath values before writing to disk. On the write side, Registry already rejects absolute paths, parent traversal (..), duplicate paths, backslashes, non-normalized POSIX paths, and the reserved SKILL.md name with 422 Unprocessable Entity.
Scopes¶
Defined in registry-pkgs/src/registry_pkgs/scopes.yml:
| Scope | Actions |
|---|---|
skills-read | skills:list, skills:get, skills:getContent, skills:getFileContent |
skills-write | skills:create, skills:update, skills:delete, skills:toggle, skills:upsertFile, skills:deleteFile |