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

Architecture

ScaiSkills is a ScaiGrid module — it runs in the same FastAPI process as the rest of the gateway, behind the same auth, accounted against the same workspace. There is no separate skill engine, no separate database. The whole module is a thin orchestration layer over six tables, an S3 bucket, and a Redis cache.

Components#

flowchart LR P[Publisher<br/>tar.gz bundle] A[Admin<br/>binds and grants] R[Runtime<br/>ScaiWave, ScaiCore<br/>per-turn or boot] subgraph SG[ScaiGrid /v1/modules/scaiskills/] V[validator vendored] S[storage<br/>content-addr S3] VR[mod_scaiskills_versions] MI[matrix_index<br/>best-effort] BS[binding_service<br/>- resolve version<br/>- walk requires.skills<br/>- lockfile JSON<br/>- pending_grants flag] RS[resolver_service<br/>- Redis cache 60s<br/>- scope precedence<br/>- lightweight manifests] MS[mcp_service<br/>- list resolver<br/>- search ScaiMatrix + fallback<br/>- view S3 + tar extract] V --> S S --> VR VR --> MI end P -- POST /skills/.. --> V A -- POST /bindings --> BS R -- POST /resolve --> RS R -- POST /mcp/skills/list,search,view --> MS

The three paths#

ScaiSkills has three distinct request flows. They share state through the same tables but the code paths barely overlap.

Publish#

  1. POST /skills writes a mod_scaiskills_skills row — slug, owner workspace, visibility, description.
  2. POST /skills/{slug}/versions accepts a multipart upload of a .tar.gz bundle.
  3. The vendored validator under modules/scaiskills/_validator/ opens the tar, parses SKILL.md's YAML frontmatter, validates it against the manifest JSON schema, and runs eight stages (slug match, semver monotonicity, references safety, secrets schema, etc.).
  4. Storage computes the content hash. If a bundle with the same hash already exists, the put is skipped and the existing URI is reused.
  5. The version row goes into mod_scaiskills_versions with the parsed manifest stored as JSON text.
  6. Best-effort: matrix_index.index_skill_version writes a text entry into the ScaiMatrix __scaiskills collection so skills.search can do semantic recall. Failure here logs and continues — search has a substring fallback.

Bind#

  1. POST /bindings receives a skill_id and a version spec (0.1.0, latest, ^0.1, etc.).
  2. The version spec resolves against mod_scaiskills_versions, filtered to status = published. Yanked versions trigger an explicit error.
  3. The resolver walks manifest.requires.skills depth-first, resolving each dependency reference the same way, detecting cycles via a visiting/visited set.
  4. The full dependency tree is serialised into resolved_deps_json on the binding row — this is the lockfile. It is never recomputed for the binding's lifetime.
  5. The manifest's declared permissions and secrets are checked. Anything declared-but-not-granted (for permissions) or required-but-not-mapped (for secrets) puts the binding in pending_grants = true.
  6. The binding row lands in mod_scaiskills_bindings. The Redis cache for the scope is invalidated.

Resolve#

  1. POST /resolve is called by the runtime — per turn for ScaiWave with scope_type = channel | user, once at boot for ScaiCore with scope_type = core. The request can also carry ancillary workspace, channel, user, and core ids to be merged in.
  2. The resolver checks Redis at scaiskills:resolve:{scope_type}:{scope_id} and returns the cached list (60 s TTL) on hit.
  3. On miss, all active, non-pending_grants bindings matching any of the requested scopes are loaded in one query.
  4. Precedence collapses duplicates per skill_id: core > user > channel > workspace. Each skill_id is represented at most once in the response.
  5. The winning bindings' manifests are loaded and projected into the lightweight shape: slug, version, description, triggers. Full bodies are not returned.
  6. The list is written back to Redis and the cache key is recorded in mod_scaiskills_cache_keys so the binding write paths can invalidate it cheaply on the next change.

The MCP surface#

/mcp/skills/list, /mcp/skills/search, and /mcp/skills/view are the progressive-disclosure tools the LLM calls during a turn. They live behind the same auth as everything else, but their semantics are designed for tool-use:

  • list returns exactly what resolve returns — the lightweight manifest set. The LLM scans descriptions and triggers.
  • search runs the user's query through ScaiMatrix semantic search over the indexed manifests, intersects against the bound set so out-of-scope skills don't leak, and falls back to a substring filter if ScaiMatrix is unavailable.
  • view is the only path that opens the tarball. It fetches the bundle from S3 by content hash, extracts SKILL.md (or references/<path> for additional files), strips the YAML frontmatter, and returns the markdown body. The LLM only pays the token cost for skills it actually decides to read.

Every MCP call logs a mod_scaiskills_invocations row tagged with the workspace, scope, operation, and bytes returned — the metering hook for downstream billing.

State#

  • Skill identities, versions, bindings, grants, invocations, cache keys — six tables under mod_scaiskills_* in ScaiGrid's MariaDB.
  • Bundles — content-addressed in S3 under scaigrid-skills/{skill_id}/{content_hash}.tar.gz. Byte-identical bundles dedup.
  • Parsed manifests — JSON-serialised on the version row so resolve and MCP never re-open the tarball.
  • Search index — a single ScaiMatrix collection (default __scaiskills) tenant-isolated by workspace id. Best-effort write at publish time.
  • Resolution cache — Redis, 60 s TTL per scope key, invalidated on every binding write.

There is no other state. Lose Redis and the next resolve repopulates from MariaDB. Lose the ScaiMatrix index and skills.search falls back to substring filtering on the resolved set.

Where the trust boundary is#

The publish path trusts the bundle's manifest only after the validator finishes. The bind path trusts the lockfile only after cycle detection completes. The resolve path trusts nothing transient — every call re-checks enabled and pending_grants against the live binding row (or against a cache entry that was invalidated the last time those flags changed). The MCP path enforces scope gating on every call: skills.view of a slug that isn't in the caller's resolved set returns 404, never the bundle.

What the runtime sees vs. what's stored#

There's a deliberate asymmetry between what POST /resolve returns and what the binding row actually carries. Resolve returns only slug, version, description, and triggers — enough for the LLM to recognise that a skill is in scope, not enough to act on it. The full SKILL.md body and the references/ directory stay in S3 until the LLM explicitly calls skills.view. That's the progressive-disclosure contract: the per-turn cost of having ten skills bound is linear in the number of slugs (a sentence each), not in the sum of bundle sizes.

The bind path is the opposite. The lockfile, the secret mappings, the grant ledger — all of it lives on the binding row and is computed once. The runtime never re-walks requires.skills, never re-validates the manifest, never re-checks grants against a moving target. Anything that could change semantics over a binding's lifetime is frozen at bind time. The only state the resolve path is allowed to read live is the binding's own enabled and pending_grants flags.

How it differs from a static system prompt#

A static system prompt baked into a bot config gives you predictability — every turn sees the same string. ScaiSkills adds:

Concern Static prompt ScaiSkills
Reuse across bots Copy-paste Bind once per scope
Versioning None Semver + monotonicity + yank
Per-turn cost All instructions every turn Slug + description; body on demand
Permission gating Manual policy Declared, granted, audited
Search by intent Not applicable skills.search with ScaiMatrix
Dependency reuse Copy-paste requires.skills with lockfile

If you only need one prompt for one bot, ScaiBot's tone config is the right tool. If you need any of the rows above, ScaiSkills is.

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