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.