author-extension · git:20260724.8351b7d · 2026-07-24 · sha256 0923323d1c11a3d7
author-extension git:20260724.8351b7dA
Immutable. This exact content is served forever at /api/v1/blob/0923323d1c11a3d7.
---
name: author-extension
description: Write a new yolop extension end-to-end — scaffold, implement, install, verify, and enable it — when the user asks yolop to add a capability, integrate with something, or "build an extension" for a need that no installed extension covers.
metadata:
internal: true
user-invocable: true
---
# Author an extension
Goal: turn a capability request ("integrate with X", "block Y", "add a tool
that Z") into a working, installed yolop extension — built by yolop itself.
An extension is a capability package served over YEP (the yolop extension
protocol): a `plugin.json` manifest plus a capability server that speaks
newline-delimited JSON-RPC over stdio. See [`knowledge/specs/extensions.md`](../../../knowledge/specs/extensions.md).
## When to use
Use this when the user wants a *new, persistent* capability and no installed
extension provides it — something worth keeping across sessions, not a one-off
shell command. Signals: "make yourself a tool for…", "add an extension that…",
"integrate yolop with…", "from now on, block/allow…".
If an installed extension already covers it, use that instead
(`list_extensions`). If the need is a single throwaway action, just do it — do
not author an extension for it.
## What an extension can contribute
A manifest must contribute at least one facet; declare only what you need.
[`extensions.md`](../../../knowledge/specs/extensions.md) defines each one — this is what to pass the
scaffold and what it generates.
| Facet | Scaffold | Generated seam |
| --- | --- | --- |
| **tools** | `tools=[…]` | `handle_tool(name, args)` returning the result dict |
| **hooks** | `hooks=[…]` | `handle_hook(event, tool_name, args)` — `pre_tool_use` can block, `post_tool_use` observes |
| **prompt** | `prompt=…` | a static system-prompt contribution |
| **mcpServers** | — | declared in the manifest directly |
| **status** | `status: true` | `emit_status(text)`; empty text clears the field. TUI-only, a no-op in `--print`/ACP |
| **skills** | `skills: true` | a starter `skills/<name>/SKILL.md`, mounted read-only when the extension is enabled |
| **commands** | `commands: ["name", …]` | `handle_command(name, arguments)`; registered as `/<ext>:<cmd>` |
| **ui_ask** | — | declare `ui_ask: true` and send `ui/ask` (`{prompt, placeholder?}`) by hand; refused in `--print`/ACP |
Not yet available: providers.
## The loop
1. **Scaffold.** `scaffold_extension name=<name> [description=…] [language=python|typescript|rust]`
with the facets you need — `tools=[…]`, `hooks=[…]`, and/or `prompt=…`.
This writes a package (manifest + server source) with the handler bodies
stubbed. `python` and `typescript` (a dependency-free Node.js server) are
single-file and need no build step; `rust` emits a `serde_json`-only crate.
Pick the one whose toolchain the environment has; Python is the default.
For `rust`, the scaffold result includes a `build` command — run it after
editing to compile the binary into `bin/` before installing. The zero-build
templates skip that.
2. **Implement.** Open the generated server (the tool result prints its path)
and fill in the marked `handle_*` bodies; leave the protocol plumbing alone.
A hook returns `{}` to allow or `{"block": True, "reason": "…"}` to deny —
and it receives *every* subscribed tool call, so gate on `tool_name`/`args`.
Keep stdout for protocol JSON only; log to stderr.
3. **Install.** `install_extension source=<package dir>` — copies the package
into the store and pins it. Installing runs third-party code; since *you*
authored it here, the sharp edge is enabling — see step 5.
4. **Verify.** `doctor_extension name=<name>` — spawns the server, runs the
handshake, and checks the served tools/prompt against the manifest. Fix any
`fail`/`warn` before enabling. If the server won't spawn, check the shebang
and that the command is executable.
5. **Enable (ask first).** Show the user the contribution summary and ask
before `enable_extension name=<name>` — enabling adds it to the harness and
runs its server every session. In the interactive TUI it is **applied to the
running session immediately** (activated on the live agent), so its
tools/prompt/hooks are available on the **next turn** — no restart — and it's
persisted for future sessions. In `--print`/ACP there's no live session to
mutate, so it only takes effect next run.
6. **Iterate live (already-enabled extensions).** Once an extension is enabled
and its server has run this session, `reload_extension name=<name>` restarts
that server in place so edits to its *implementation* take effect
immediately — no yolop restart. This is the fast inner loop for fixing a
handler you authored: edit the server, `reload_extension`, call the tool
again. The manifest is the session's approval boundary, so a *surface*
change (adding a tool, changing a schema) still needs a restart, and reload
can never widen the grant.
## Acceptance check
The loop is proven when yolop, unaided, produces an extension that actually
works — e.g. a `pre_tool_use` hook that blocks any `git` command, or a tool
that returns a computed value — installed, doctor-green, and effective after a
restart. The end-to-end deny path is covered by
`scaffolded_extension_blocks_git_end_to_end` in `src/extensions/mod.rs`.
## Notes
- Names: ascii letters, digits, `-`, `_`. The dir name must equal the manifest
`name`.
- Do not hardcode absolute paths in the manifest — the package is copied on
install. The scaffold resolves the server via the package's `bin/` on PATH.
- Iterate by editing the server and re-running `doctor_extension`; no reinstall
is needed while working from the scaffolded dir, but re-run `install_extension`
to pick up edits into the store.