---
title: "actor.skill-listing@2 · OpenVibe.Services"
canonical: "https://openvibe.services/docs/contracts/actor.skill-listing"
updated: null
---

# actor.skill-listing@2 · OpenVibe.Services

HTML: [actor.skill-listing@2 · OpenVibe.Services](https://openvibe.services/docs/contracts/actor.skill-listing)

# ActorSkillListingV2 `actor.skill-listing@2`

Generated at 2026-11-13 06:25:48 UTC from [`openvibe-contracts v0.419.0`](https://github.com/OpenVibers/OpenVibe.Contracts/tree/v0.419.0) and [`openvibe-sdk v0.161.0`](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.161.0).

- Version: 2.1.0
- Owner: actor
- Visibility: public
- Status: active
- Compatibility: backward
- Decision: [ADR-060](/docs/adr/ADR-060)
- Schema: [`https://openvibe.network/contracts/actor/skill-listing.v2.json`](/docs/contracts/actor.skill-listing.json)

actor.skill-listing@2: a skill in the registry, the record every site shows on its card (ADR-058, ADR-060). It keeps every field and meaning of actor.skill-listing@1 and adds what a sandbox skill needs to be shown and run: the document is actor.skill@2, CODE lists, per code resource, its name (the resource path a tool's entry names), the Media object id its bytes are stored under and the sha256 of those bytes, so the bytes that run are the bytes that were scanned; and the SCAN's findings may name the TOOL they are about, so a person reads the scan one tool at a time. A sandbox listing carries code; a prompt listing carries none. It still carries the PUBLISHER from the registry's own record (vouched: unlike the document's author, this is who published it), the SCAN (its state and findings travel with the listing), and the CURATOR's state. Install rules every site keeps: a passed skill installs in one tap; a flagged one only after the person reads the findings and confirms; a failed one never; a stale or archived one is not offered. Installing copies the document into the person's agent; it grants nothing (the skill's requires is a declaration). VISIBILITY is public (the default, and what every listing is today) or private (only its publisher sees or installs it); because a private listing always belongs to a person, VISIBILITY private requires PUBLISHER.KIND person.

## Fields

| Field | Type | Required | Description | Constraints |
| --- | --- | --- | --- | --- |
| `listing` | const | yes |  | - `= "actor.skill-listing@2"` |
| `id` | string | yes | The listing's id. | - `pattern ^skl_[0-9A-HJKMNP-TV-Z]{26}$` |
| `skill` | [actor.skill](/docs/contracts/actor.skill) | yes | The document as published. |  |
| `code` | array of object |  | Per code resource of a sandbox skill: the resource path a tool entry names, the Media object the registry stored its bytes under, and the sha256 of those bytes. Required when the skill runs in a sandbox; absent for a prompt skill. | - `maxItems 10`<br>- `unique items`<br>- `items: no other fields` |
| `code[].name` | string | yes | The resource path, as the document's tools entry names it. | - `pattern ^[a-z0-9][a-z0-9_-]{0,59}(?:/[a-z0-9][a-z0-9_-]{0,59}){0,3}\.js$` |
| `code[].media_id` | string | yes | The Media object id the code bytes are stored under. | - `pattern ^med_[0-9A-HJKMNP-TV-Z]{26}$` |
| `code[].sha256` | string | yes | Lowercase hex SHA-256 of the code bytes. | - `pattern ^[0-9a-f]{64}$` |
| `publisher` | object | yes | Who published it, from the registry's own record. | - `no other fields` |
| `publisher.kind` | enum | yes | OpenVibe's own skills, or a person's. | - `one of "openvibe", "person"` |
| `publisher.subject` | string |  | The person who published it (kind person). | - `pattern ^usr_[0-9A-HJKMNP-TV-Z]{26}$` |
| `publisher.name` | string |  |  | - `minLength 1`<br>- `maxLength 80` |
| `visibility` | enum |  | public: anyone may see and install it, as every listing is today (the default, and what an absent field means). private: only its publisher sees or installs it, so its publisher.kind is person. | - `one of "public", "private"`<br>- `default "public"` |
| `scan` | object | yes | What the registry's scan found. It travels with the listing, and a finding about a tool names it. | - `no other fields` |
| `scan.state` | enum | yes |  | - `one of "pending", "passed", "flagged", "failed"` |
| `scan.scanner` | string |  | The scanner and its version. | - `minLength 1`<br>- `maxLength 80` |
| `scan.scanned_at` | string |  |  | - `format date-time` |
| `scan.findings` | array of object |  |  | - `maxItems 50`<br>- `items: no other fields` |
| `scan.findings[].rule` | string | yes | Which rule, such as code.network or code.env. | - `pattern ^[a-z][a-z0-9_.-]{1,63}$` |
| `scan.findings[].severity` | enum | yes |  | - `one of "info", "warn", "block"` |
| `scan.findings[].where` | string |  | instructions, description, or a resource path. | - `maxLength 120` |
| `scan.findings[].tool` | string |  | The tool the finding is about, when it is about one tool. | - `pattern ^[a-z][a-z0-9_]{0,31}$` |
| `scan.findings[].note` | string | yes | What it found, in plain words. | - `minLength 1`<br>- `maxLength 280` |
| `curator` | object | yes | The curator ships with the catalogue: what is used stays, what is not goes stale. | - `no other fields` |
| `curator.state` | enum | yes |  | - `one of "active", "stale", "archived"` |
| `curator.last_used_at` | string |  |  | - `format date-time` |
| `curator.near_duplicate_of` | string |  | A listing that does the same job. | - `pattern ^skl_[0-9A-HJKMNP-TV-Z]{26}$` |
| `installs` | integer |  |  | - `minimum 0` |
| `listed_at` | string | yes |  | - `format date-time` |
| `retired_at` | string |  |  | - `format date-time` |

## Examples

From the contract's own test fixtures: valid ones validate, rejected ones must fail.

 Valid: flagged

~~~~
{
  "listing": "actor.skill-listing@1",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPR",
  "skill": {
    "skill": "actor.skill@1",
    "slug": "summarize-a-stream",
    "name": "Summarize a stream",
    "description": "Use when someone asks what happened in a stream recording: a short summary with the moments that mattered.",
    "version": "1.0.0",
    "instructions": "Read the transcript. Write five bullets, each with its time. Name nobody who did not speak.",
    "runs": "prompt",
    "integrity": {
      "alg": "sha256",
      "hash": "dadad5ff3666d12b2270015d96dd5d6f784138a2eeb9010f5a1979ce86f9ff02"
    }
  },
  "publisher": {
    "kind": "openvibe"
  },
  "scan": {
    "state": "flagged",
    "scanner": "openvibe-skill-scan 1.0.0",
    "scanned_at": "2026-11-07T23:02:00.000Z",
    "findings": [
      {
        "rule": "asks.undeclared_kind",
        "severity": "warn",
        "where": "instructions",
        "note": "The instructions ask to post, and the skill does not declare post."
      }
    ]
  },
  "curator": {
    "state": "stale",
    "near_duplicate_of": "skl_01JAB2C3D4E5F6G7H8J9K0MNPQ"
  },
  "installs": 0,
  "listed_at": "2026-11-07T23:02:00.000Z"
}
~~~~

Valid: passed

~~~~
{
  "listing": "actor.skill-listing@1",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPQ",
  "skill": {
    "skill": "actor.skill@1",
    "slug": "weekly-changelog-post",
    "name": "Weekly changelog post",
    "description": "Use when asked to write the week's changelog: what shipped, grouped by product, as a draft post.",
    "version": "1.2.0",
    "instructions": "1. Read the changelog entries of the last seven days.\n2. Group them by product, newest first.\n3. Write a draft with the template in template.md.\n4. Publish only after the person approves it.",
    "resources": [
      {
        "path": "template.md",
        "media_type": "text/markdown",
        "text": "# This week on OpenVibe\n\n## <product>\n- <what shipped>"
      }
    ],
    "requires": {
      "kinds": [
        "publish"
      ],
      "apps": [],
      "hosts": [
        "openvibe.blog"
      ]
    },
    "runs": "prompt",
    "license": "CC-BY-4.0",
    "author": {
      "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
      "name": "Ann"
    },
    "created_at": "2026-11-07T23:00:00.000Z",
    "integrity": {
      "alg": "sha256",
      "hash": "b3e351154b87238e67434bc973c15c7a831742a43728529b05532dd14e5d9893"
    }
  },
  "publisher": {
    "kind": "person",
    "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
    "name": "Ann"
  },
  "scan": {
    "state": "passed",
    "scanner": "openvibe-skill-scan 1.0.0",
    "scanned_at": "2026-11-07T23:01:00.000Z",
    "findings": []
  },
  "curator": {
    "state": "active",
    "last_used_at": "2026-11-07T23:30:00.000Z"
  },
  "installs": 12,
  "listed_at": "2026-11-07T23:01:00.000Z"
}
~~~~

Valid: private-person

~~~~
{
  "listing": "actor.skill-listing@2",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPW",
  "skill": {
    "skill": "actor.skill@2",
    "slug": "summarize-a-stream",
    "name": "Summarize a stream",
    "description": "Use when someone asks what happened in a stream recording: a short summary with the moments that mattered.",
    "version": "1.0.0",
    "instructions": "Read the transcript. Write five bullets, each with its time. Name nobody who did not speak.",
    "resources": [
      {
        "path": "outline.md",
        "media_type": "text/markdown",
        "text": "# Summary\n\n- <time> <moment>"
      }
    ],
    "requires": {
      "kinds": [
        "post"
      ]
    },
    "runs": "prompt",
    "license": "MIT",
    "integrity": {
      "alg": "sha256",
      "hash": "4a3e9e27aff7547cdbd9ad085faa6fa9ea4c523e87df3cdcc491d75e35b590d2"
    }
  },
  "publisher": {
    "kind": "person",
    "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
    "name": "Ann"
  },
  "visibility": "private",
  "scan": {
    "state": "passed",
    "scanner": "openvibe-skill-scan 1.1.0",
    "scanned_at": "2026-11-08T03:01:00.000Z",
    "findings": []
  },
  "curator": {
    "state": "active"
  },
  "listed_at": "2026-11-08T03:01:00.000Z"
}
~~~~

Valid: sandbox-public

~~~~
{
  "listing": "actor.skill-listing@2",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPQ",
  "skill": {
    "skill": "actor.skill@2",
    "slug": "fetch-a-page-title",
    "name": "Fetch a page title",
    "description": "Use when someone gives a web address and asks what the page is called: reads the title over the internet.",
    "version": "2.1.0",
    "instructions": "Call fetch_title with the https URL. It runs in a sandbox with the internet, and only for this call.",
    "resources": [
      {
        "path": "fetch_title.js",
        "media_type": "text/javascript",
        "text": "'use strict';\nlet raw = '';\nprocess.stdin.setEncoding('utf8');\nprocess.stdin.on('data', (chunk) => { raw += chunk; });\nprocess.stdin.on('end', async () => {\n  const input = JSON.parse(raw);\n  const response = await fetch(input.url, { signal: AbortSignal.timeout(8000) });\n  const html = await response.text();\n  const title = (html.match(/<title[^>]*>([^<]*)<\\/title>/i) || [])[1] || '';\n  process.stdout.write(JSON.stringify({ status: response.status, title: title.trim() }));\n});\n"
      }
    ],
    "tools": [
      {
        "name": "fetch_title",
        "description": "Read a page over https and answer its status and title.",
        "language": "javascript",
        "entry": "fetch_title.js",
        "args": {
          "type": "object",
          "properties": {
            "url": {
              "type": "string",
              "pattern": "^https://",
              "maxLength": 2048
            }
          },
          "required": [
            "url"
          ],
          "additionalProperties": false
        },
        "net": "public",
        "limits": {
          "wall_ms": 15000,
          "mem_bytes": 134217728
        }
      }
    ],
    "requires": {
      "kinds": [
        "skill.run",
        "send"
      ]
    },
    "runs": "sandbox",
    "license": "MIT",
    "author": {
      "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
      "name": "Ann"
    },
    "integrity": {
      "alg": "sha256",
      "hash": "53d984eb0e639c1b07c99b9a5aec6d64ea9e594cbe404a0a9256debf7cb8b49e"
    }
  },
  "code": [
    {
      "name": "fetch_title.js",
      "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
      "sha256": "f75ae8394b4d2e7d7667e0645071534862f321b2015017c26ca49bf1f3a32b80"
    }
  ],
  "publisher": {
    "kind": "person",
    "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
    "name": "Ann"
  },
  "scan": {
    "state": "flagged",
    "scanner": "openvibe-skill-scan 1.1.0",
    "scanned_at": "2026-11-08T01:01:00.000Z",
    "findings": [
      {
        "rule": "code.network",
        "severity": "warn",
        "where": "fetch_title.js",
        "tool": "fetch_title",
        "note": "The tool asks for the internet; the person must read this before install."
      }
    ]
  },
  "curator": {
    "state": "active",
    "last_used_at": "2026-11-08T01:30:00.000Z"
  },
  "installs": 3,
  "listed_at": "2026-11-08T01:01:00.000Z"
}
~~~~

Rejected: bad-state

~~~~
{
  "listing": "actor.skill-listing@1",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPT",
  "skill": {
    "skill": "actor.skill@1",
    "slug": "summarize-a-stream",
    "name": "Summarize a stream",
    "description": "Use when someone asks what happened in a stream recording: a short summary with the moments that mattered.",
    "version": "1.0.0",
    "instructions": "Read the transcript. Write five bullets, each with its time. Name nobody who did not speak.",
    "runs": "prompt",
    "integrity": {
      "alg": "sha256",
      "hash": "dadad5ff3666d12b2270015d96dd5d6f784138a2eeb9010f5a1979ce86f9ff02"
    }
  },
  "publisher": {
    "kind": "openvibe"
  },
  "scan": {
    "state": "trusted"
  },
  "curator": {
    "state": "active"
  },
  "listed_at": "2026-11-07T23:02:00.000Z"
}
~~~~

Rejected: no-scan

~~~~
{
  "listing": "actor.skill-listing@1",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPS",
  "skill": {
    "skill": "actor.skill@1",
    "slug": "summarize-a-stream",
    "name": "Summarize a stream",
    "description": "Use when someone asks what happened in a stream recording: a short summary with the moments that mattered.",
    "version": "1.0.0",
    "instructions": "Read the transcript. Write five bullets, each with its time. Name nobody who did not speak.",
    "runs": "prompt",
    "integrity": {
      "alg": "sha256",
      "hash": "dadad5ff3666d12b2270015d96dd5d6f784138a2eeb9010f5a1979ce86f9ff02"
    }
  },
  "publisher": {
    "kind": "openvibe"
  },
  "curator": {
    "state": "active"
  },
  "listed_at": "2026-11-07T23:02:00.000Z"
}
~~~~

Rejected: private-openvibe

~~~~
{
  "listing": "actor.skill-listing@2",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPX",
  "skill": {
    "skill": "actor.skill@2",
    "slug": "summarize-a-stream",
    "name": "Summarize a stream",
    "description": "Use when someone asks what happened in a stream recording: a short summary with the moments that mattered.",
    "version": "1.0.0",
    "instructions": "Read the transcript. Write five bullets, each with its time. Name nobody who did not speak.",
    "resources": [
      {
        "path": "outline.md",
        "media_type": "text/markdown",
        "text": "# Summary\n\n- <time> <moment>"
      }
    ],
    "requires": {
      "kinds": [
        "post"
      ]
    },
    "runs": "prompt",
    "license": "MIT",
    "integrity": {
      "alg": "sha256",
      "hash": "4a3e9e27aff7547cdbd9ad085faa6fa9ea4c523e87df3cdcc491d75e35b590d2"
    }
  },
  "publisher": {
    "kind": "openvibe"
  },
  "visibility": "private",
  "scan": {
    "state": "passed",
    "scanner": "openvibe-skill-scan 1.1.0",
    "scanned_at": "2026-11-08T03:01:00.000Z",
    "findings": []
  },
  "curator": {
    "state": "active"
  },
  "listed_at": "2026-11-08T03:01:00.000Z"
}
~~~~

Rejected: sandbox-without-code

~~~~
{
  "listing": "actor.skill-listing@2",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPQ",
  "skill": {
    "skill": "actor.skill@2",
    "slug": "slugify-a-title",
    "name": "Slugify a title",
    "description": "Use when a draft needs a URL slug: lowercase words joined by hyphens, no network.",
    "version": "1.0.0",
    "instructions": "Call slugify with the title. It runs in a sandbox with no network. Use the slug in a URL.",
    "resources": [
      {
        "path": "slugify.js",
        "media_type": "text/javascript",
        "text": "'use strict';\nlet raw = '';\nprocess.stdin.setEncoding('utf8');\nprocess.stdin.on('data', (chunk) => { raw += chunk; });\nprocess.stdin.on('end', () => {\n  const input = JSON.parse(raw);\n  const slug = String(input.title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n  process.stdout.write(JSON.stringify({ slug }));\n});\n"
      }
    ],
    "tools": [
      {
        "name": "slugify",
        "description": "Turn a title into a URL slug: lowercase words joined by hyphens.",
        "language": "javascript",
        "entry": "slugify.js",
        "args": {
          "type": "object",
          "properties": {
            "title": {
              "type": "string",
              "maxLength": 120,
              "description": "The title to turn into a slug."
            },
            "shorten": {
              "type": "boolean"
            }
          },
          "required": [
            "title"
          ],
          "additionalProperties": false
        },
        "net": "deny"
      }
    ],
    "requires": {
      "kinds": [
        "skill.run"
      ]
    },
    "runs": "sandbox",
    "license": "MIT",
    "author": {
      "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
    },
    "created_at": "2026-11-08T01:00:00.000Z",
    "integrity": {
      "alg": "sha256",
      "hash": "84f9b0bc23d10534158184aa9b1952ef078f7e21d4d4fa6f3aa0f3a1e13ca1db"
    }
  },
  "publisher": {
    "kind": "person",
    "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
  },
  "scan": {
    "state": "passed"
  },
  "curator": {
    "state": "active"
  },
  "listed_at": "2026-11-08T01:01:00.000Z"
}
~~~~

Rejected: unknown-visibility

~~~~
{
  "listing": "actor.skill-listing@2",
  "id": "skl_01JAB2C3D4E5F6G7H8J9K0MNPY",
  "skill": {
    "skill": "actor.skill@2",
    "slug": "summarize-a-stream",
    "name": "Summarize a stream",
    "description": "Use when someone asks what happened in a stream recording: a short summary with the moments that mattered.",
    "version": "1.0.0",
    "instructions": "Read the transcript. Write five bullets, each with its time. Name nobody who did not speak.",
    "resources": [
      {
        "path": "outline.md",
        "media_type": "text/markdown",
        "text": "# Summary\n\n- <time> <moment>"
      }
    ],
    "requires": {
      "kinds": [
        "post"
      ]
    },
    "runs": "prompt",
    "license": "MIT",
    "integrity": {
      "alg": "sha256",
      "hash": "4a3e9e27aff7547cdbd9ad085faa6fa9ea4c523e87df3cdcc491d75e35b590d2"
    }
  },
  "publisher": {
    "kind": "person",
    "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
    "name": "Ann"
  },
  "visibility": "shared",
  "scan": {
    "state": "passed",
    "scanner": "openvibe-skill-scan 1.1.0",
    "scanned_at": "2026-11-08T03:01:00.000Z",
    "findings": []
  },
  "curator": {
    "state": "active"
  },
  "listed_at": "2026-11-08T03:01:00.000Z"
}
~~~~

## Validate

~~~~
const contracts = require('openvibe-contracts');
contracts.validate('actor.skill-listing@2', value);   // { valid, errors: [{ path, message }] }
~~~~
