--- 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//SKILL.md`, mounted read-only when the extension is enabled | | **commands** | `commands: ["name", …]` | `handle_command(name, arguments)`; registered as `/:` | | **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= [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=` — 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=` — 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=` — 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=` 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.