主页
Framework Version 0.1.0 MIT Independent Repository

LingXiPluginSDK

为 LingXiAgent 编写进程外(out-of-process)原生插件的官方 Swift Package。声明交互式 Slash 命令、扩展模型可调用的 Tool、订阅生命周期 Hook,并只读观察宿主推送的运行时快照。SDK 不含 Core:插件是独立可执行文件,通过 stdin/stdout 上的 JSON Lines IPC 与运行中的 LingXiAgent 通信。

macOS: 13.0+
Linux: Swift 6.0 toolchain (x86_64 / aarch64)
Windows: 可构建(Swift 6 工具链),非官方日常验证平台
Swift: 6.0+ tools version
依赖: Foundation only

Declaration

import LingXiPluginSDK

Overview

LingXiPluginSDK 是 LingXiAgent 的官方扩展开发套件。它不是提示词包装器,也不是脚本解释器:你用现代 Swift 写一个独立可执行文件,类型安全,进程独立。SDK 里只有协议、DTO 与 IPC 驱动——没有会话存储、没有权限引擎、没有 Provider 运行时,因此插件返回值再离谱也不会弄坏用户正在进行的会话:你在另一个进程里。

设计上遵循两条边界:

  • 进程隔离,而非动态注入:插件以独立子进程运行,Core 与插件之间用 stdin/stdout 上的换行分隔 JSON(JSON Lines IPC)通信,不使用 dlopen,因此插件不在 Core 的地址空间里读它的内存;
  • 只读感知,不接管调度:插件可以读取宿主推送的运行时快照(会话 Token 水位、P-Core / E-Core 计数、耗时指标、工作区状态),但不能改写 Agent 的调度与会话状态——主驾驶员始终是 Client / TUI。

Plugin Lifecycle

Core spawns the plugin process ↓ host.snapshot Core 推送权威运行时快照 ↓ plugin.initialize 握手:manifest / ipcVersion / tools / commands ↓ activate(context:) 你的注册对宿主可见 … tool.execute · command.execute · hook.emit(每次调用前先刷新快照) ↓ stdin 关闭 (EOF) deactivate(),进程退出
Note
编译后的插件二进制放入全局 ~/.lingxiagent/plugins/,或项目级 <project>/.lingxi/plugins/,LingXiAgent 启动时自动发现并校验清单。它必须是当前平台可执行的二进制:macOS / Linux 直接用文件名,Windows 复制 my-plugin.exe。

Quick Start

只需 3 步,即可完成一个生产级 Swift 插件的开发与部署。

1 创建 Swift Package 并声明 LingXiPluginSDK 依赖

// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "MyFirstPlugin",
    platforms: [.macOS(.v13)],
    dependencies: [
        .package(url: "https://github.com/LingXiFox/LingXiPluginSDK.git", from: "0.1.0")
    ],
    targets: [
        .executableTarget(
            name: "my-plugin",
            dependencies: [
                .product(name: "LingXiPluginSDK", package: "LingXiPluginSDK")
            ]
        )
    ]
)

LingXiPluginSDK 是独立仓库与独立 SemVer 的 MIT Swift Package,不需要拉取 LingXiAgent 仓库即可编译插件。

2 编写 main.swift:实现 LingXiPlugin 协议

import Foundation
import LingXiPluginSDK

@main
struct MyFirstPlugin: LingXiPlugin {
    init() {}

    var manifest: PluginManifest {
        PluginManifest(
            id: "com.example.my-plugin",
            name: "My Plugin",
            version: "0.1.0",
            description: "演示命令注册与宿主运行时快照读取",
            capabilities: [.projectRead]
        )
    }

    func activate(context: PluginContext) async throws {
        context.registerTool(EchoTool())
        context.registerCommand(StatusCommand())
        context.on(.sessionStart) { payload in
            context.logger.info("session started: \(payload.subjectID)")
        }
    }
}

struct EchoTool: PluginTool {
    var name: String { "echo" }
    var description: String { "Echoes its argument back." }
    func execute(arguments: String, context: ToolExecutionContext) async throws -> String {
        arguments
    }
}

struct StatusCommand: PluginCommand {
    var name: String { "status" }
    var description: String { "Reports what the host published." }

    func execute(args: [String], context: CommandExecutionContext) async throws -> PluginCommandResult {
        guard let workspace = try? await context.info.getWorkspaceInfo() else {
            return .message("宿主尚未推送运行时快照", presentation: .inline)
        }
        return .message("工作区 \(workspace.rootPath)(\(workspace.currentGitBranch ?? "非 Git"))",
                        presentation: .modal,
                        title: "Workspace")
    }
}

这份示例与 SDK 仓库里的 DocumentationExampleCompileTests 同源:文档片段是被编译验证过的,不是设想出来的 API。

3 编译并一键投递二进制到插件目录

# 编译 Release 二进制
swift build -c release

# 投递到 LingXiAgent 全局插件目录(或项目级 .lingxi/plugins/)
mkdir -p ~/.lingxiagent/plugins
cp .build/release/my-plugin ~/.lingxiagent/plugins/

# 在 TUI 中输入 /plugins 查看加载状态,敲击 /status 体验居中弹出窗口

平台差异:macOS 与 Linux 拷贝 .build/release/my-plugin;Windows 使用 swift build -c release 产出的 my-plugin.exe。插件必须是宿主进程能直接 exec 的原生二进制,不能是脚本包装。

Topics

Security Model

下面这张表只写当前代码确实实现并由测试覆盖的层。把「进程隔离」写成「操作系统沙箱」是对使用者的误导,因此本页同时列出这套机制不提供的东西。

安全层 当前实现
Process isolation 插件是独立 Process,不是被注入的代码;不存在 dlopen 同址共享。
Credential environment isolation 子进程得到净化后的环境变量:Provider key 与 LINGXI_CREDENTIALS_PASSPHRASE 不会继承。
Manifest capability admission 握手时逐项送 PermissionEngine 预审;被判 deny 立即终止插件进程。
Typed IPC boundary 换行分隔的类型化请求/响应;未知方法、缺参、畸形载荷都转成失败调用,不会打挂插件循环。
Watchdog & termination 单次调用超时(默认 3.0s)即终止;关闭 stdin 让插件按 EOF 正常退出,必要时级联强杀进程树。
这套机制不提供什么
  • 没有 OS 级 syscall / 文件系统沙箱:插件进程仍以运行 LingXiAgent 的用户权限执行,可以直接调用 FileManager 或发起网络连接。
  • Capability 是宿主侧的准入策略,不是内核边界:它约束宿主愿意与这个插件协作的范围,不拦截插件自己发起的 syscall。
  • 因此本页不写「任何越界访问一定会被拦截」这类断言;需要硬隔离时,请把插件跑在容器或独立用户下,这属于部署层职责。
Important
插件二进制来自第三方仓库时,它拥有的就是你授予 LingXiAgent 的那个用户权限。安装前请审查来源与代码,并把 capabilities 声明视为「这个插件打算做什么」的线索,而不是运行时的强制上限。
Language-Agnostic IPC Plugin IPC version 1

LingXi Plugin IPC · JSON Lines 协议规范

官方 SDK 用 Swift,但插件协议本身与语言无关:任何能读写 stdin/stdout 的语言都可以实现插件二进制,只要遵守下面的帧格式与方法名。 方法名的唯一权威来源是 SDK 里的 PluginIPC.Method,文档不能自创名字。

1. 帧格式与方向

  • 载体通道:Core 与插件通过子进程的 stdin / stdout 通信,不开网络端口;
  • 分帧:一行一个 JSON 对象,以 \\n 定界(JSON Lines / newline-delimited JSON);
  • 请求:{"id": "...", "method": "...", "params": ...};响应:{"id": "...", "result": ..., "error": null};
  • 不是 JSON-RPC 2.0:线上报文没有 "jsonrpc": "2.0" 字段,也没有批量与 notification 语义。所有调用都是 Core → Plugin 的单向请求/响应,运行时信息通过 host.snapshot 由宿主推送,插件不反向发起 RPC。
Method 方向 参数类型 结果类型
host.snapshotCore → PluginPluginRuntimeSnapshotempty
plugin.initializeCore → PluginPluginInitializeParamsPluginHandshakeResult
tool.executeCore → PluginPluginToolCallParamsString
command.executeCore → PluginPluginCommandCallParamsPluginCommandCallResult
hook.emitCore → PluginPluginHookPayloadempty

调用顺序固定为 spawn → host.snapshot → plugin.initialize → activate(context:);此后每次 tool.execute / command.execute / hook.emit 之前都会先刷新一次快照。

2. 报文样例(与真实 Codable DTO 一致)

Push host.snapshot(宿主推送权威运行时状态)
{"id":"snap-1","method":"host.snapshot","params":{
  "observedAt":"2026-10-01T08:00:00Z",
  "ipcVersion":1,
  "workspace":{"rootPath":"/work/demo","isGitRepository":true,"currentGitBranch":"main","dirtyFileCount":2,"primaryLanguages":["Swift"],"coreVersion":"1.1.0"},
  "peCore":{"pCoreTokens":18420,"eCoreObjects":37,"eCoreReferences":52,"lastEvictionTrigger":"pCoreSoftLimit","reasoningEffort":"high","backgroundTaskCount":1},
  "contextState":{"activeModelID":"gpt-5.6-luna","totalTokenUsage":15400,"contextWindowPercentage":0.12,"isCompacted":false,"messageCount":8},
  "performance":null
}}

段落缺省或为 null 就代表宿主此刻没有这项事实:读取它会抛 PluginInfoUnavailable,SDK 不会填 idle / 0。上面的 performance:null 就是这种情况。

Method plugin.initialize(握手:宣告清单与贡献项)
// 1. Core 发起握手(带上宿主自己的 IPC 版本)
{"id":"init-1","method":"plugin.initialize","params":{"hostIPCVersion":1,"coreVersion":"1.1.0"}}

// 2. 插件回包:manifest + 自己支持的 ipcVersion + tools + commands
{"id":"init-1","result":{
  "ipcVersion":1,
  "manifest":{
    "id":"com.example.rust-plugin",
    "name":"Rust 诊断扩展",
    "version":"1.0.0",
    "description":"调用本地分析引擎做安全体检",
    "author":"example",
    "capabilities":["projectRead"],
    "minimumCoreVersion":"1.1.0"
  },
  "tools":[{"name":"rust_audit","description":"Scan a path and report issues.","inputSchema":"{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}}}"}],
  "commands":[{"name":"rust-info","aliases":["ri"],"description":"Show Rust plugin status.","category":"Diagnostics","argumentHint":""}]
},"error":null}

兼容性由 ipcVersion 协商:双方没有共同支持版本时,握手阶段直接失败并终止进程,而不是等到某个 command 解码时才炸。minimumCoreVersion 只是人类可读提示,从不参与线上判定。

Method tool.execute(模型自主调度工具)
// 1. 模型在推理循环里调用插件工具;arguments 是原始 JSON 字符串,由插件自己解释 inputSchema
{"id":"tool-88","method":"tool.execute","params":{
  "toolName":"rust_audit",
  "arguments":"{\"path\":\"Sources/\"}",
  "sessionID":"sess-abc-123",
  "toolCallID":"call_9901"
}}

// 2. 插件返回纯文本(或文本化的 JSON 结论)
{"id":"tool-88","result":"{\"issues\":0,\"scannedFiles\":42,\"status\":\"clean\"}","error":null}
Method command.execute(用户敲斜杠命令)
// 1. 用户输入 /rust-info --verbose
{"id":"cmd-42","method":"command.execute","params":{
  "commandName":"rust-info",
  "arguments":["--verbose"],
  "sessionID":"sess-abc-123"
}}

// 2. presentation 取 "modal" 或 "inline";isPrompt=true 时文本作为提示词展开
{"id":"cmd-42","result":{
  "isPrompt":false,
  "text":"• scanned files: 42\n• issues: 0",
  "presentation":"modal",
  "title":"Rust 审计诊断报告"
},"error":null}
Method hook.emit(生命周期事件广播)
// 事件取值:sessionStart · sessionEnd · agentTurnStart · agentTurnEnd · toolBefore · toolAfter
{"id":"hook-7","method":"hook.emit","params":{"event":"sessionStart","subjectID":"sess-abc-123","metadata":{"workspace":"/work/demo"}}}

{"id":"hook-7","result":null,"error":null}
Errors 错误与边界语义
  • 失败以响应形式返回:{"id":"...","result":null,"error":"message"};宿主把它转成一次失败的调用,不影响会话存续。
  • 未知方法、缺少 params、载荷畸形都会得到错误响应,插件主循环本身不应崩溃。
  • stdin 关闭(EOF)是唯一的正常退出信号:插件应在此调用 deactivate() 后退出。
  • 超时默认 3.0s;长任务请挪到后台执行,请求路径只返回一个任务句柄,不要占住这次调用。

Compatibility & Versioning

This SDK0.1.0
Plugin IPC version1(PluginIPC.currentVersion;supportedVersions = [1])
Swift tools version6.0
Minimum LingXiAgent(tools / commands / hooks)1.1.0
Minimum LingXiAgent(context.info 有数据)会推送 host.snapshot 的构建(1.1.0 之后)
许可证MIT(与 LingXiAgent 的 LCSAL-1.1 / PolyForm 轨道无关)

对不推送快照的旧宿主:tools 与 commands 照常工作,context.info 抛 PluginInfoUnavailable——不会出现编造的指标。SDK、Agent、Model Catalog 三者版本各自独立演进,不做同步锁版。

Troubleshooting

现象 / 报错 实际发生了什么
插件从未出现二进制不在 ~/.lingxiagent/plugins(或 .lingxi/plugins),或没有可执行权限。
Plugin initialization capability request: … denied宿主的权限策略拒绝了清单里声明的某项 capability,握手阶段即终止进程。
Unsupported host IPC version N双方没有共同的 PluginIPC.supportedVersions:用当前 SDK 重新构建插件,或升级宿主。
Plugin 'x' speaks LingXi Plugin IPC vN同一类不匹配,只是由宿主侧在加载时检出。
Tool 'x' not found in plugin路由的名字与握手时宣告的 tools 不一致;工具名是精确匹配。
Command 'x' not found in plugin同上,针对命令(别名同样参与匹配)。
Plugin IPC call timed out after 3.0s处理器没在超时内应答,宿主终止进程:把长耗时工作移出请求路径。
Plugin process is not running进程已退出或被终止:检查 stderr 与 deactivate() 路径。
Plugin runtime info unavailable: peCore (no snapshot received)宿主从未推送快照:宿主过旧,或在 host.snapshot 之前就读了 info。

See Also