---
slug: overview
title: 插件开发总览与快速开始
titleEn: Plugin Development Overview
summary: 一页看懂 TestSecScan 的四种插件形态、两套 WASM SDK 的适用边界、签名与上架流程，以及该从哪篇文档开始。
summaryEn: A single page covering the four plugin forms, which WASM SDK applies where, the signing and publishing flow, and where to start.
category: 总览
order: 10
enabled: true
updatedAt: "2026-10-05"
---
# 插件开发总览与快速开始

> 本文面向**第三方开发者**：你只需要 TestSecScan 的官方发行包 + 本文档，就能从零写出一个可被控制器、扫描节点或 AI 渗透代理调用的插件。

[[toc]]

## 一、四种插件形态，先选对赛道

TestSecScan 的"插件"并不是一个东西，而是多套互相独立、各有 SDK 与签名体系的扩展点（下表五种，含 AIAgent 侧的插件与外部工具两类）。**选错赛道的典型症状是：代码能编译，但宿主永远不会调用你。**

| 形态 | 扩展点 | 宿主 | 开发语言 | SDK / 文档 |
|---|---|---|---|---|
| 控制器插件 | 订阅并处理控制器转发的 TCP 指令 | 控制器 | Go / Rust / C / C++ (WASI) | [控制器插件开发](/docs/controller-plugin) |
| WASM POC 模板 | 处理单个流量包，产出漏洞发现 | 扫描节点 | Go / Rust / C / C++ (WASI) | [WASM POC 模板开发](/docs/scan-poc-wasm) |
| 应用插件 | 钩住扫描节点业务点（任务/流量/MITM） | 扫描节点 | Go / Rust / C / C++ (WASI, c-shared) | [应用插件开发](/docs/scan-app-plugin) |
| AIAgent 外部工具 | 作为大模型可调用的一个"工具" | AI 渗透代理 | 任意可执行程序 / WASI wasm | [AiAgent 外部工具开发](/docs/aiagent-tool) |
| AiAgent 插件 | 订阅并处理 AI 渗透代理转发的 TCP 指令 | AI 渗透代理 | Go / Rust / C / C++ (WASI) | [AiAgent 插件开发](/docs/aiagent-plugin) |

另有 **Go 热加载 POC**（见 [Go 热加载 POC 开发](/docs/go-poc-hotload)）：它不是插件，而是**漏洞模板的一种形态**，用纯 Go 源码编写、由扫描节点**在进程内解释执行**（目标机无需安装 Go 工具链），适合"需要一个图灵完备脚本、但不值得走 WASM 构建链"的场景。

## 二、两套 WASM SDK 的边界（最容易踩的坑）

扫描节点为两类 WASM 模块注入了**两套完全不重叠的宿主函数**：

| | WASM POC 模板（`scan_*`） | 应用插件（`app_*`） |
|---|---|---|
| 宿主命名空间 | `scan_http` / `scan_log` / `scan_report` / `scan_call` / `scan_t` / `scan_config` / `scan_set_timeout` | `app_log` / `app_send_tcp*` / `app_call` / `app_spawn_tool` / … |
| 入口 | `_start`（`func main`），一次处理一个流量包 | `应用_` 前缀的**导出函数**，按钩子调用 |
| 构建方式 | 普通 wasip1 命令模块 | **必须 `-buildmode=c-shared`** |
| 生效场景 | **仅 POC 模板列表**（漏洞模板）中 `PocType=wasm` 的条目 | 插件商店安装的扫描节点应用插件 |
| SDK 文件（在下载的 SDK 包里） | `scan.go` | `app.go` |

**关键限制：`scan_*` 这组宿主函数只在"POC 模板执行"的调用路径上注入。**也就是说，把 `scan.HTTP(...)` 写进应用插件、或把 `app.SpawnTool(...)` 写进 WASM POC，都会因为宿主没有导出对应函数而**实例化失败**。判断方法很简单：

- 你的产物在 **POC 模版列表**里作为一条漏洞被扫描 → 用 `scan.*`；
- 你的产物在 **插件商店 / 应用插件目录**里作为常驻插件跑 → 用 `app.*`。

同理，控制器插件用 `ctl_*`，AiAgent 插件用 `aia_*`，AiAgent 外部工具用 stdin/stdout JSON——各套互不通用。

> ★ **跨端复用同一个 id（推荐做法）**：一个插件包可同时提供 gui / controller / scan / aiagent 四份实现，各端在**自己那台机器**上做同一件事（如读写本机 hosts）。控制器按 `info.yaml` 的 `installTargets` 把对应子树分发给各端；权限则在「插件商店 > 设置 > 插件目录沙箱」**按来源端分别授权**（同一路径在不同端是两条独立记录）。官方参考插件 `plugin-hostsdns-0001` 即四端齐备，可作模板。

### 2.1 第三个坑：同一个源码，不同端要不同的 buildmode（★ 编译期看不出来）

这是**对第三方开发者最不友好**的一条：**同一份 Go 源码**装到不同目标端，要用不同的 `-buildmode`，
而且**两种模式都能编译成功**，只有插件被宿主**真正加载**时才炸 —— 报错还很隐晦
（`wasmexport function called before runtime initialization / call _start first`）。

| 目标端 | 宿主驱动方式 | 正确产物入口符号 | 构建方式 |
|---|---|---|---|
| `scan`（应用插件） | 清空自动 `_start`，改调 `_initialize` + 直调 `应用_*` 导出 | `_initialize` | **必须 `-buildmode=c-shared`** |
| `controller`（订阅式） | 实例化后直接调 `_start`（`main` 内读 stdin / 写 stdout） | `_start` | 普通命令模块（**不加** buildmode） |
| `aiagent`（订阅式） | 优先 `_start`，退回 `plugin_handle` | `_start`（或 `plugin_handle`） | 普通命令模块（**不加** buildmode） |

**怎么避免踩坑 —— 三种方式，任选其一（推荐第一种）**：

1. **源码自声明（最省事）**：在插件源码头注释里写一行
   ```
   //tss:buildmode c-shared      // scan 应用插件
   //tss:buildmode command       // controller / aiagent（也可省略，默认就是）
   ```
   插件商店的**构建服务**与本地 `build-plugins.sh` 都会读这行，自动用正确的 buildmode 编译。
   这句话跟着源码走，换个端复制源码时也不会丢。

2. **`info.yaml` 显式声明**：在 `targets.<目标端>` 下加 `buildMode: c-shared`（或 `command`）。
   优先级最高，适合"源码不方便改、但要在包元数据里固定"的场景。

3. **什么都不写**：构建服务按目标端**默认口径**兜底（scan → c-shared，其余 → command）。
   若默认口径与你的源码不符（罕见），构建服务**编译后会核对产物入口符号**，不匹配就报错并提示怎么改，
   且在**未显式声明**时自动换另一种模式重编一次（帮你把坑填上）。

> [!TIP]
> 记不住也没关系：构建服务在**编译完成后**会解析产物的导出符号，对照该端宿主契约校验。
> 传错 buildmode 不再"装上去才炸"，而是**构建阶段就报错**，并告诉你"该加还是该去掉 `-buildmode=c-shared`"。


## 三、最短上手路径

```text
① 选形态（上表）
② 复制对应 SDK 到你的工程（每篇文档都给了 go.mod 写法）
③ 本地编译成 .wasm / 可执行文件
④ 本地签名（控制器密钥 Ed25519）
⑤ 放进宿主加载目录，或提交到插件商店 / POC 商店
⑥ 在 GUI 里看到它、触发它、看日志
```

每一篇分册文档都包含：**目录结构 → 编译命令 → 全部公开函数逐个说明（参数/返回/场景/示例）→ 注意事项 → 完整可运行示例 → 调试方法**。

### 每篇分册怎么读（统一结构）

五篇分册都按同一套结构写，按下面的顺序读不会卡住：

| 章节 | 你会得到什么 |
|---|---|
| 一、它是什么 / 什么时候该写它 | 选型判断，避免写错赛道 |
| **入口在哪：宿主怎么加载并调用你** | 文件放哪 → 怎么签名/校验 → 宿主何时编译/实例化 → **入口函数何时被调用** → 数据怎么进（stdin / 主机函数）、结果怎么出 → 怎么重载 |
| 逐个函数 / 钩子 / 字段精讲 | **每个公开符号一节**：作用 → 完整签名 → **参数表（传什么、从哪来、示例值）** → **返回（字段表 + 怎么取值 + 失败时的表现）** → **可复制代码片段** → 注意事项 |
| 内置函数 / 宿主函数全表 | 底层 `env` 宿主函数与内置工具函数的参数、返回值、ID 对照 |
| 完整示例 | 从最简到进阶，可直接照抄改 |
| **调试手册** | 日志在哪看 → 5 分钟最小验证流程 → 「症状 → 原因 → 解决」表格 |
| 注意事项清单 | 每条都写清"写错会怎样" |

> [!TIP]
> 遇到"这个函数返回什么、我怎么拿到返回值"时，直接跳到该函数的 `####` 小节看**参数表 + 返回 + 代码片段**三件套；遇到"我的插件根本没被调用"时，先看**入口在哪**那一章（九成是没导出/没签名/没声明能力）。

## 四、下载：文档与 SDK 包

每篇文档页**右上角**都有下载按钮，也可以直接用下面的固定地址（随版本更新，始终指向当前生效版本）：

| 要下载的东西 | 地址 |
|---|---|
| 本页 Markdown（单篇） | 文档页右上角「下载 Markdown」，或 `/api/v1/site/devdocs/download/<slug>` |
| 全部文档（打包 .zip，含 README 索引） | `/api/v1/site/devdocs/archive.zip` |
| 控制器插件 SDK（`ctl`：Go/C/C++/Rust） | `/api/v1/site/devdocs/sdk/controller.zip` |
| WASM POC 模板 SDK（`scan`：Go/C/C++/Rust） | `/api/v1/site/devdocs/sdk/wasm-scan.zip` |
| 应用插件 SDK（`app`：Go/C/C++/Rust） | `/api/v1/site/devdocs/sdk/wasm-app.zip` |
| Go 热加载 POC 参考包（`scanapi.go` + 类型定义） | `/api/v1/site/devdocs/sdk/gopoc.zip` |
| 全部 SDK 一次打包 | `/api/v1/site/devdocs/sdk/all.zip` |

每个 SDK 包内都有独立的 `README.md`，写清了**工程怎么接（`go.mod` 的 `replace` 怎么写）、编译命令、注意事项**；
解压后把整个目录拷进你的插件工程即可。

> [!NOTE]
> SDK 包内的源码是**快照**（与本平台各端工程里的 SDK 逐字节一致，由护栏测试锁定）。
> 官方会通过后台发布更新（下载按钮上标注当前版本，如 `v1.0.0`）；升级后重新下载即可拿到最新 SDK，
> 若你已把旧 SDK 拷进工程，请对比后替换。

## 五、签名与上架（安全硬门槛）

TestSecScan 的四类扩展**一律"无签名不加载"**，且验签使用**宿主持有的公钥**，绝不信任包内自带的公钥：

- 控制器 / 扫描节点 WASM：Ed25519 签名，`sig.json` 内 `signStatus` 必须为 `verified` 或 `official`；
- AiAgent 外部工具：对**整包摘要**签名（目录内除 `sig.json` 外全部文件按相对路径升序参与 SHA-256），加载与每次调用前**各验一次**（防 TOCTOU）；
- 签名私钥只有控制器持有；第三方开发者通过 GUI「一键签名」或官方构建服务完成签名。

> [!WARNING]
> 不要试图用自签密钥对绕过校验：宿主只认控制器下发的公钥，自签的包会被判为 `verify_failed`，模型/扫描引擎**看不到**它。这不是"看到了但拒绝执行"，而是根本不会进入注册表。

## 六、文档维护约定（面向后续 SDK 演进）

本套开发文档与 SDK 源码**同步演进**：

1. 新增/修改任何公开 SDK 函数后，必须同步更新对应分册，并在本页"两套 WASM SDK 的边界"等对照表中登记；
2. 仓库内置了 SDK 覆盖护栏测试（`buildservice/devdocs/coverage_test.go`），会解析各端 SDK 源码做两道断言：① 每个公开符号都出现在文档正文里；② 每个公开符号都有**以它命名的 `####` 小节且小节内含代码片段**（即"每个函数都要有怎么调用、怎么取返回值的例子"）。**漏写或只写一句话说明都会导致测试失败**，从而强制同步；
3. 文档正文可在后台「官网设置 → 开发文档」在线修改，修改以覆盖层落盘，内置版本随版本升级更新，可一键"恢复默认"。

<!-- en -->
# Plugin Development Overview and Quick Start

> This article is for **third-party developers**: all you need is the official TestSecScan distribution package plus this documentation to write, from scratch, a plugin that the controller, a scan node, or the AI penetration agent can invoke.

[[toc]]

## 1. Four Plugin Forms: Choose the Right Track First

A TestSecScan "plugin" is not a single thing but four independent extension points, each with its own SDK and signing system. **The classic symptom of choosing the wrong track is: your code compiles, but the host will never call it.**

| Form | Extension point | Host | Development language | SDK / Docs |
|---|---|---|---|---|
| Controller plugin | Subscribes to and handles TCP commands forwarded by the controller | Controller | Go / Rust / C / C++ (WASI) | [Controller Plugin Development](/docs/controller-plugin) |
| WASM POC template | Handles one traffic packet and produces vulnerability findings | Scan node | Go / Rust / C / C++ (WASI) | [WASM POC Template Development](/docs/scan-poc-wasm) |
| Application plugin | Hooks into scan node business points (task / traffic / MITM) | Scan node | Go / Rust / C / C++ (WASI, c-shared) | [Application Plugin Development](/docs/scan-app-plugin) |
| AIAgent external tool | Acts as a "tool" the large model can call | AI penetration agent | Any executable program / WASI wasm | [AiAgent External Tool Development](/docs/aiagent-tool) |

There is also the **Go hot-load POC** (see [Go Hot-Load POC Development](/docs/go-poc-hotload)): it is not a plugin but **one form of vulnerability template**. It is written in plain Go source and **interpreted in-process** by the scan node (the target machine needs no Go toolchain), which suits scenarios where "you need a Turing-complete script, but a full WASM build chain is not worth it".

## 2. The Boundary Between the Two WASM SDKs (the Easiest Trap to Fall Into)

A scan node injects **two completely non-overlapping sets of host functions** for the two kinds of WASM modules:

| | WASM POC template (`scan_*`) | Application plugin (`app_*`) |
|---|---|---|
| Host namespace | `scan_http` / `scan_log` / `scan_report` / `scan_call` / `scan_t` / `scan_config` / `scan_set_timeout` | `app_log` / `app_send_tcp*` / `app_call` / `app_spawn_tool` / … |
| Entry point | `_start` (`func main`), one traffic packet per invocation | **Exported functions** prefixed with `应用_`, called per hook |
| Build mode | An ordinary wasip1 command module | **Requires `-buildmode=c-shared`** |
| Where it takes effect | **Only entries with `PocType=wasm`** in the POC template list (vulnerability templates) | Scan node application plugins installed from the plugin store |
| SDK file (inside the downloaded SDK package) | `scan.go` | `app.go` |

**Key limitation: the `scan_*` host functions are injected only on the "POC template execution" call path.** In other words, putting `scan.HTTP(...)` into an application plugin, or `app.SpawnTool(...)` into a WASM POC, will make instantiation **fail**, because the host does not export the corresponding functions. Telling the two apart is easy:

- Your artifact is scanned as a vulnerability from the **POC template list** → use `scan.*`;
- Your artifact runs as a resident plugin from the **plugin store / application plugin directory** → use `app.*`.

By the same token, controller plugins use `ctl_*` and AiAgent external tools use stdin/stdout JSON — the four sets are not interchangeable.

### 2.1 The Third Trap: the Same Source Needs a Different Build Mode per Endpoint (★ Invisible at Compile Time)

This is the **least developer-friendly** rule: the **same Go source** needs a different `-buildmode`
depending on the target endpoint, and **both modes compile successfully** — it only blows up when the
plugin is **actually loaded** by the host, with an obscure error
(`wasmexport function called before runtime initialization / call _start first`).

| Endpoint | How the host drives it | Correct artifact entry symbol | Build mode |
|---|---|---|---|
| `scan` (application plugin) | Clears the automatic `_start`, calls `_initialize`, then calls the exported `应用_*` hooks | `_initialize` | **Requires `-buildmode=c-shared`** |
| `controller` (subscription) | Instantiates and calls `_start` directly (`main` reads stdin / writes stdout) | `_start` | Ordinary command module (**no** buildmode) |
| `aiagent` (subscription) | Prefers `_start`, falls back to `plugin_handle` | `_start` (or `plugin_handle`) | Ordinary command module (**no** buildmode) |

**How to avoid this — pick any of three, the first is recommended**:

1. **Self-declare in the source (easiest)**: add one line in your source's header comment:
   ```
   //tss:buildmode c-shared      // scan application plugin
   //tss:buildmode command       // controller / aiagent (may also be omitted; it is the default)
   ```
   Both the plugin-store **build service** and the local `build-plugins.sh` read this line and pick
   the correct build mode automatically. The declaration travels with the source, so it survives copying.

2. **Declare it in `info.yaml`**: add `buildMode: c-shared` (or `command`) under `targets.<endpoint>`.
   Highest priority; use it when you would rather not touch the source but want the mode fixed in metadata.

3. **Declare nothing**: the build service falls back to the per-endpoint **default** (scan → c-shared,
   the rest → command). If that default does not match your source (rare), the build service
   **checks the artifact's entry symbol after compiling** and, when the mode is not explicitly declared,
   automatically retries with the other mode — filling the pit for you.

> [!TIP]
> Don't memorize it: after each build the service parses the artifact's export symbols and validates
> them against that endpoint's host contract. A wrong build mode no longer fails at load time — it fails
> **at build time**, telling you whether to add or drop `-buildmode=c-shared`.

## 3. The Shortest Path to Getting Started

```text
① Choose the form (table above)
② Copy the matching SDK into your project (every article shows how to wire up go.mod)
③ Compile locally into a .wasm file / executable
④ Sign locally (controller key, Ed25519)
⑤ Put it into the host's load directory, or submit it to the plugin store / POC store
⑥ See it in the GUI, trigger it, read the logs
```

Every chapter article covers: **directory layout → build commands → every public function explained one by one (parameters / returns / scenarios / examples) → caveats → complete runnable examples → debugging methods**.

### How to Read Each Chapter Article (Uniform Structure)

All five chapter articles follow the same structure; reading them in the order below will keep you unstuck:

| Section | What you get |
|---|---|
| 1. What it is / when you should write it | Choosing the right form; avoiding the wrong track |
| **Where the entry point is: how the host loads and calls you** | Where to put the file → how to sign / verify → when the host compiles / instantiates → **when the entry function is called** → how data goes in (stdin / host functions) and how results come out → how to reload |
| Function / hook / field walkthroughs | **One section per public symbol**: purpose → full signature → **parameter table (what to pass, where it comes from, example values)** → **returns (field table + how to read the value + behavior on failure)** → **copy-ready code snippet** → caveats |
| Built-in / host function reference | Parameters, return values and ID cross-reference for the low-level `env` host functions and built-in tool functions |
| Complete examples | From minimal to advanced, ready to copy and adapt |
| **Debugging handbook** | Where to find logs → a 5-minute minimal verification flow → a "symptom → cause → fix" table |
| Caveats checklist | Every entry spells out "what happens if you get it wrong" |

> [!TIP]
> When you wonder "what does this function return, and how do I get the return value", jump straight to that function's `####` section and read the three-part set: **parameter table + returns + code snippet**. When "my plugin is never called at all", read the **Where the entry point is** chapter first (nine times out of ten, you forgot to export it / sign it / declare the capability).

## 4. Downloads: Documentation and SDK Packages

Every documentation page has download buttons in the **top-right corner**, and you can also use the fixed addresses below (they follow releases and always point to the version currently in effect):

| What to download | Address |
|---|---|
| This page as Markdown (single article) | "Download Markdown" in the page's top-right corner, or `/api/v1/site/devdocs/download/<slug>` |
| All documentation (packed .zip, with a README index) | `/api/v1/site/devdocs/archive.zip` |
| Controller plugin SDK (`ctl`: Go/C/C++/Rust) | `/api/v1/site/devdocs/sdk/controller.zip` |
| WASM POC template SDK (`scan`: Go/C/C++/Rust) | `/api/v1/site/devdocs/sdk/wasm-scan.zip` |
| Application plugin SDK (`app`: Go/C/C++/Rust) | `/api/v1/site/devdocs/sdk/wasm-app.zip` |
| Go hot-load POC reference package (`scanapi.go` + type definitions) | `/api/v1/site/devdocs/sdk/gopoc.zip` |
| All SDKs in a single package | `/api/v1/site/devdocs/sdk/all.zip` |

Every SDK package contains its own `README.md` explaining **how to wire it into your project (how to write the `replace` directive in `go.mod`), the build commands, and the caveats**; after unzipping, copy the whole directory into your plugin project.

> [!NOTE]
> The source code inside each SDK package is a **snapshot** (byte-for-byte identical to the SDKs in this platform's per-end projects, locked down by guardrail tests).
> Official updates are published through the backend (the download button shows the current version, e.g. `v1.0.0`); download again after an upgrade to get the latest SDK,
> and if you have already copied an older SDK into your project, compare and replace it.

## 5. Signing and Publishing (a Hard Security Gate)

All four kinds of TestSecScan extensions follow the same rule: **no signature, no loading** — and signature verification uses **the public key held by the host**, never a public key bundled inside the package:

- Controller / scan node WASM: Ed25519 signature; inside `sig.json`, `signStatus` must be `verified` or `official`;
- AiAgent external tool: the signature covers the **whole-package digest** (every file in the directory except `sig.json`, hashed with SHA-256 in ascending relative-path order), and verification runs **once at load time and once before every invocation** (TOCTOU protection);
- The signing private key is held only by the controller; third-party developers sign through one-click signing in the GUI or through the official build service.

> [!WARNING]
> Do not try to bypass verification with a self-signed key pair: the host only trusts the public key delivered by the controller, so a self-signed package is judged `verify_failed` and the model / scan engine **cannot see** it. This is not "it was seen but execution was refused" — it never enters the registry at all.

## 6. Documentation Maintenance Conventions (for Future SDK Evolution)

This documentation set and the SDK source code **evolve together**:

1. After adding or modifying any public SDK function, you must update the corresponding chapter article and register it in the comparison tables on this page, such as "The Boundary Between the Two WASM SDKs";
2. The repository ships an SDK coverage guardrail test that parses the SDK source of each end and makes two assertions: ① every public symbol appears in the documentation body; ② every public symbol has a `####` section named after it, and that section contains a code snippet (in other words, "every function needs an example of how to call it and how to read its return value"). **Omitting it, or writing only a one-line description, fails the test**, which forces the documentation to stay in sync;
3. The documentation body can be edited online in the backend under "Site Settings → Developer Docs"; edits are persisted as an override layer, while the built-in version is refreshed with each release and can be restored to the default with one click.
