Skip to content
Back to skills

Implement Plugin

ASecurity

Scaffold, implement, and test Starlark plugins. From protocol/event/phase selection to running sample tests

  • 16 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
securitypythongobashapi

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned September 22, 2026

npx -y skills add usk6666/yorishiro-proxy --skill implement-plugin --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Implement Plugin?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Implement Plugin
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/usk6666-implement-plugin/badge)](https://www.skillsdirectory.com/skills/usk6666-implement-plugin)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: "Scaffold, implement, and test Starlark plugins. From protocol/event/phase selection to running sample tests"
user-invokable: true
---

# /implement-plugin

A workflow skill for creating Starlark plugins on the RFC-001 §9.3 plugin engine (`internal/pluginv2/`). Interactively select protocol/event/phase to generate a working scaffold.

## Arguments

- `/implement-plugin` — Create a plugin in interactive mode
- `/implement-plugin <description>` — Auto-generate a plugin from a description (e.g., `/implement-plugin Add X-Request-ID header to HTTP requests`)

## Steps

### Phase 1: Requirements Clarification

If no description argument is given, interactively confirm:

1. **Purpose**: What should the plugin do?
2. **Protocol**: Target plugin protocol — one of `http`, `ws`, `grpc`, `grpc-web`, `sse`, `raw`, `tls`, `connection`, `socks5` (the plugin protocol vocabulary; see `internal/pluginv2/surface.go`).
3. **Event**: Hook event under that protocol (e.g. `on_request`, `on_message`, `on_chunk`).
4. **Phase**: `pre_pipeline` (default — before Intercept/Transform) or `post_pipeline` (after; right place for signing/last-mile mutation). Lifecycle / observation events (`on_close`, `on_handshake`, `on_disconnect`, `socks5.on_connect`, etc.) take **no `phase=` argument** — passing one is a load-time error.
5. **Action**: CONTINUE (mutate in place) / DROP (transaction-start only) / RESPOND (transaction-start request, plus `http.on_response` for replacement).

If a description argument is given, auto-determine from its content.

### Phase 2: Context Collection

Read the following to understand the plugin API:

1. `docs/rfc/plugin-migration.md` — Direct migration table from legacy hook names to `(protocol, event, phase)` and the rationale.
2. `internal/pluginv2/surface.go` — The 17-entry hook surface table. Authoritative for which (protocol, event) pairs exist and which actions each accepts.
3. `internal/pluginv2/convert.go` — The Starlark `msg` dict shape per protocol (snake_case field names, `*HeadersValue` ordered list, `msg["raw"]` first-class byte injection).
4. `internal/pluginv2/lifecycle.go` — Dict shapes for lifecycle events (`connection.on_connect`, `socks5.on_connect`, `tls.on_handshake`, `ws.on_close`).
5. `internal/pluginv2/respond_action.go` — Typed `action.RESPOND(...)` / `action.RESPOND_GRPC(...)` builtin signatures.
6. `examples/plugins/` — Existing sample plugins for each major protocol.

### Phase 3: Plugin Generation

Create a plugin file in `examples/plugins/`:

- Filename: `<snake_case_name>.star`
- Include a comment block at the top describing purpose, the `register_hook` call(s), and a config snippet
- Hook registration is **script-driven** via `register_hook(protocol, event, fn[, phase=...])` — there is no `protocol`/`hooks` config key
- Hook signature is `def fn(msg, ctx):` — two positional arguments
- Returning `None` means CONTINUE; `action.DROP` drops; `action.RESPOND(...)` short-circuits with a synthetic response
- Mutate `msg` in place — `msg["headers"].append(name, value)`, `msg["body"] = b"..."`, `msg["raw"] = b"..."`. Mutations to the dict and to `*HeadersValue` are committed regardless of the return value.

#### Template Structure

```python
# <Plugin Name>
#
# Purpose: <description of purpose>
#
# Hook identity (RFC-001 §9.3):
#   register_hook("<protocol>", "<event>", fn[, phase="post_pipeline"])
#
# Config:
#   { "path": "examples/plugins/<name>.star", "on_error": "skip" }

def <hook_name>(msg, ctx):
    # Read fields: msg["method"], msg["headers"].get_first("X"), ...
    # Mutate fields: msg["headers"].append("X-Foo", "bar"), msg["body"] = b"..."
    return None  # CONTINUE

register_hook("<protocol>", "<event>", <hook_name>)
```

#### Protocol → `msg` dict shape (RFC-001 §9.3 + USK-669 snake_case)

| Plugin protocol | Events | Key fields on `msg` |
|-----------------|--------|---------------------|
| `http` | `on_request`, `on_response` | `method`, `scheme`, `authority`, `path`, `raw_query`, `status`, `status_reason`, `headers`, `trailers`, `body`, `anomalies` (RO), `raw` |
| `ws` | `on_upgrade`, `on_message`, `on_close` | `opcode`, `fin`, `masked`, `mask`, `payload`, `close_code`, `close_reason`, `compressed`, `raw` |
| `grpc` / `grpc-web` | `on_start`, `on_data`, `on_end` | start: `service`, `method`, `metadata`, `timeout`, `content_type`, `encoding`, `accept_encoding`; data: `service` (RO), `method` (RO), `compressed`, `wire_length`, `payload`, `end_stream`; end: `status`, `message`, `status_details`, `trailers` |
| `sse` | `on_event` | `event`, `data`, `id`, `retry`, `anomalies` (RO), `raw` |
| `raw` | `on_chunk` | `bytes`, `raw` |
| `tls` | `on_handshake` (lifecycle, no phase) | `side`, `sni`, `alpn`, `version_name`, `cipher_name`, `peer_cert_subject`, `client_fingerprint` (frozen) |
| `connection` | `on_connect`, `on_disconnect` (lifecycle, no phase) | connect: `conn_id`, `client_addr`, `listener_name`; disconnect: `conn_id`, `client_addr`, `duration_ms` (frozen) |
| `socks5` | `on_connect` (lifecycle, no phase) | `conn_id`, `client_addr`, `target_addr` (frozen) |

`headers` / `trailers` / `metadata` are `*HeadersValue` instances, not dicts. Methods: `append(name, value)`, `delete_first(name) -> bool`, `replace_at(index, name, value)`, `get_first(name) -> str|None`. Iteration yields `(name, value)` 2-tuples; `name in headers` is case-insensitive. Wire order, casing, and duplicates are preserved.

#### `ctx` (second positional argument)

- `ctx.client_addr` — client IP string (port stripped) or `None`
- `ctx.tls` — frozen dict of `{sni, alpn, version_name, cipher_name, peer_cert_subject, client_fingerprint}` or `None`
- `ctx.transaction_state` / `ctx.stream_state` — mutable scoped dicts: `.get(k)`, `.set(k, v)`, `.delete(k)`, `.keys()`, `.clear()`. Lifetime is owned by the Layer (transaction = (ConnID, FlowID) for HTTP / (ConnID, StreamID) for streaming protocols; stream = (ConnID, StreamID) always).

#### Action constraints (RFC-001 §9.3, surface.go)

| Action | Allowed events |
|--------|----------------|
| CONTINUE | All events (always implicit) |
| DROP | Transaction-start only: `http.on_request`, `ws.on_upgrade`, `grpc.on_start`, `grpc-web.on_start`, `connection.on_connect`, `socks5.on_connect` |
| RESPOND | Transaction-start request events (as above, minus `connection`/`socks5`), plus `http.on_response` (replace upstream response). gRPC uses `action.RESPOND_GRPC(status, message, trailers)` instead of `action.RESPOND(...)`. |

Mid-stream events (`on_data`, `on_message`, `on_event`, `on_chunk`) and lifecycle/observation events (`on_close`, `on_end`, `on_handshake`, `on_disconnect`) accept CONTINUE only — terminating a stateful stream uses native termination (gRPC `RST_STREAM`, WS close frame), not an action enum.

### Phase 4: Verification

1. Visually verify Starlark syntax (compliance with `go.starlark.net` syntax rules)
2. Build and run the engine load tests:

```bash
make build
make test
```

The engine's `LoadPlugins` validates `register_hook` calls at load time — unknown protocol/event, invalid phase, or non-callable `fn` are reported as `LoadError` with `Kind` = `LoadErrUnknownProtocol` / `LoadErrUnknownEvent` / `LoadErrInvalidPhase` / `LoadErrPhaseNotSupported` / `LoadErrNotCallable`.

### Phase 5: Present Configuration Example

Present the `PluginConfig` for the generated plugin (RFC-001 shape — no `protocol`/`hooks` keys at the config level):

```json
{
  "path": "examples/plugins/<name>.star",
  "vars": { "<key>": "<value>" },
  "on_error": "skip",
  "max_steps": 1000000,
  "redact_keys": []
}
```

`vars` is exposed as a frozen Starlark `config` dict; `redact_keys` hides values from `plugin_introspect`. Plugins are loaded once at proxy boot; to change the loaded set, edit the config and restart the proxy (RFC §9.3 D2).

For runtime introspection, use the `plugin_introspect` MCP tool — read-only listing of loaded plugins and their `(protocol, event, phase)` registrations:

```json
// MCP tool: plugin_introspect
{}
```

The legacy `plugin` MCP tool (`list`/`reload`/`enable`/`disable`) was removed in USK-695; there is no replacement for `reload`/`enable`/`disable` by design.

## Notes

- **gRPC mid-stream events accept CONTINUE only.** DROP/RESPOND on `grpc.on_data` / `grpc.on_end` is a load-time/dispatch-time error. `grpc.on_start` accepts DROP and `action.RESPOND_GRPC(...)`.
- **WebSocket mid-stream events accept CONTINUE only.** To redact a `ws.on_message` payload, mutate `msg["payload"]` in place rather than dropping. To refuse a connection, hook `ws.on_upgrade` and return DROP.
- **`(socks5, on_connect)` accepts DROP** for connection-level allowlists, but **no `phase=` argument** — it is a lifecycle event.
- **WebSocket Ping/Pong frames are still delivered** as `(ws, on_message)` events; filter by `msg["opcode"]` (0x9 = ping, 0xA = pong, 0x1 = text, 0x2 = binary) when only certain frame types matter.
- **`msg["raw"] = b"..."` writes verbatim wire bytes** (RFC §9.3 D4 "raw wins") — when both raw and message-side fields are mutated, raw is shipped and message-side mutations are recorded only in the variant snapshot.
- **Body cap**: `msg["body"]` is capped at 1 MiB; oversize bodies are surfaced as `ErrBodyTooLarge` and the plugin is skipped (the body is never truncated for the plugin).
- **Starlark is a subset of Python**: `import`, `class`, file I/O, and network access are not available. `print()` writes to the proxy log.
- **Module-level variables are frozen after load** — mutable objects (lists, dicts) at module level cannot be modified inside hooks. Use immutables (strings, ints, tuples) at module level; mutate via `msg` / `ctx.transaction_state` / `ctx.stream_state` / `state.*` instead.
- **`config` is a frozen dict** of `vars` from the plugin config. Use `config["key"]` (or membership-test first with `"key" in config`) — there is no schema enforcement beyond the engine's primitive type whitelist.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.