---
slug: aiagent-plugin
title: AIAgent 插件（WASM）开发
titleEn: AiAgent WASM Plugin Development
summary: 编写跑在 AI 渗透代理上的常驻 WASM 插件：订阅 TCP 指令、aia_* 主机函数（含系统文件读写与沙箱授权）、pack 内放什么。
summaryEn: 'Write a resident WASM plugin on the AI agent: subscribe to TCP messages, use aia_* host functions (including sandboxed file access), and what goes into the package.'
category: AI 渗透代理
order: 35
enabled: true
updatedAt: "2026-10-13"
---
# AIAgent 插件（WASM）开发

[[toc]]

> 本文面向**第三方开发者**：如果你要写一个跑在 **AI 渗透代理（AiAgent）** 上、订阅并处理 TCP 指令的常驻 WASM 插件，看本篇。运行模型与[控制器插件](/docs/controller-plugin)高度同构（订阅 + stdin/stdout 行协议），**写法可直接迁移**。

## 一、它是什么，什么时候该写 AiAgent 插件

AiAgent 插件与「AiAgent 外部工具」不是一回事：

| | AiAgent **插件**（本篇） | AiAgent **外部工具**（[另见](/docs/aiagent-tool)） |
|---|---|---|
| 宿主角色 | 常驻**业务插件**，被动接收 TCP 指令 | 大模型可**主动调用**的一个工具 |
| 加载方式 | 插件商店分发到 `AiConfig/plugins/<uuid>/`，宿主加载 | `AiConfig/externaltools/<toolDir>/`，模型按需调用 |
| 入口 | `main()`（`_start`）读 stdin 写 stdout | WASI 命令模块，参数 JSON 进 stdin、结果出 stdout |
| 适用 | 需要常驻、订阅平台事件、读写本机资源 | 给模型增加一个"函数" |

**什么时候该写 AiAgent 插件**：功能需要在 **AiAgent 所在的那台机器**上做某件事（例如读写该机器的 hosts），且希望它以平台事件（TCP 指令）为触发。若只是想让模型多一个可调用工具，用外部工具。

> 同一个插件 id 可以在四个端（gui / controller / scan / aiagent）各有一份独立实现 —— 每一端都作用于**自己所在的那台机器**。平台官方参考插件做法见下节。

## 二、四个端的关系（同一 id，四份实现）

一个完整的插件包可以同时提供四个端的实现：

```
Plugin/
  gui/<uuid>/         页面插件（iframe + tss.*，见 GUI 插件文档）
  Controller/<uuid>/  控制器 WASM 插件（ctl_* 主机函数）
  scan/<uuid>/        扫描节点 WASM 应用插件（app_* 主机函数）
  aiagent/<uuid>/     本篇：AiAgent WASM 插件（aia_* 主机函数）
```

`info.yaml` 的 `installTargets` 列出要安装的端（`gui` / `controller` / `scan` / `aiagent`）；控制器按端把对应子树分发给各节点。

**为什么要分端**：各端在**不同机器**上，能访问的资源不同。插件目录沙箱正是按来源端（Host）分条授权 —— 同一个路径在 gui 与 aiagent 是两条独立记录。

## 三、包结构

AiAgent 端子树的标准结构（`Plugin/aiagent/<uuid>/`）：

```
Plugin/aiagent/<uuid>/
  build/aiagent.wasm   构建产物（宿主优先取它；也接受 build/ 下任意 .wasm）
  src/                 源码（main.go + 运行时 core，见第五节）
  info.yaml            插件元数据（在 Controller/<uuid>/ 下，全包一份）
```

宿主加载规则：扫描 `AiConfig/plugins/<uuid>/build/`，**优先 `aiagent.wasm`**，否则取该目录下任意 `.wasm`。

## 四、运行模型

宿主对每条 TCP 消息执行一次：

```
宿主把消息 JSON 写入 stdin
      ↓
实例化模块 → 调用 _start（即 main()）
      ↓
插件读 stdin → 处理 → 写 stdout（[INIT] / [RESP] 行协议）
```

- **`_start` 入口**：与控制器插件一致（不是 `-buildmode=c-shared` 的库模式）。
- **`[INIT]` 行**：插件声明元信息（名称/版本/订阅/能力）。宿主机每次执行都会解析刷新。
- **`[RESP]` 行**：`{"handled":bool,"error":""}`。`handled=true` 表示插件已消费该消息。

订阅：插件在 `init` 里声明"我关心哪些消息"（按 Command1/Command2/CommandA 匹配，空=通配），宿主只投递命中的消息。

## 五、最小工程

写两个文件（自包含，仅标准库；也可用官方 SDK 包）：

**`aiacore.go`**：运行时骨架（`SetInfo` / `Subscribe` / `Capability` / `Handle` + `aia_*` 声明 + `main`）。可直接从随包的 SDK 包 `aiagent-plugin/aiacore.go` 复制。

**`main.go`**：你的业务逻辑：

```go
//go:build ignore

package main

import "encoding/json"

func init() {
    SetInfo("我的 AiAgent 插件", "1.0.0")
    Subscribe("", "", "MyCommand")   // 订阅 CommandA == MyCommand
    Capability("tcp.handle")

    Handle(func(msg Msg) bool {
        if msg.CommandA != "MyCommand" {
            return false
        }
        // ... 你的逻辑 ...
        out, _ := json.Marshal(map[string]any{"ok": true})
        Log("handled: " + string(out))
        return true   // 已消费
    })
}
```

`//go:build ignore` 的作用：让插件源码**不被主工程 `go build ./...` 当自身代码编译**。构建服务在编译前会自动剥离该行。

## 六、构建

```bash
GOWORK=off GOOS=wasip1 GOARCH=wasm go build -o build/aiagent.wasm .
```

> **两个必踩的坑**
> 1. `GOWORK=off` —— 否则工作区（go.work）会把插件源码纳入主工程模块解析。
> 2. **不要**加 `-buildmode=c-shared` —— AiAgent 宿主以 `_start` 驱动（实例化时自动跑 `main`）。
>    用了 `c-shared` 会变成"库"模式（导出 `_initialize` + 函数、没有 `_start`），宿主加载不到。
>    （对照：扫描节点**应用插件**恰恰相反，**必须** `-buildmode=c-shared`。）

> [!TIP]
> **让构建服务/脚本自动选对模式**：在源码头注释写一行 `//tss:buildmode command`，
> 插件商店构建服务与本地 `build-plugins.sh` 都会读它并按普通命令模块编译。
> 这是"同一份源码换端要换构建模式"的坑：两种模式都能编过、只有真加载才炸；
> 构建服务会在**编译后**解析产物导出符号（本端要求 `_start` 或 `plugin_handle`），
> 不匹配即报错，把坑提前到构建期。

## 七、主机 API（`aia_*`）

宿主在 `env` 命名空间导出以下函数（插件用 `//go:wasmimport env <name>` 静态导入；导出集合变化时宿主会递增 `HostApiVersion`，插件可在 `info.yaml` 用 `minAgentHostApi` 声明最低要求）。

#### `aia_log`

**作用**：输出一条日志（进 AiAgent 日志，便于排查）。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_log
func aiaLog(msg string)
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `msg` | string | 日志文本 | `"hosts 已读取：12 行"` |

**返回 / 响应**：无。

**代码片段**：

```go
Log("hosts 已读取：12 行")
```

**注意事项**：日志走 AiAgent 的系统日志与「开发者模式」实时推送；不要在热路径里打大量日志。

#### `aia_read_file`

**作用**：读**AiAgent 所在机器**上的文件（走插件目录沙箱授权）。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_read_file
func aiaReadFile(path string, out unsafe.Pointer, outCap uint32) uint32
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `path` | string | 逻辑键或绝对路径 | `"system.hosts"` |
| `out` / `outCap` | unsafe.Pointer / uint32 | 输出缓冲 | — |

逻辑键（推荐，跨平台由宿主解析）：`system.hosts`、`system.resolv`、`system.tmp`、`user.home`。相对路径 = 插件自身目录内文件（无需外部授权）。

**返回 / 响应**：写入 out 的是 JSON `{"ok":bool,"data":<base64>,"size":n,"error":"..."}`（内容 base64 编码）。

**代码片段**：

```go
raw := ReadFile("system.hosts")
var r struct {
    OK    bool   `json:"ok"`
    Data  string `json:"data"`
    Error string `json:"error"`
}
if json.Unmarshal([]byte(raw), &r) == nil && r.OK {
    content, _ := base64.StdEncoding.DecodeString(r.Data)
    _ = content
}
```

**注意事项**：

1. **授权**：走「插件商店 > 设置 > 插件目录沙箱」（来源端 = `aiagent`）。**登记 ≠ 放行** —— 首次读取未登记路径会**自动登记**（权限位默认全关）并返回错误，管理员开启后才真正可读。
2. 相对路径直接读插件自身目录（沙箱内），无需授权。
3. 单文件上限 4MB。

#### `aia_write_file`

**作用**：写**AiAgent 所在机器**上的文件（整文覆盖，走同一套沙箱授权）。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_write_file
func aiaWriteFile(path string, data unsafe.Pointer, dataLen uint32, out unsafe.Pointer, outCap uint32) uint32
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `path` | string | 逻辑键或绝对路径 | `"system.hosts"` |
| `data` | unsafe.Pointer | 待写字节指针 | `unsafe.Pointer(&b[0])` |
| `dataLen` | uint32 | 字节数 | `uint32(len(b))` |
| `out` / `outCap` | unsafe.Pointer / uint32 | 输出缓冲 | — |

**返回 / 响应**：JSON `{"ok":bool,"size":n,"error":"..."}`。

**代码片段**：

```go
b := []byte("127.0.0.1 local.test\n")
var obuf [4096]byte
n := WriteFile("system.hosts", b)
_ = n
```

**注意事项**：

1. 写权限与读权限**各自独立**；**拒绝时一个字节都不写**。
2. 相对路径写插件自身目录（沙箱内），无需授权。父目录自动创建。

#### `aia_register_fs_entry`

**作用**：运行时把一条外部路径登记进插件目录沙箱（单向申请，登记 ≠ 放行）。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_register_fs_entry
func aiaRegisterFsEntry(reqJSON string, out unsafe.Pointer, outCap uint32) uint32
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `reqJSON` | string | 登记请求 JSON（`{name,kind,path,reason}`） | `{"name":"系统 hosts","kind":"file","path":"/etc/hosts"}` |
| `out` / `outCap` | unsafe.Pointer / uint32 | 输出缓冲 | — |

**返回 / 响应**：JSON `{"ok":bool,"id":"","error":""}`（`ok=true` 只表示"已登记"）。

**代码片段**：

```go
RegisterFsEntry(FsEntry{Name: "系统 hosts 文件", Kind: "file",
    Path: "/etc/hosts", Reason: "读取本机 hosts"})
```

**注意事项**：

1. 幂等；不会覆盖管理员已调的权限位。
2. 危险路径（凭据/私钥/系统根）登记阶段即拒。
3. 建议在 `init` 里登记一次，让管理员提前在界面看到这条申请。

#### `aia_set_timeout`

**作用**：请求本插件单次执行的超时（毫秒）。AiAgent 侧当前按宿主默认超时执行，本函数为能力占位/意图声明。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_set_timeout
func aiaSetTimeout(ms uint32)
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `ms` | uint32 | 毫秒 | `120000` |

**返回 / 响应**：无。

**代码片段**：

```go
SetTimeout(120000)
```

**注意事项**：宿主对单次执行有硬上限，超时会终止本次调用。

## 八、系统文件访问与沙箱（重要）

AiAgent 插件读写**自身目录以外**的文件，必须经过「插件目录沙箱」授权，与其余各端**同一套模型**：

1. **登记**：插件在 `init` 里 `RegisterFsEntry`（或直接调用 `ReadFile`/`WriteFile` 触发自动登记）；
2. **授权**：管理员在「插件商店 > 设置 > 插件目录沙箱」按**来源端 `aiagent`** 逐条开启权限位（读/写/创建/执行）；**登记 ≠ 放行**，未开启即拒绝；
3. **放行/拒绝**：宿主是唯一出口，越界访问一律拒绝并写审计日志；危险路径（凭据、私钥、文件系统根、宿主配置目录）**登记即拒、授权也不放行**。

判定按「来源端 + 插件 id + 路径（归一化后）+ 权限位」，与 GUI / 控制器 / 扫描节点口径一致。**同一路径在不同端是两条独立授权**。

相对路径（无前导 `/` 与盘符）表示**插件自身目录内**的文件，不受上述约束（沙箱内直接读写）。

#### `aia_dns_get`

**作用**：读**AiAgent 所在机器**的各网卡 DNS。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_dns_get
func aiaDnsGet(out unsafe.Pointer, outCap uint32) uint32
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `out` / `outCap` | unsafe.Pointer / uint32 | 输出缓冲 | — |

**返回 / 响应**：JSON `{"ok":bool,"dnsAdapters":[{"Name","Index","Dns":[]}],"error":""}`。

**代码片段**：

```go
raw := DnsGet()
var r struct {
    OK          bool         `json:"ok"`
    DnsAdapters []DnsAdapter `json:"dnsAdapters"`
    Error       string       `json:"error"`
}
_ = json.Unmarshal([]byte(raw), &r)
```

**注意事项**：

1. **为什么不能只靠 `aia_read_file`**：DNS 在 **Windows 是注册表**、**macOS 走 `scutil`** —— 都不是文件；跨平台差异留在宿主。
2. **授权**：走「插件商店 > 设置 > 插件目录沙箱」，条目键为逻辑键 `system.dns`（`kind=capability`），需要 **aiagent 端的读权限**；未登记会**自动登记 + 拒绝**。

#### `aia_dns_set`

**作用**：设置 **AiAgent 所在机器**的 DNS（按网卡定向；`Dns: []` = 还原自动获取）。

**签名 / 触发形式**：

```go
//go:wasmimport env aia_dns_set
func aiaDnsSet(dataJSON string, out unsafe.Pointer, outCap uint32) uint32
```

**参数表**：

| 参数 | 类型 | 传什么 | 示例值 |
|---|---|---|---|
| `dataJSON` | string | 网卡列表 JSON | `[{"Name":"eth0","Dns":["8.8.8.8"]}]` |
| `out` / `outCap` | unsafe.Pointer / uint32 | 输出缓冲 | — |

**返回 / 响应**：`{"ok":bool,"error":"..."}`。

**代码片段**：

```go
wraw := DnsSet([]DnsAdapter{{Name: "eth0", Dns: []string{"223.5.5.5"}}})
```

**注意事项**：需 **aiagent 端的写权限**（读、写独立）；单个网卡失败不中断其余网卡。


## 九、调试

- 插件日志：走 `aia_log`（`Log()`），在 AiAgent 日志与「开发者模式」实时推送里看；
- 加载失败排查：看 AiAgent 启动日志的"插件加载完成：已加载 N 个（跳过 M 个）"；被跳过通常是 `build/` 下没有 `.wasm`；
- 执行失败：宿主会记一条"插件处理消息失败（已跳过该插件）"，并**不影响其它插件与主流程**（单插件隔离）。

<!-- en -->
# AiAgent WASM Plugin Development

[[toc]]

> This document is for **third-party developers** writing a resident WASM plugin that runs on the **AI agent (AiAgent)** and handles TCP commands. Its runtime model is nearly identical to the [controller plugin](/docs/controller-plugin) (subscription + stdin/stdout line protocol) — code migrates directly.

## 1. What it is, and when to write one

An AiAgent plugin is **not** an "AiAgent external tool":

| | AiAgent **plugin** (this doc) | AiAgent **external tool** ([see](/docs/aiagent-tool)) |
|---|---|---|
| Host role | A resident business plugin receiving TCP commands | A tool the LLM can call on demand |
| Loading | Distributed to `AiConfig/plugins/<uuid>/` and loaded by the host | `AiConfig/externaltools/<toolDir>/`, called by the model |
| Entry | `main()` (`_start`), reads stdin, writes stdout | WASI command module: args JSON in, result out |
| Good for | Resident behavior, subscribing to platform events, local resources | Adding a "function" for the model |

Write one when the behavior must happen **on the machine where the AiAgent runs** (e.g. reading/writing that machine's hosts), triggered by platform events. If you only need another callable tool, use an external tool.

## 2. Relationship of the four endpoints

```
Plugin/
  gui/<uuid>/         page plugin (iframe + tss.*)
  Controller/<uuid>/  controller WASM plugin (ctl_*)
  scan/<uuid>/        scan-node WASM app plugin (app_*)
  aiagent/<uuid>/     this doc (aia_*)
```

`installTargets` in `info.yaml` lists the endpoints to install (`gui`, `controller`, `scan`, `aiagent`). Each endpoint acts on **its own machine**, which is why the plugin directory sandbox grants permissions **per source endpoint**: the same path on gui and aiagent are two independent records.

## 3. Package layout

```
Plugin/aiagent/<uuid>/
  build/aiagent.wasm   build output (preferred; any .wasm under build/ is also accepted)
  src/                 sources
  info.yaml            metadata (one per package, under Controller/<uuid>/)
```

## 4. Runtime model

For every TCP message the host: writes the message JSON to stdin, instantiates the module and calls `_start` (`main()`); the plugin reads stdin and writes stdout lines (`[INIT]` / `[RESP]`). `handled=true` in `[RESP]` means the plugin consumed the message. Subscriptions are declared in `init` (matched on Command1/Command2/CommandA; empty = wildcard).

## 5. Minimal project

`aiacore.go` holds the runtime skeleton (`SetInfo` / `Subscribe` / `Capability` / `Handle`, the `aia_*` imports and `main`); `main.go` holds your logic:

```go
//go:build ignore
package main

func init() {
    SetInfo("My AiAgent plugin", "1.0.0")
    Subscribe("", "", "MyCommand")
    Capability("tcp.handle")
    Handle(func(msg Msg) bool {
        if msg.CommandA != "MyCommand" { return false }
        return true
    })
}
```

## 6. Build

```bash
GOWORK=off GOOS=wasip1 GOARCH=wasm go build -o build/aiagent.wasm .
```

> Two pitfalls: use `GOWORK=off`; and do **not** pass `-buildmode=c-shared` (the AiAgent host drives `_start`). Note that scan-node **app plugins** are the opposite — they **require** `c-shared`.

> [!TIP]
> **Let the build service/script pick the right mode for you**: add `//tss:buildmode command` to
> your source's header comment; both the plugin-store build service and the local `build-plugins.sh`
> read it and build an ordinary command module. This is the "same source, different build mode per
> endpoint" trap: both modes compile, but only the right one loads. The build service **parses the
> artifact's export symbols after compiling** (this endpoint requires `_start` or `plugin_handle`)
> and fails the build on a mismatch, moving the trap to build time.

## 7. Host API (`aia_*`)

Exported by the host in the `env` namespace: `aia_log`, `aia_read_file`, `aia_write_file`, `aia_register_fs_entry`, `aia_set_timeout`. The host bumps `HostApiVersion` when the export set changes; declare a minimum with `minAgentHostApi` in `info.yaml`.

#### `aia_read_file`

Reads a file **on the AiAgent machine** (sandbox-gated). Returns JSON `{"ok":bool,"data":<base64>,"size":n,"error":"..."}`. Logical keys: `system.hosts`, `system.resolv`, `system.tmp`, `user.home`. Relative paths mean files inside the plugin's own directory.

#### `aia_write_file`

Writes a file on the AiAgent machine (full overwrite, same gating). Write and read bits are independent; nothing is written when denied.

#### `aia_register_fs_entry`

Registers an external path in the plugin directory sandbox (idempotent; registration is **not** permission).

#### `aia_log`

Structured logging: writes a line into the AiAgent log (system log and the "developer mode" live stream). Do not log heavily on hot paths.

#### `aia_set_timeout`

Requests a per-invocation timeout for this plugin (milliseconds). On the AiAgent side execution currently uses the host default, so this is a capability placeholder / intent declaration; the host enforces a hard cap on a single invocation.

## 8. Sandboxed file access

Reading/writing outside the plugin's own directory requires the "Plugin Store > Settings > Plugin Directory Sandbox" grant, matched on source endpoint `aiagent` + plugin id + normalized path + permission bits — identical to the other endpoints. Dangerous paths are rejected at registration and never allowed. Relative paths are always inside the plugin's own directory.

#### `aia_dns_get`

Reads the per-adapter DNS of the AiAgent machine.

#### `aia_dns_set`

Sets the per-adapter DNS of the AiAgent machine. DNS is **not a file** (registry on Windows, `scutil` on macOS), so it is a dedicated capability: authorization uses the plugin directory sandbox keyed by `system.dns` (`kind=capability`) with the **aiagent** endpoint's read/write grant; unregistered access auto-registers and is denied. `Dns: []` restores an adapter to automatic (DHCP).


## 9. Debugging

Plugin logs go through `aia_log`; a failed plugin is skipped in isolation without affecting other plugins or the main flow.
