openapi: 3.0.3
info:
  title: Graph-of-Skills API
  description: |
    Skill disambiguation and retrieval for AI coding agents. Upload a tenant's
    skill library (SKILL.md-style markdown files), then query it in natural
    language to get back the skill that actually matches, not just the one
    that looks similar.

    ## Authentication
    Every endpoint except `/pending`, `/install-link` and `/install/{token}`
    requires a self-serve
    API key, scoped to the `graph-of-skills` product. Pass it as `Authorization: Bearer <key>` or,
    for clients that can't set that header, `X-API-Key: <key>`. An invalid,
    unscoped, or revoked key gets the same 401 as a missing one — the API
    never reveals whether a key exists. `/pending` and `/install-link` take a
    signed-in account session instead and refuse every API key.
    `/install/{token}` takes neither: the link that `/install-link` makes
    carries its own permission, for one skill and for 10 minutes.

    Get a key at https://www.akilima.tech/get-api-key?product=graph-of-skills
    (free tier, no card required).

    ## Rate limits
    Two independent caps apply per tenant: a per-minute burst limit (fails
    with a distinct `retryAfterSeconds` body) and a monthly retrieval volume
    cap (fails with a `limit`/`upgradeUrl` body). Both are enforced on
    `/retrieve` and the `search_skills` MCP tool identically.

    ## MCP
    `/mcp` is the same retrieval capability exposed as a Model Context
    Protocol server (Streamable HTTP transport) for coding agents that
    connect via MCP instead of calling REST directly:
    `claude mcp add --transport http graph-of-skills
    https://www.akilima.tech/api/graph-of-skills/mcp`.
  version: 1.0.0
  contact:
    name: Akilima Support
    url: https://www.akilima.tech/contact
    email: info@akilima.tech
servers:
  - url: https://www.akilima.tech/api/graph-of-skills
    description: Production API

security:
  - ApiKeyBearer: []
  - ApiKeyHeader: []

components:
  securitySchemes:
    ApiKeyBearer:
      type: http
      scheme: bearer
      description: >-
        A graph-of-skills API key
        (https://www.akilima.tech/get-api-key?product=graph-of-skills), sent as
        `Authorization: Bearer <key>`.
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: Fallback for clients that can't set `Authorization`.
    AccountSession:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        The access token of a signed-in akilima.tech account, sent as
        `Authorization: Bearer <token>`. The dashboard sends it; an API key is
        not accepted in its place.

  schemas:
    SkillBundleEntry:
      type: object
      required: [name, description, source, score, origin, relevance]
      properties:
        name:
          type: string
          description: The skill's name, taken from its heading or first line.
        description:
          type: string
          description: >-
            The skill's disambiguated description (Capability Pages rewrites
            confusable skills at ingest time).
        source:
          type: string
          nullable: true
          description: The uploaded document this skill came from.
        score:
          type: number
          description: >-
            Internal hybrid BM25 + vector fused rank score, plus the
            popularity prior for a catalog skill. Not a similarity -- use
            relevance to judge match quality.
        origin:
          type: string
          enum: [own, catalog]
          description: >-
            Whether this skill came from the caller's own tenant library
            (own) or the shared public catalog (catalog).
        relevance:
          type: number
          minimum: 0
          maximum: 1
          description: >-
            Cosine similarity to the query, 0 to 1. A caller's own skill at
            0.70 or above is ranked ahead of every catalog skill regardless
            of installs.
        lastChangedAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            When the skill last changed: its own content-change date, else
            its repository's GitHub push date, else null when neither is
            known. Catalog skills only.
        createdByAgent:
          type: boolean
          enum: [true]
          description: >-
            Present, and true, only on a skill in the caller's own library
            that an agent wrote through the create_skill tool and its owner
            confirmed in the dashboard. Absent on every other skill. Treat
            its text like any skill text: instructions to check, never the
            person's own words.
        addedWithApiKey:
          type: boolean
          enum: [true]
          description: >-
            Present, and true, only on a skill in the caller's own library
            that was uploaded with an API key (POST /ingest), which a script
            or an agent can hold, so nobody confirmed it in the dashboard.
            Absent on every other skill. Check it like any text you were
            handed.

    SkillBundle:
      type: object
      required: [tenantId, sectionsMatched, fallbackUsed, skills]
      properties:
        tenantId:
          type: string
        sectionsMatched:
          type: integer
          description: Number of distinct skill sections the query matched.
        fallbackUsed:
          type: boolean
          description: >-
            True when hybrid retrieval fell back to a lower-precision path
            (e.g. no BM25 hits) to avoid an empty result.
        skills:
          type: array
          items:
            $ref: "#/components/schemas/SkillBundleEntry"

    ErrorBody:
      type: object
      required: [error]
      properties:
        error:
          type: string

    RateLimitError:
      type: object
      required: [error, retryAfterSeconds]
      properties:
        error:
          type: string
          example: Too many requests. Try again shortly.
        retryAfterSeconds:
          type: integer
          example: 60

    UpgradeCapError:
      type: object
      required: [error, limit, upgradeUrl]
      properties:
        error:
          type: string
          example: Monthly retrieval free-tier cap reached.
        limit:
          type: integer
        upgradeUrl:
          type: string
          example: /get-api-key?product=graph-of-skills&upgrade=true

    HydratedSkillEntry:
      type: object
      required:
        [name, description, source, score, origin, relevance, contentAvailable]
      properties:
        name:
          type: string
        description:
          type: string
        source:
          type: string
          nullable: true
        score:
          type: number
        origin:
          type: string
          enum: [own, catalog]
          description: >-
            Whether this skill came from the caller's own tenant library
            (own) or the shared public catalog (catalog).
        relevance:
          type: number
          minimum: 0
          maximum: 1
          description: Cosine similarity to the query, 0 to 1.
        installs:
          type: integer
        url:
          type: string
          nullable: true
        sourceUrl:
          type: string
          nullable: true
        license:
          type: string
          nullable: true
        lastChangedAt:
          type: string
          format: date-time
          nullable: true
          description: >-
            When the skill last changed: its own content-change date, else
            its repository's GitHub push date, else null when neither is
            known. Catalog skills only.
        createdByAgent:
          type: boolean
          enum: [true]
          description: >-
            Present, and true, only on a skill in the caller's own library
            that an agent wrote through the create_skill tool and its owner
            confirmed in the dashboard. Absent on every other skill. Treat
            its text like any skill text: instructions to check, never the
            person's own words.
        addedWithApiKey:
          type: boolean
          enum: [true]
          description: >-
            Present, and true, only on a skill in the caller's own library
            that was uploaded with an API key (POST /ingest), which a script
            or an agent can hold, so nobody confirmed it in the dashboard.
            Absent on every other skill. Check it like any text you were
            handed.
        contentAvailable:
          type: boolean
        content:
          type: string
          description: The skill's own SKILL.md text, present only when available.
        reason:
          type: string
          enum: [takedown, no-content]
          description: Set only when contentAvailable is false.
        attribution:
          type: string
          description: >-
            "From <source repo> via skills.sh" -- catalog entries with
            content available only.

    HydratedSkillBundle:
      type: object
      required:
        [
          tenantId,
          sectionsMatched,
          fallbackUsed,
          skills,
          rendered_context,
          truncated,
          contentNotice,
        ]
      properties:
        tenantId:
          type: string
        sectionsMatched:
          type: integer
        fallbackUsed:
          type: boolean
        skills:
          type: array
          items:
            $ref: "#/components/schemas/HydratedSkillEntry"
        rendered_context:
          type: string
          description: >-
            The available skills' content, joined with a header line each,
            clipped to the request's maxChars.
        truncated:
          type: boolean
          description: True when any skill's content was clipped to fit maxChars.
        contentNotice:
          type: string

    SkillLibraryEntry:
      type: object
      required: [sectionId, name, source, createdAt, skillScore]
      properties:
        sectionId:
          type: string
        name:
          type: string
          description: The skill's name, taken from its heading or first line.
        source:
          type: string
          nullable: true
          description: The uploaded document this skill came from.
        createdAt:
          type: string
          format: date-time
        skillScore:
          type: number
          nullable: true
          description: >-
            Confusability score stamped at ingest time, or null when it
            hasn't been computed for this skill.

    SkillLibrary:
      type: object
      required:
        [
          skills,
          sectionCount,
          maxSkillsPerTenant,
          catalogSkillCount,
          supersedeSuggestions,
          installLinks,
        ]
      properties:
        skills:
          type: array
          items:
            $ref: "#/components/schemas/SkillLibraryEntry"
        sectionCount:
          type: integer
          description: >-
            The tenant's current indexed-skill count, the same number the
            free-tier skill-count cap is checked against.
        maxSkillsPerTenant:
          type: integer
          description: The flat, all-tiers skill-count ceiling.
        catalogSkillCount:
          type: integer
          description: >-
            The number of public catalogue skills searched for every tenant
            by default, independent of this tenant's own uploads.
        supersedeSuggestions:
          type: array
          items:
            $ref: "#/components/schemas/SupersedeSuggestion"
          description: >-
            Own skills that a newer, near-identical catalogue skill could
            replace, and that the tenant hasn't already answered about.
        installLinks:
          type: boolean
          description: >-
            True when this server can mint install links (`/install-link`).
            The dashboard shows its install button only then.

    SupersedeSuggestion:
      type: object
      required:
        [
          ownSectionId,
          catalogSkillId,
          catalogSkillName,
          catalogInstalls,
          catalogLastChangedAt,
          similarity,
        ]
      properties:
        ownSectionId:
          type: string
          description: The tenant's own skill this suggestion is about.
        catalogSkillId:
          type: string
          description: The public catalogue skill it could be replaced with.
        catalogSkillName:
          type: string
        catalogInstalls:
          type: integer
          description: The catalogue skill's install count.
        catalogLastChangedAt:
          type: string
          format: date-time
          description: When the catalogue skill last changed.
        similarity:
          type: number
          description: Cosine similarity between the two skills, 0 to 1.

    InstallLink:
      type: object
      required: [url, command, expiresAt]
      properties:
        url:
          type: string
          format: uri
          description: >-
            Where the skill's `SKILL.md` downloads from, with no login. The
            link carries its own permission, so anyone who holds it can
            download that one skill until it expires.
        command:
          type: string
          description: >-
            The command that installs the skill from `url`, for a terminal on
            the machine that should have it.
          example: npx skills add https://www.akilima.tech/i/<link> -g -a claude-code -y
        expiresAt:
          type: string
          format: date-time
          description: When the link stops working, 10 minutes after it was made.

    McpCapabilities:
      type: object
      required: [name, version, capabilities]
      properties:
        name:
          type: string
          example: graph-of-skills
        version:
          type: string
          example: 0.1.0
        capabilities:
          type: object
          properties:
            tools:
              type: object
              properties:
                search_skills:
                  type: object

    PendingSkill:
      type: object
      required:
        [id, name, request, skillMd, riskFlags, createdAt, expiresAt, daysLeft]
      properties:
        id:
          type: string
        name:
          type: string
          description: The skill's name, from the frontmatter of `skillMd`.
        request:
          type: string
          description: What the person asked the agent for.
        skillMd:
          type: string
          description: The SKILL.md exactly as the agent wrote it.
        riskFlags:
          type: array
          description: >-
            Text the safety check flagged when the draft was saved. A flag
            below the refusal line is shown here and does not stop a confirm.
          items:
            type: object
            required: [family, rule, line, excerpt]
            properties:
              family:
                type: string
                enum:
                  [
                    hidden_text,
                    encoded_blob,
                    exfiltrate_credentials,
                    hide_or_override,
                    disable_safety,
                    remote_code,
                  ]
              rule:
                type: string
              line:
                type: integer
              excerpt:
                type: string
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        daysLeft:
          type: integer
          description: Whole days left, rounded up.

    PendingSkillList:
      type: object
      required: [drafts]
      properties:
        drafts:
          type: array
          description: The caller's unexpired drafts, newest first.
          items:
            $ref: "#/components/schemas/PendingSkill"

    PendingConfirmed:
      type: object
      required: [ok, name, sectionsCreated, chunksCreated]
      properties:
        ok:
          type: boolean
          enum: [true]
        name:
          type: string
        sectionsCreated:
          type: integer
        chunksCreated:
          type: integer

    PendingDiscarded:
      type: object
      required: [ok, name]
      properties:
        ok:
          type: boolean
          enum: [true]
        name:
          type: string

    PendingFailure:
      type: object
      required: [ok, reason, message, retryable]
      properties:
        ok:
          type: boolean
          enum: [false]
        reason:
          type: string
          enum:
            [not_found, expired, bad_draft, high_risk, cap_reached, unavailable]
        message:
          type: string
        retryable:
          type: boolean
          description: >-
            True when the same call can succeed later. False when the draft or
            the account has to change first.
        riskFlags:
          type: array
          description: Present with `high_risk`.
          items:
            type: object
        limit:
          type: integer
          description: Present with `cap_reached`.
        upgradeUrl:
          type: string
          description: Present with `cap_reached`.

  responses:
    Unauthorized:
      description: >-
        Missing, malformed, unknown, revoked, or wrong-product API key --
        always the same body, so a caller can't learn which case applies.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            error: Unauthorized
    BadRequest:
      description: Invalid request body, or an empty/missing query.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
    RateLimited:
      description: Over the per-minute burst limit.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/RateLimitError"
    OverCap:
      description: Over the monthly free-tier volume cap.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UpgradeCapError"

paths:
  /ingest:
    post:
      summary: Upload a skill library
      description: >-
        Accepts one or more SKILL.md-style markdown files under the repeated
        `files` multipart field. Confusable skills are disambiguated
        (Capability Pages) and indexed for retrieval, scoped to the calling
        tenant. At most 5 files per request (one file takes about 9 seconds);
        send larger libraries as several requests. Authenticate with
        `Authorization: Bearer <key>`; no `Origin` header is needed.
        A file that starts with a frontmatter block (`name`, `description`)
        is one skill; a file without one is cut into one skill per heading.
        A file uploaded with a key is stored as `key/<file name>` and comes
        back with `addedWithApiKey: true`, because a key can be held by an
        agent and nobody confirmed the skill in the dashboard. File names
        that start with `key/` or `created/` are reserved and get a 400.
      operationId: ingestSkillLibrary
      tags: [Ingestion]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [files]
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                  description: One or more skill-library markdown files.
      responses:
        "200":
          description: Files ingested successfully.
          content:
            application/json:
              schema:
                type: object
                required: [tenantId, sectionsCreated, chunksCreated]
                properties:
                  tenantId:
                    type: string
                  sectionsCreated:
                    type: integer
                  chunksCreated:
                    type: integer
        "400":
          description: >-
            No `files` provided, the request body isn't a valid multipart
            form, or a file name starts with `created/`. That prefix is
            reserved for skills added through the `create_skill` tool, and
            nothing in the request is stored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          description: >-
            More than 5 files in one request. Nothing was stored. The body
            carries `limit` (files that fit), `received` and a `hint`.
          content:
            application/json:
              schema:
                type: object
                required: [error, limit, received]
                properties:
                  error:
                    type: string
                  limit:
                    type: integer
                  received:
                    type: integer
                  hint:
                    type: string
        "429":
          description: >-
            Over the flat, all-tiers skill-count ceiling (a technical fair-use
            limit, not a paid upgrade lever).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpgradeCapError"

  /retrieve:
    post:
      summary: Query the skill library
      description: >-
        Runs hybrid (BM25 + vector) retrieval over the tenant's ingested
        skills and returns the disambiguated matches, budget-aware and
        ranked. Identical behavior, input for input, to the `search_skills`
        MCP tool below.
      operationId: retrieveSkillBundle
      tags: [Retrieval]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  description: Natural-language description of the task at hand.
      responses:
        "200":
          description: Matching skills, best first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SkillBundle"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          description: Rate-limited (burst) or over the monthly cap.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/RateLimitError"
                  - $ref: "#/components/schemas/UpgradeCapError"

  /bundle:
    post:
      summary: Query the skill library and get hydrated content
      description: >-
        Runs the same merged, budget-aware retrieval as `/retrieve`, then
        hydrates each selected skill with its real content (a catalog
        skill's published SKILL.md, or the caller's own indexed section) and
        a ready-to-paste rendered_context string clipped to maxChars.
        Content is provided as published by its creators; the caller is
        responsible for how they use it.
      operationId: retrieveHydratedBundle
      tags: [Retrieval]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  description: Natural-language description of the task at hand.
                maxSkills:
                  type: integer
                  description: Most skills to return. Default 5, max 20.
                maxChars:
                  type: integer
                  description: >-
                    Character budget for rendered_context. Default 15000,
                    max 60000.
      responses:
        "200":
          description: Matching skills with content, best first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HydratedSkillBundle"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          description: Rate-limited (burst) or over the monthly cap.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/RateLimitError"
                  - $ref: "#/components/schemas/UpgradeCapError"

  /library:
    get:
      summary: List a tenant's indexed skills and free-tier usage
      description: >-
        Returns every skill currently indexed for the calling tenant plus
        how much of the free-tier skill-count cap they've used --
        the read side of `/ingest`, used to render the account dashboard's
        skill-library card.
      operationId: listSkillLibrary
      tags: [Library]
      responses:
        "200":
          description: The tenant's skill library and usage.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SkillLibrary"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /mcp:
    get:
      summary: MCP server capabilities (health check)
      description: >-
        A lightweight, auth-gated capabilities check for a developer or
        monitoring caller. Not part of the MCP protocol handshake itself --
        see POST below for that.
      operationId: getMcpCapabilities
      tags: [MCP]
      responses:
        "200":
          description: Server capabilities.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/McpCapabilities"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      summary: MCP protocol endpoint (Streamable HTTP transport)
      description: >-
        The real Model Context Protocol entry point: JSON-RPC 2.0 over the
        MCP Streamable HTTP transport (stateless -- no session is persisted
        between requests). Exposes one tool, `search_skills(query)`, which
        calls the exact same retrieval path as `POST /retrieve`. Intended for
        an MCP client library (e.g. `@modelcontextprotocol/sdk`'s
        `StreamableHTTPClientTransport`), not direct hand-written requests --
        see https://modelcontextprotocol.io for the protocol's own request/
        response shapes.
      operationId: callMcp
      tags: [MCP]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: A JSON-RPC 2.0 MCP request (initialize, tools/list, tools/call, ...).
      responses:
        "200":
          description: A JSON-RPC 2.0 MCP response.
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"

  /pending:
    get:
      summary: List the skill drafts waiting for review
      description: >-
        Returns the calling account's drafts that an agent wrote with the
        `create_skill` tool and that have not expired. A draft is not
        searchable until it is confirmed, and it is deleted after seven days.
        Requires a signed-in account session. An API key gets 401, so the
        agent that wrote a draft cannot release it.
      operationId: listPendingSkills
      tags: [Library]
      security:
        - AccountSession: []
      responses:
        "200":
          description: The caller's drafts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingSkillList"
        "401":
          description: No valid account session. Every API key gets this too.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Unauthorized
        "503":
          description: The drafts could not be read. Try again in a minute.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingFailure"
    post:
      summary: Confirm or discard one skill draft
      description: >-
        `confirm` adds the draft to the caller's own skill library, where the
        next search can find it, and deletes the draft. The safety check runs
        again first, and the skill-count cap applies unless the draft replaces
        a skill made the same way. Nothing is added on a refusal, and the draft
        stays so it can be read or discarded. `discard` deletes the draft.
        Requires a signed-in account session. An API key gets 401.
      operationId: actOnPendingSkill
      tags: [Library]
      security:
        - AccountSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id, action]
              properties:
                id:
                  type: string
                  description: The draft's `id` from the list.
                action:
                  type: string
                  enum: [confirm, discard]
      responses:
        "200":
          description: Done.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/PendingConfirmed"
                  - $ref: "#/components/schemas/PendingDiscarded"
        "400":
          description: Missing `id`, or `action` isn't `confirm` or `discard`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: No valid account session. Every API key gets this too.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Unauthorized
        "404":
          description: >-
            No draft with that id in the caller's account. It may be confirmed,
            discarded or expired already.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingFailure"
        "410":
          description: The draft expired and was removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingFailure"
        "422":
          description: >-
            `bad_draft`: the stored text no longer reads as a skill.
            `high_risk`: the safety check now finds text that works against
            the person using it, and `riskFlags` lists it. Nothing was added.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingFailure"
        "429":
          description: >-
            The skill-count cap is reached. `limit` and `upgradeUrl` say what
            it is and where to raise it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingFailure"
        "503":
          description: >-
            The draft or the library could not be read, or the skill could not
            be added. Try again in a minute. The draft is kept.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PendingFailure"

  /install-link:
    post:
      summary: Make a 10-minute link that installs one of your own skills
      description: >-
        Returns a link for one skill in the caller's own skill library and the
        `npx skills add` command that installs it. The link works for 10
        minutes and as many times as it is used in that time. It carries its
        own permission, so treat it like a password until it expires. The
        installed `SKILL.md` is rebuilt from the stored skill, so scripts and
        extra files the skill came with are not in it. Catalogue skills are
        not covered; they have their own install command.
        Requires a signed-in account session. An API key gets 401, so an agent
        cannot make a link for itself.
      operationId: mintSkillInstallLink
      tags: [Library]
      security:
        - AccountSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sectionId]
              properties:
                sectionId:
                  type: string
                  maxLength: 200
                  description: The skill's `sectionId` from `/library`.
      responses:
        "200":
          description: The link, its install command and when it stops working.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InstallLink"
        "400":
          description: The body isn't JSON, or `sectionId` is missing or not a string.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: No valid account session. Every API key gets this too.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Unauthorized
        "404":
          description: >-
            No skill with that id in the caller's own library. It may belong to
            another account, be a catalogue skill, or have been deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "500":
          description: The skill could not be read. Try again later.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "503":
          description: Install links are not set up on this server.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
              example:
                error: Install links are not set up on this server.

  /install/{token}/{rest}:
    get:
      summary: Download one of your own skills from an install link
      description: >-
        The address inside the command that `/install-link` returns. The
        `npx skills add` command treats it as a base and fetches two files
        under it: `.well-known/agent-skills/index.json`, which names the one
        skill with the SHA-256 of its text, then `SKILL.md`. Any other `rest`
        answers 404. It takes no API key and no
        login: the token in the path is the only credential, so treat the link
        like a password until it expires. A good token answers with that one
        skill's `SKILL.md`, as many times as it is used in its 10 minutes. A
        token that is wrong in any way answers 404 with the same body: altered,
        expired, for a skill that was deleted or belongs to another account, or
        sent to a server where install links are not set up. The answer does
        not say which. Nothing is cached. The file is rebuilt from the stored
        skill, so scripts and extra files the skill came with are not in it.
      operationId: downloadSkillFromInstallLink
      tags: [Library]
      security: []
      parameters:
        - name: token
          in: path
          required: true
          description: >-
            The token at the end of the link `/install-link` made. Letters,
            digits, `-` and `_`, about 55 of them (an older long form also holds
            one `.`); never longer than 1,024 characters.
          schema:
            type: string
            maxLength: 1024
        - name: rest
          in: path
          required: true
          description: >-
            `.well-known/agent-skills/index.json` for the index, or `SKILL.md`
            for the skill's text.
          schema:
            type: string
            enum: [".well-known/agent-skills/index.json", SKILL.md]
      responses:
        "200":
          description: The skill's `SKILL.md`.
          headers:
            Cache-Control:
              description: Always `no-store`; the link is a credential.
              schema:
                type: string
                example: no-store
            X-Robots-Tag:
              description: Always `noindex`.
              schema:
                type: string
                example: noindex
          content:
            text/markdown:
              schema:
                type: string
              example: |
                ---
                name: "send-email"
                description: "Send a plain email to a recipient."
                ---

                Write the message, then send it.
        "404":
          description: >-
            The link does not work. The body is the fixed text `Not found`
            whatever the reason, and it is never cached.
          content:
            text/plain:
              schema:
                type: string
              example: Not found
        "500":
          description: The skill could not be read. Try the link again.
          content:
            text/plain:
              schema:
                type: string
              example: Could not load the skill.

  /supersede:
    post:
      summary: Answer a supersede suggestion
      description: >-
        Records the tenant's answer to "use this newer, near-identical
        catalogue skill instead of your own?" (`SkillLibrary.supersedeSuggestions`,
        returned by `/library`). Re-answering the same pair overwrites the
        previous choice rather than adding a new record; no answer ever
        deletes the tenant's own skill.
      operationId: recordSupersedeDecision
      tags: [Library]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ownSectionId, catalogSkillId, choice]
              properties:
                ownSectionId:
                  type: string
                  description: The tenant's own skill the suggestion is about.
                catalogSkillId:
                  type: string
                  description: The catalogue skill it could be replaced with.
                choice:
                  type: string
                  enum: [use_catalog, keep_mine, keep_both]
      responses:
        "200":
          description: The decision was recorded.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
        "400":
          description: Missing field, or `choice` isn't one of the three values.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: >-
            `ownSectionId` isn't one of the calling tenant's own indexed
            skills.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
