Skip to content

Ship a package's skills, so tool selection works on a clone - #181

Merged
davidmckayv merged 1 commit into
mainfrom
feat/a-package-ships-its-skills
Aug 22, 2026
Merged

Ship a package's skills, so tool selection works on a clone#181
davidmckayv merged 1 commit into
mainfrom
feat/a-package-ships-its-skills

Conversation

@davidmckayv

Copy link
Copy Markdown
Contributor

The problem #178 left

Tool selection narrows a Bot's tools to the ones its matching skills declare. A deployment starts with no skills at all — there is no skills.yaml, nothing seeds any, and nothing has ever written one (origin: "catalogue" exists in the schema and no code path produces it).

So on every fresh clone there is nothing to match against, every Bot falls to nothing-declared, and the narrowing never switches on. I shipped the mechanism inert and did not notice.

#180 spotted the same gap and fills it with a screen where you tick tools per skill. That works, but it makes the manual step permanent: somebody maps tools to skills by hand, in every deployment, again after every new connector. That is curation a product with a services team absorbs. OpenBot is a template.

What this does

A tenant package may carry skills.yaml:

skills:
  - slug: find-a-document
    title: Find a document
    summary: Search the connected document sources for a file and read what it says.
    instructions: >-
      Search first, then read the file you found rather than answering from its title.
    tools:
      - google-drive/search_files
      - google-drive/read_file_content

Seeded on boot as deployment skills. Everybody sees them in the / menu; which Bots carry them is decided in Admin like any other. Connecting a connector is the only step left.

Four decisions worth naming

The file is optional. Every package written before this loads unchanged and ships no skills. Present and malformed is still refused, like every other package file.

Declared refs are not checked against known tools, unlike the API path. A package is written before anybody connects anything, so it necessarily names tools for connectors that may not be added yet — and that is precisely what makes them work the moment somebody does. Refusing would mean a template could only ship skills for connectors it could guarantee, which is none of them. An unknown ref sits inert; the offer is still intersected with the Bot's grants. skill_tools carries no foreign key for this reason.

Naming a tool grants nothing, unchanged. This PR does not touch the grant path, the intersection in grantedTools, or the policy.

A skill somebody wrote here keeps its name. The / namespace is shared and first to take it wins. A collision is skipped and logged, not thrown — unlike the agent case directly above it in the same function. Anybody signed in may write a skill, so throwing would let one person stop the deployment booting by choosing a name.

skills.yaml is in the package checksum, so editing it is a package change and the next boot reseeds rather than reporting itself unchanged.

Proof

server/tests/tenant-package.test.ts, 13 new tests, 40 in the file, all passing. Parsing: no file, empty file, refs carried through, a ref for a connector nobody added, a skill with no tools, a slug nobody could type, a ref that is not serverId/toolName, missing instructions. Seeding against a real database: lands with origin: catalogue and null owner, unknown refs stored anyway, re-seeding replaces the declared set rather than adding to it, and a user-written slug survives with the deployment still booting.

Full suite: 1302 pass, 0 fail. Typecheck, lint, format clean.

Driven in the browser. Restarted the local deployment against the example package and watched it seed:

      slug       |  origin   | tools
-----------------+-----------+-------
 check-a-claim   | catalogue |     2
 find-a-document | catalogue |     3
 shipcheck       | yours     |     0     <- written here before, untouched
 whats-changed   | catalogue |     2
 who-owns-this   | catalogue |     2

All four render on /skills under Workspace skills with their / commands, and the pre-existing user skill stays under Your skills.

Honest limits

With only Drive connected you are still under the floor. Drive publishes four tools, and narrowing starts above twelve, so a clone with one connector sees no narrowing — correctly. What this changes is that the second and third connector need no clicks: the skills are already there declaring what they need. It also makes the shipped skills useful on their own as / commands, which they are regardless of selection.

Only Drive is covered, because Drive is the only catalogue connector. A package for a deployment with its own MCP servers writes its own refs.

No screen shows what a Bot would actually be offered. A skill can declare a tool its Bot does not hold; that is correct and invisible until mcp.tools_discovered records it. #180 names the same gap and it is still real.

On #180

Not opposed on craft — it is careful work and honestly reported. But it should not be the path everyone walks. If this lands, the picker becomes a convenience for someone hand-writing a bespoke skill, next to a package that already ships the common ones. Worth deciding which lands first, since both touch the same declaration.

Selection narrows a Bot's tools to the ones its matching skills declare,
and a deployment starts with no skills. There was no skills.yaml, nothing
seeded any, and nothing ever wrote one with origin "catalogue" — so on
every fresh clone there was nothing to match against and the narrowing
never switched on. The feature shipped inert.

Left to a screen it stays that way: somebody maps tools to skills by hand,
in every deployment, again after every new connector. That is curation a
product with a services team absorbs and a template cannot. OpenBot is a
template, so the declaration ships with the thing that declares it.

A tenant package may now carry skills.yaml. Slug, title, summary,
instructions, and the serverId/toolName refs the skill needs. Seeded on
boot as deployment skills, so everybody sees them in the / menu and
connecting a connector is the only step left.

The file is optional, so every existing package loads unchanged and ships
none. Declared refs are deliberately not checked against the tools this
deployment has seen, unlike the API path: a package is written before
anybody connects anything, so it necessarily names tools for connectors
that may not exist yet, and refusing would mean a template could only ship
skills for connectors it could guarantee, which is none. An unknown ref
sits inert, because the offer is still intersected with the grants.

A skill somebody wrote here keeps its name. The / namespace is shared and
first to take a name keeps it; a collision is skipped and logged rather
than thrown, because anybody signed in may write a skill and throwing
would let one person stop the deployment booting by choosing a name.

The example package ships four, all Drive: find-a-document, whats-changed,
who-owns-this, check-a-claim.
@davidmckayv
davidmckayv merged commit 146519f into main Aug 22, 2026
8 checks passed
@davidmckayv
davidmckayv deleted the feat/a-package-ships-its-skills branch August 22, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant