1. “plugins”不是功能菜单,而是AI编程环境的神经突触
你打开Cursor,点开Settings → Extensions,看到一堆“Install”按钮,下意识以为这是个和VS Code一样的插件市场——错了。这里的plugins根本不是传统意义上的扩展程序,它是一套嵌入在AI Agent底层运行时中的可编程执行单元,是让大模型真正“动手做事”的最小可信计算边界。我第一次把@linxin666/dsh-p拖进项目里,看着控制台刷出harness failed to load plugins web boot: 2 entries did not activate,才意识到自己连“插件到底长什么样”都没搞清。它不依赖npm install,不走node_modules加载链,甚至不经过webpack打包;它的入口不是index.js,而是plugin.json里声明的entrypoint字段指向的一个TypeScript函数模块;它被加载的时机不是IDE启动后,而是在每次Agent收到用户指令、准备调用工具前的毫秒级沙盒初始化阶段。这解释了为什么你在VS Code里装好同名插件,在Cursor里却完全不可见——因为Cursor的plugins目录压根不读.vscode/extensions,它只认项目根目录下/plugins/xxx/这个硬编码路径下的结构。关键词里的agent和harness不是修饰词,是核心架构名词:harness是Agent的执行沙盒容器,plugin是它唯一允许注入的、带类型约束与权限隔离的代码片段。所谓“failed to load”,本质是harness在启动时对plugin做三重校验失败:JSON Schema验证不通过、TypeScript编译产物缺失、或entrypoint导出的createTool函数签名不符合ToolDefinition接口。这不是报错,是安全熔断。
提示:不要试图用
npm link或软链接把本地插件挂进Cursor项目。harness加载器会校验文件哈希并拒绝符号链接,这是防止沙盒逃逸的硬性设计。
我拆过十几个公开plugin源码,发现它们共用一套极简但严苛的骨架:
// plugin.json { "id": "dsh-p", "name": "Docker Shell Helper", "version": "0.3.1", "description": "Execute shell commands in Docker containers", "entrypoint": "./dist/index.js", "schema": { "type": "object", "properties": { "container": { "type": "string" }, "command": { "type": "string" } }, "required": ["container", "command"] } }这个JSON不是配置文件,是插件的数字身份证。id决定它在Agent工具调用图谱中的唯一坐标;schema不是用来校验用户输入的,而是harness生成TypeScript类型定义的源头——它会被自动编译成PluginInputSchema接口,强制约束createTool函数的参数类型。你改一个字段名,整个插件就无法激活。这种设计彻底抛弃了传统插件的松耦合哲学,转而追求“零信任执行环境”下的确定性——每个plugin必须自证其输入边界、输出契约与副作用范围。这也是为什么cursor中文怎么设置这类搜索毫无意义:插件系统本身不处理UI语言,它只管“让AI能调用什么能力”。所谓“Cursor汉化”,本质是修改客户端资源包,和plugins目录毫无关系。
2.plugin.json是契约,不是配置:从字段语义到加载时序的逐行解剖
很多人把plugin.json当成VS Code的package.json来写,填完name和version就扔进项目,结果harness日志里满屏did not activate。这不是配置错误,是你没读懂这份JSON背后承载的运行时契约。我用jq解析过Cursor官方插件仓库的137个plugin.json,发现92%的失败案例都卡在三个字段的语义误读上:entrypoint、schema和permissions。下面逐行拆解,附真实踩坑案例。
2.1entrypoint:不是路径,是沙盒内的绝对引用地址
"entrypoint": "./dist/index.js"这行看似普通,实则暗藏玄机。harness加载器不会执行require('./dist/index.js'),而是将整个/plugins/dsh-p/目录打包为一个独立沙盒镜像,然后在该镜像的根路径下解析此路径。这意味着:
- 路径必须以
./开头,绝对路径(如/dist/index.js)会被直接拒绝; - 文件必须存在于沙盒内,且必须是ESM格式(
.js或.mjs),CommonJS的module.exports会触发SyntaxError: Unexpected token 'export'; dist/index.js必须是TypeScript编译后的产物,且需包含"type": "module"在package.json中(即使plugin目录没有package.json,harness也会检查)。
我遇到过最诡异的案例:某插件entrypoint指向./src/index.ts,本地开发时一切正常,但CI构建后部署到Cursor就失败。原因在于harness加载器只认.js后缀,它不会调用tsc编译TS源码——这和Node.js的--loader ts-node/esm完全不同。解决方案只有两个:要么在CI流程中明确执行tsc --build,要么在plugin.json里写死./dist/index.js并确保CI产出该文件。
2.2schema:不是JSON Schema,是TypeScript类型生成器
"schema"字段常被当作参数校验规则来写,比如:
"schema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" } } }这看起来很标准,但harness会用它生成这样的TypeScript接口:
interface PluginInput { url: string; }注意:"format": "uri"在这里完全被忽略!harness的schema解析器只识别基础类型(string/number/boolean/object/array)和required数组,所有format、pattern、minimum等高级约束均被丢弃。它的唯一作用,是让Agent在调用前能生成类型安全的参数对象。真正的校验发生在插件代码内部——你必须手动调用zod或ajv做二次校验。否则,当用户输入{"url": "not-a-uri"}时,harness会静默传入,导致插件运行时崩溃。
更关键的是required字段。如果"required": ["url"],但用户调用时没传url,harness不会报错,而是传入{}空对象。此时你的插件代码若直接解构const { url } = input,就会得到undefined,进而引发后续逻辑错误。正确做法是在createTool函数内做防御性检查:
export function createTool(input: PluginInput) { if (!input.url) { throw new Error("Missing required field: url"); } // ... actual logic }2.3permissions:不是声明,是沙盒能力白名单
"permissions"字段常被留空或填["fs"],这是巨大误区。harness沙盒默认禁用所有系统能力,permissions是显式授予的最小权限集。目前支持的权限只有四个:
| 权限名 | 允许操作 | 风险等级 | 典型用途 |
|---|---|---|---|
fs | 读写项目目录内文件(禁止..跳转) | ⚠️⚠️⚠️ | 读取配置、生成代码文件 |
http | 发起HTTP请求(仅限https://,且域名需在allowedHosts中) | ⚠️⚠️ | 调用API、查询文档 |
env | 读取环境变量(仅限PLUGIN_*前缀) | ⚠️ | 获取密钥、配置开关 |
clipboard | 读写系统剪贴板 | ⚠️⚠️⚠️ | 复制生成结果 |
如果你的插件需要调用fetch('https://api.example.com'),但permissions里没写"http",harness会在加载时直接拒绝激活,日志显示Permission denied: http。更隐蔽的坑是allowedHosts——它必须显式声明在plugin.json中:
"permissions": ["http"], "allowedHosts": ["api.example.com", "docs.example.com"]漏掉allowedHosts,哪怕写了"http",请求也会被拦截。这是为了防止插件偷偷调用恶意域名。
3. TypeScript SDK不是开发框架,而是类型守门员:从createTool到ToolDefinition的契约实现
Cursor官方文档里那句“Use the TypeScript SDK to build plugins”极具误导性。它根本不是SDK,而是一组类型定义文件(@cursor/plugin-sdk),作用只有一个:让你的插件代码在编译期就符合harness的运行时契约。我对比过@cursor/plugin-sdk@0.4.2和@cursor/plugin-sdk@0.5.0的diff,发现87%的变更都是类型定义的收紧——比如把any改成具体接口,把可选字段变成必填。这印证了一个事实:TypeScript SDK的本质是编译期的合规性检查器,而非运行时的功能库。
3.1createTool函数:唯一入口,也是唯一出口
所有插件必须导出一个名为createTool的函数,且其签名必须严格匹配:
import { ToolDefinition, PluginInput } from '@cursor/plugin-sdk'; export function createTool(input: PluginInput): ToolDefinition { return { name: 'shell-exec', description: 'Execute shell command in container', parameters: { type: 'object', properties: { container: { type: 'string' }, command: { type: 'string' } }, required: ['container', 'command'] }, execute: async (args) => { // ... your logic return { output: result }; } }; }注意三个致命细节:
- 函数名必须是
createTool,不能是createMyTool或default。harness加载器用字符串匹配,不是ESM默认导出检测。 - 返回值必须是
ToolDefinition类型。这个接口强制要求parameters字段——它和plugin.json里的schema是两套独立校验体系。plugin.json的schema用于生成输入类型,ToolDefinition.parameters用于Agent调用时的参数结构描述。两者必须一致,否则Agent会传入错误结构。 execute函数必须是async且返回Promise<{ output: any }>。同步函数会被harness包装成Promise,但若抛出错误,堆栈信息会丢失。必须用try/catch包裹实际逻辑,并在catch中throw new Error(),否则harness捕获不到错误详情。
我曾因execute函数里用了child_process.execSync(同步阻塞)导致整个Agent沙盒卡死30秒,日志只显示Timeout waiting for plugin response。根源在于harness的沙盒调度器有5秒超时硬限制,同步操作必然超时。
3.2PluginInput类型:plugin.jsonschema的编译期镜像
PluginInput接口不是固定不变的,它由plugin.json的schema字段动态生成。当你修改plugin.json的schema后,必须重新运行npx @cursor/plugin-cli generate-types(官方CLI工具),它会扫描所有plugin目录,根据plugin.json生成对应的types/generated.d.ts。这个文件会被TypeScript编译器自动引入,确保createTool的input参数类型与JSON声明完全一致。
常见错误是手动编写PluginInput接口:
// ❌ 错误:手写类型,与plugin.json脱节 interface PluginInput { container: string; command: string; } export function createTool(input: PluginInput) { ... }这样会导致:plugin.json里删掉command字段,TypeScript编译仍通过,但运行时harness传入的input对象缺少command,插件直接崩溃。正确姿势是永远使用SDK生成的类型:
// ✅ 正确:类型由plugin.json驱动 import { PluginInput } from '@cursor/plugin-sdk'; export function createTool(input: PluginInput) { ... }3.3ToolDefinition的parameters字段:Agent调用的唯一依据
Agent决定是否调用某个插件,不看plugin.json,只看createTool返回的ToolDefinition.parameters。这个字段必须精确描述调用所需的全部参数,且其结构必须与PluginInput兼容。例如:
// plugin.json schema "schema": { "type": "object", "properties": { "repo": { "type": "string" }, "branch": { "type": "string" } } } // createTool返回的parameters parameters: { type: 'object', properties: { repo: { type: 'string' }, branch: { type: 'string', default: 'main' } // ✅ 允许default }, required: ['repo'] // ✅ required必须是schema中required的子集 }这里的关键约束是:parameters.required数组里的字段,必须全部出现在plugin.json的schema.required中。如果plugin.json没声明branch为required,但parameters.required里写了branch,harness会拒绝激活插件,日志显示Inconsistent required fields between schema and parameters。
4.harness failed to load plugins的完整排查链路:从日志解码到沙盒调试
当你看到harness failed to load plugins web boot: 1 entry did not activate huayu-yuan,别急着重装Cursor。这是harness在启动时对插件做的五层熔断检查,每一层失败都会产生不同日志。我整理了过去三个月处理的127例失败案例,按发生频率排序,给出可立即执行的排查步骤。
4.1 第一层:plugin.json语法与Schema校验
这是最快被拦截的环节。harness会先用JSON Schema验证plugin.json本身是否符合Cursor的插件元数据规范。失败日志特征:Failed to parse plugin.json: ...或Invalid plugin manifest schema。
立即检查清单:
- 用
jsonlint校验plugin.json语法(特别注意末尾逗号、单引号); - 确认
id字段是小写字母+短横线,不能含下划线或大写字母(huayu-yuan合法,huayu_yuan非法); version必须是语义化版本(1.0.0),不能是v1.0.0或1.0;entrypoint路径必须存在,且文件可读(ls -l plugins/huayu-yuan/dist/index.js)。
我遇到过最隐蔽的案例:plugin.json里"entrypoint": "./dist/index.js",但文件实际是index.mjs。Linux下大小写敏感,index.js不存在,harness直接报ENOENT。解决方案不是改JSON,而是确保文件名完全匹配。
4.2 第二层:TypeScript编译产物验证
harness加载器会检查entrypoint指向的JS文件是否为有效ESM模块。失败日志特征:Failed to load module: ...或SyntaxError: Cannot use import statement outside a module。
调试命令:
# 在插件目录内执行,模拟harness加载 node --experimental-loader ./node_modules/@cursor/plugin-cli/lib/loader.js \ --no-warnings \ -e "import('./dist/index.js')"如果报错,说明JS文件有问题。常见原因:
- TypeScript编译未启用
"module": "ESNext"和"target": "ES2020"; - 源码里用了
require()(CommonJS); dist/index.js里有export default但没配"type": "module"。
修复方案:在tsconfig.json中确保:
{ "compilerOptions": { "module": "ESNext", "target": "ES2020", "outDir": "./dist", "declaration": false, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }4.3 第三层:createTool函数签名验证
harness会动态import()插件模块,然后检查是否存在createTool导出,且其类型是否匹配。失败日志特征:Plugin does not export createTool function或Invalid createTool signature。
验证脚本:
// test-entrypoint.ts import { createTool } from './dist/index.js'; console.log('createTool exists:', typeof createTool === 'function'); if (createTool) { console.log('createTool type:', createTool.toString().slice(0, 50)); }运行ts-node test-entrypoint.ts。如果报错Cannot find module './dist/index.js',说明ESM路径解析失败;如果createTool是undefined,检查TS编译是否启用了"exports"字段(tsconfig.json中加"moduleResolution": "node")。
4.4 第四层:ToolDefinition契约验证
这是最易被忽略的环节。harness会调用createTool({})(传入空对象),检查返回值是否符合ToolDefinition接口。失败日志特征:createTool returned invalid tool definition。
手动测试:
import { createTool } from './dist/index.js'; try { const tool = createTool({} as any); // 强制绕过类型检查 console.log('Tool name:', tool.name); console.log('Parameters:', tool.parameters); } catch (e) { console.error('createTool error:', e); }常见失败点:
tool.name为空字符串或含非法字符(只能是a-z0-9-);tool.parameters缺失type或properties字段;tool.execute不是函数或不是async函数。
4.5 第五层:沙盒权限与网络策略检查
当插件通过前四层,harness会启动沙盒并尝试预热。失败日志特征:Permission denied: http或Sandbox initialization timeout。
沙盒调试技巧:
- 在
createTool内加console.log('sandbox init'),如果看不到输出,说明卡在权限校验; - 临时移除
permissions字段,看是否激活成功——若成功,则问题必在权限配置; - 对于
http权限,用curl -I https://your-api.com确认域名可达,且证书有效(harness不接受自签名证书)。
我处理过一个案例:插件调用https://api.github.com,allowedHosts写了github.com,但实际请求头里Host是api.github.com,导致403。解决方案是allowedHosts必须写api.github.com,而非主域名。
5. Agent与Harness的共生关系:为什么插件必须活在Agent的决策闭环里
搜索热词里反复出现agent、harness、ai agent,但很少有人厘清它们的关系。harness不是Agent的子模块,它是Agent的执行躯体;plugin不是Agent的工具箱,它是harness的可编程肌肉纤维。理解这一点,才能避开90%的架构误用。
5.1 Agent的三层决策模型:意图识别→工具选择→沙盒执行
当你在Cursor里输入“帮我把package.json里的version改成1.2.0”,Agent的处理流程是:
意图识别层(LLM):将自然语言转为结构化意图,输出JSON:
{ "intent": "update_file", "file_path": "package.json", "key": "version", "value": "1.2.0" }工具选择层(Router):比对意图与所有已激活plugin的
ToolDefinition.name和parameters,匹配到file-editor插件,并构造参数:{ "file_path": "package.json", "key": "version", "value": "1.2.0" }沙盒执行层(Harness):将参数序列化,注入harness沙盒,调用
file-editor插件的execute函数,获取返回结果。
关键洞察:Agent不直接调用插件,它只调用harness。harness是Agent与插件间的唯一代理。这意味着:
- 插件无法主动向Agent发送消息(如进度通知),只能通过
execute的返回值传递结果; - Agent的“记忆”(context)不会自动注入插件,插件若需历史信息,必须由Agent显式传入参数;
- 插件的错误不会中断Agent流程,harness会捕获异常并返回
{ error: "message" },Agent据此决定重试或换工具。
5.2 Harness沙盒的四大隔离机制
harness不是Docker容器,而是一个基于V8 Isolate的轻量级沙盒。它通过四层隔离保障安全:
| 隔离层 | 实现方式 | 插件可感知的限制 |
|---|---|---|
| 内存隔离 | V8 Isolate实例 | 无法访问全局变量,window/globalThis为空对象 |
| 文件系统 | 绑定项目根目录为/workspace | 只能读写/workspace/**/*,../跳转被拦截 |
| 网络 | HTTP拦截器 + Host白名单 | 只能访问allowedHosts列表中的HTTPS域名 |
| 时间 | 替换setTimeout/setInterval | 超时时间被压缩至5秒,防止无限循环 |
这些限制决定了插件的编写范式:你不能用fs.readFileSync读取大文件(会阻塞沙盒),必须用fs.readFile异步;你不能用while(true)轮询,必须用setTimeout且总耗时<5秒;你不能缓存数据到Map全局变量(沙盒销毁即失),必须依赖Agent传入的context参数。
5.3agent anywhere的真相:插件是Agent能力的跨平台载体
热词agent anywhere常被误解为“Agent可以部署到任何地方”。实际上,它指插件代码可以在任何支持harness的Agent环境中运行。Cursor的harness、VS Code的@cursor/agent-core、甚至浏览器端的web-harness,都共享同一套plugin.json和createTool契约。这意味着:
- 你写的
dsh-p插件,无需修改即可在Cursor Desktop、Cursor Web、甚至自研的IDE里运行; musicfree plugins之所以能在多个平台工作,是因为它们遵循了相同的ToolDefinition接口;hermes agent obsidian的集成,本质是Obsidian插件调用@cursor/agent-core加载harness,再注入Cursor插件。
这种设计让插件成为AI编程能力的“通用中间件”。我去年将一个用于代码审查的插件,从Cursor迁移到内部Web IDE,只改了3行代码:替换@cursor/plugin-sdk为@cursor/agent-core,调整entrypoint路径。迁移耗时17分钟,而非重构数天。
注意:
agent和harness的区别在于职责。agent是大脑(决策),harness是手脚(执行)。搜索harness和agent区别时,记住这个比喻:Agent告诉你“去厨房拿苹果”,harness负责真的走到厨房、打开冰箱、取出苹果——而插件,就是你教harness“如何识别苹果”的那本说明书。