@capybara-hq/plugin-tools 0.1.0-rc.2 → 0.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/plugin-permissions.d.ts +17 -0
- package/dist/check/plugin-permissions.js +30 -3
- package/dist/check/plugin-tool-keys.d.ts +13 -0
- package/dist/check/plugin-tool-keys.js +38 -0
- package/dist/check/run.js +2 -0
- package/dist/check/tool-design.d.ts +1 -1
- package/dist/check/tool-design.js +3 -3
- package/package.json +4 -4
package/dist/check/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { checkMigrations, checkSql } from "./migrations.js";
|
|
|
5
5
|
export { checkPluginPack, PLUGIN_PACK_MAX_BYTES } from "./plugin-pack.js";
|
|
6
6
|
export { checkPluginPermissions } from "./plugin-permissions.js";
|
|
7
7
|
export { checkPluginState } from "./plugin-state.js";
|
|
8
|
+
export { checkPluginToolKeys, TOOL_KEY_MAX } from "./plugin-tool-keys.js";
|
|
8
9
|
export { syncPluginVersions } from "./plugin-versions.js";
|
|
9
10
|
export { checkPluginReleaseMetadata } from "./release-metadata.js";
|
|
10
11
|
export { formatFinding, printCheck } from "./report.js";
|
package/dist/check/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { checkMigrations, checkSql } from "./migrations.js";
|
|
|
5
5
|
export { checkPluginPack, PLUGIN_PACK_MAX_BYTES } from "./plugin-pack.js";
|
|
6
6
|
export { checkPluginPermissions } from "./plugin-permissions.js";
|
|
7
7
|
export { checkPluginState } from "./plugin-state.js";
|
|
8
|
+
export { checkPluginToolKeys, TOOL_KEY_MAX } from "./plugin-tool-keys.js";
|
|
8
9
|
export { syncPluginVersions } from "./plugin-versions.js";
|
|
9
10
|
export { checkPluginReleaseMetadata } from "./release-metadata.js";
|
|
10
11
|
export { formatFinding, printCheck } from "./report.js";
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { type Finding } from "./source.js";
|
|
2
2
|
export type { Finding };
|
|
3
|
+
/** A `ctx.tools.register({ name })` call found in an extension's setup. */
|
|
4
|
+
export interface ToolRegistration {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
/** The extension name of the enclosing defineExtension (undefined: not a literal). */
|
|
8
|
+
extension: string | undefined;
|
|
9
|
+
/** The tool name (undefined: not a literal or same-file const). */
|
|
10
|
+
tool: string | undefined;
|
|
11
|
+
/** The call carries a `capybara:allow-dynamic-permission <reason>` marker. */
|
|
12
|
+
allowed: boolean;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* 插件权限静态检查(AST):把 setup(ctx) 里对平台能力的访问与 defineExtension 清单比对。规则:
|
|
5
16
|
* - perm-secrets:用了 ctx.secrets 而 permissions.secrets 不是 true;
|
|
@@ -20,3 +31,9 @@ export type { Finding };
|
|
|
20
31
|
* 扫描范围:传入目录下的非测试源码。
|
|
21
32
|
*/
|
|
22
33
|
export declare function checkPluginPermissions(pluginDirs: string[]): Finding[];
|
|
34
|
+
/**
|
|
35
|
+
* Every `ctx.tools.register({ name })` of the non-test sources under `pluginDirs`, found by the
|
|
36
|
+
* same scanner as {@link checkPluginPermissions} (so it follows ctx aliases and same-file helpers).
|
|
37
|
+
* Unparseable files are skipped here; the permissions check reports them.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findToolRegistrations(pluginDirs: string[]): ToolRegistration[];
|
|
@@ -115,6 +115,7 @@ function readManifest(obj, decls) {
|
|
|
115
115
|
caps: new Set(),
|
|
116
116
|
storage: false,
|
|
117
117
|
models: false,
|
|
118
|
+
name: stringValue(prop(obj, "name")?.value ? resolve(prop(obj, "name")?.value, decls) : undefined),
|
|
118
119
|
setup: undefined,
|
|
119
120
|
};
|
|
120
121
|
for (const key of ["requires", "optional", "provides"]) {
|
|
@@ -165,7 +166,7 @@ function readManifest(obj, decls) {
|
|
|
165
166
|
}
|
|
166
167
|
return m;
|
|
167
168
|
}
|
|
168
|
-
function scanSetup(f, m, decls) {
|
|
169
|
+
function scanSetup(f, m, decls, onTool) {
|
|
169
170
|
const out = [];
|
|
170
171
|
/** 明确的违规:豁免注释不起作用。 */
|
|
171
172
|
const add = (at, rule, message) => {
|
|
@@ -253,6 +254,13 @@ function scanSetup(f, m, decls) {
|
|
|
253
254
|
const spec = args[0] ? resolve(args[0], decls) : undefined;
|
|
254
255
|
const nameProp = spec?.type === "ObjectExpression" ? prop(spec, "name") : undefined;
|
|
255
256
|
const name = nameProp ? stringValue(resolve(nameProp.value, decls)) : undefined;
|
|
257
|
+
onTool?.({
|
|
258
|
+
file: f.file,
|
|
259
|
+
line: f.lineOf(n.start),
|
|
260
|
+
extension: m.name,
|
|
261
|
+
tool: name,
|
|
262
|
+
allowed: !!findMarker(f, n)?.reason,
|
|
263
|
+
});
|
|
256
264
|
if (name === undefined)
|
|
257
265
|
return dynamic(n, "perm-tools", "the tool name");
|
|
258
266
|
if (!m.tools.has(name)) {
|
|
@@ -338,7 +346,7 @@ function scanSetup(f, m, decls) {
|
|
|
338
346
|
}
|
|
339
347
|
return out;
|
|
340
348
|
}
|
|
341
|
-
function scanFile(f) {
|
|
349
|
+
function scanFile(f, onTool) {
|
|
342
350
|
const decls = buildDecls(f);
|
|
343
351
|
const out = [];
|
|
344
352
|
walk(f.program, (n) => {
|
|
@@ -351,7 +359,7 @@ function scanFile(f) {
|
|
|
351
359
|
const obj = arg ? resolve(arg, decls) : undefined;
|
|
352
360
|
if (obj?.type !== "ObjectExpression")
|
|
353
361
|
return;
|
|
354
|
-
out.push(...scanSetup(f, readManifest(obj, decls), decls));
|
|
362
|
+
out.push(...scanSetup(f, readManifest(obj, decls), decls, onTool));
|
|
355
363
|
});
|
|
356
364
|
return out;
|
|
357
365
|
}
|
|
@@ -390,3 +398,22 @@ export function checkPluginPermissions(pluginDirs) {
|
|
|
390
398
|
}
|
|
391
399
|
return findings;
|
|
392
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Every `ctx.tools.register({ name })` of the non-test sources under `pluginDirs`, found by the
|
|
403
|
+
* same scanner as {@link checkPluginPermissions} (so it follows ctx aliases and same-file helpers).
|
|
404
|
+
* Unparseable files are skipped here; the permissions check reports them.
|
|
405
|
+
*/
|
|
406
|
+
export function findToolRegistrations(pluginDirs) {
|
|
407
|
+
const out = [];
|
|
408
|
+
for (const dir of pluginDirs) {
|
|
409
|
+
for (const { file, isTest } of listSourceFiles(dir)) {
|
|
410
|
+
if (isTest)
|
|
411
|
+
continue;
|
|
412
|
+
const parsed = parseFile(file);
|
|
413
|
+
if (isParseError(parsed))
|
|
414
|
+
continue;
|
|
415
|
+
scanFile(parsed, (r) => out.push(r));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return out.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
419
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Finding } from "./source.js";
|
|
2
|
+
export type { Finding };
|
|
3
|
+
/** The model-visible key of an extension tool is `plugin__<extension>__<tool>`; 64 characters at most. */
|
|
4
|
+
export declare const TOOL_KEY_PREFIX = "plugin__";
|
|
5
|
+
export declare const TOOL_KEY_MAX = 64;
|
|
6
|
+
/**
|
|
7
|
+
* plugin-tool-key-length: the key `plugin__<extension>__<tool>` of every `ctx.tools.register`
|
|
8
|
+
* must fit in {@link TOOL_KEY_MAX} characters (model APIs cap tool names at 64). The names must be
|
|
9
|
+
* string literals or same-file consts, as for `perm-tools`; a name that is not is reported as
|
|
10
|
+
* "cannot be confirmed statically" unless the call carries `// capybara:allow-dynamic-permission
|
|
11
|
+
* <reason>`. Scans the non-test sources under `pluginDirs`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkPluginToolKeys(pluginDirs: string[]): Finding[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { findToolRegistrations } from "./plugin-permissions.js";
|
|
2
|
+
/** The model-visible key of an extension tool is `plugin__<extension>__<tool>`; 64 characters at most. */
|
|
3
|
+
export const TOOL_KEY_PREFIX = "plugin__";
|
|
4
|
+
export const TOOL_KEY_MAX = 64;
|
|
5
|
+
/**
|
|
6
|
+
* plugin-tool-key-length: the key `plugin__<extension>__<tool>` of every `ctx.tools.register`
|
|
7
|
+
* must fit in {@link TOOL_KEY_MAX} characters (model APIs cap tool names at 64). The names must be
|
|
8
|
+
* string literals or same-file consts, as for `perm-tools`; a name that is not is reported as
|
|
9
|
+
* "cannot be confirmed statically" unless the call carries `// capybara:allow-dynamic-permission
|
|
10
|
+
* <reason>`. Scans the non-test sources under `pluginDirs`.
|
|
11
|
+
*/
|
|
12
|
+
export function checkPluginToolKeys(pluginDirs) {
|
|
13
|
+
const rule = "plugin-tool-key-length";
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const r of findToolRegistrations(pluginDirs)) {
|
|
16
|
+
if (r.extension === undefined || r.tool === undefined) {
|
|
17
|
+
if (r.allowed)
|
|
18
|
+
continue;
|
|
19
|
+
out.push({
|
|
20
|
+
file: r.file,
|
|
21
|
+
line: r.line,
|
|
22
|
+
rule,
|
|
23
|
+
message: `Cannot statically confirm the length of the tool key (${r.extension === undefined ? "extension name" : "tool name"} is not a literal). Use a string literal (or a same-file const), or add // capybara:allow-dynamic-permission <reason>`,
|
|
24
|
+
});
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const key = `${TOOL_KEY_PREFIX}${r.extension}__${r.tool}`;
|
|
28
|
+
if (key.length > TOOL_KEY_MAX) {
|
|
29
|
+
out.push({
|
|
30
|
+
file: r.file,
|
|
31
|
+
line: r.line,
|
|
32
|
+
rule,
|
|
33
|
+
message: `Tool key "${key}" is ${key.length} characters; the limit is ${TOOL_KEY_MAX} (plugin__<extension>__<tool>). Shorten the tool name to at most ${Math.max(0, TOOL_KEY_MAX - TOOL_KEY_PREFIX.length - r.extension.length - 2)} characters or the extension name`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
package/dist/check/run.js
CHANGED
|
@@ -4,6 +4,7 @@ import { checkMigrations } from "./migrations.js";
|
|
|
4
4
|
import { checkPluginPack } from "./plugin-pack.js";
|
|
5
5
|
import { checkPluginPermissions } from "./plugin-permissions.js";
|
|
6
6
|
import { checkPluginState } from "./plugin-state.js";
|
|
7
|
+
import { checkPluginToolKeys } from "./plugin-tool-keys.js";
|
|
7
8
|
import { checkPluginReleaseMetadata } from "./release-metadata.js";
|
|
8
9
|
import { printCheck } from "./report.js";
|
|
9
10
|
import { checkPluginTestImports } from "./test-imports.js";
|
|
@@ -30,6 +31,7 @@ export function pluginChecks(scope) {
|
|
|
30
31
|
name: "Tool design",
|
|
31
32
|
run: () => checkToolDesign([...(scope.toolDesignDirs ?? []), ...plugins]),
|
|
32
33
|
},
|
|
34
|
+
{ name: "Tool key length", run: () => checkPluginToolKeys(plugins) },
|
|
33
35
|
{ name: "Plugin test imports", run: () => checkPluginTestImports(plugins) },
|
|
34
36
|
{
|
|
35
37
|
name: "Plugin release metadata",
|
|
@@ -5,7 +5,7 @@ export type { Finding };
|
|
|
5
5
|
* - tool-description:`ctx.tools.register(spec, …)` 与 `BuiltinTool` 字面量(有 spec 与 run)里的 spec
|
|
6
6
|
* 必须有字符串字面量 description(可 `+` 拼接)、不少于 20 字符、不含中文(模型可见文案一律英文);
|
|
7
7
|
* - tool-error-actionable:`new ToolError(msg)` 的 msg 是字符串字面量 / 模板(或本文件内的常量、函数返回值),
|
|
8
|
-
* 且含建议动词 use / call / retry / check / ask / pass / provide 之一,让模型知道下一步做什么。
|
|
8
|
+
* 且含建议动词 use / call / retry / check / ask / pass / provide / read 之一,让模型知道下一步做什么。
|
|
9
9
|
* spec 可以直接写对象字面量,也可以引用本文件内的 const;其他写法(跨文件、动态构造)报 tool-description。
|
|
10
10
|
* 已知缺口:从别的文件导入的消息常量 / 函数不追踪;非 ToolError 类的错误不查;运行时拼出的 spec 不查。
|
|
11
11
|
* 扫描:传入目录下的非测试源码。
|
|
@@ -2,7 +2,7 @@ import { isParseError, listSourceFiles, parseFile, stringValue, walk, } from "./
|
|
|
2
2
|
const MIN_DESCRIPTION = 20;
|
|
3
3
|
const CJK = /[㐀-鿿豈- -〿-]/;
|
|
4
4
|
/** 可执行的建议动词:错误信息里至少出现其一(整词、不分大小写)。 */
|
|
5
|
-
const ACTION_VERB = /\b(?:use|call|retry|check|ask|pass|provide)\b/i;
|
|
5
|
+
const ACTION_VERB = /\b(?:use|call|retry|check|ask|pass|provide|read)\b/i;
|
|
6
6
|
const WRAPPERS = new Set([
|
|
7
7
|
"ParenthesizedExpression",
|
|
8
8
|
"TSAsExpression",
|
|
@@ -129,7 +129,7 @@ const MSG_DESC_MISSING = "工具缺少 description,或它不是字符串字面
|
|
|
129
129
|
const MSG_DESC_SHORT = `工具 description 至少 ${MIN_DESCRIPTION} 个字符:说清工具做什么、何时该用、何时不该用`;
|
|
130
130
|
const MSG_DESC_CJK = "工具 description 是模型可见文案,必须是英文,不得含中文字符";
|
|
131
131
|
const MSG_ERR_DYNAMIC = "ToolError 的消息必须是字符串字面量或模板(或指向本文件内的此类常量 / 函数),否则无法检查是否可执行";
|
|
132
|
-
const MSG_ERR_ACTION = "ToolError 的消息要让模型知道下一步怎么做:必须含建议动词之一(use / call / retry / check / ask / pass / provide)";
|
|
132
|
+
const MSG_ERR_ACTION = "ToolError 的消息要让模型知道下一步怎么做:必须含建议动词之一(use / call / retry / check / ask / pass / provide / read)";
|
|
133
133
|
/** 取一个 spec 表达式对应的对象字面量(直接写,或经本文件内的常量引用)。 */
|
|
134
134
|
function specObject(n, env) {
|
|
135
135
|
let e = unwrap(n);
|
|
@@ -204,7 +204,7 @@ function scanFile(f) {
|
|
|
204
204
|
* - tool-description:`ctx.tools.register(spec, …)` 与 `BuiltinTool` 字面量(有 spec 与 run)里的 spec
|
|
205
205
|
* 必须有字符串字面量 description(可 `+` 拼接)、不少于 20 字符、不含中文(模型可见文案一律英文);
|
|
206
206
|
* - tool-error-actionable:`new ToolError(msg)` 的 msg 是字符串字面量 / 模板(或本文件内的常量、函数返回值),
|
|
207
|
-
* 且含建议动词 use / call / retry / check / ask / pass / provide 之一,让模型知道下一步做什么。
|
|
207
|
+
* 且含建议动词 use / call / retry / check / ask / pass / provide / read 之一,让模型知道下一步做什么。
|
|
208
208
|
* spec 可以直接写对象字面量,也可以引用本文件内的 const;其他写法(跨文件、动态构造)报 tool-description。
|
|
209
209
|
* 已知缺口:从别的文件导入的消息常量 / 函数不追踪;非 ToolError 类的错误不查;运行时拼出的 spec 不查。
|
|
210
210
|
* 扫描:传入目录下的非测试源码。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/plugin-tools",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.3",
|
|
4
4
|
"description": "Capybara plugin toolchain: the capybara-plugin CLI to pack, check and release Capybara plugins",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"esbuild": "0.28.2",
|
|
52
52
|
"oxc-parser": "^0.154.0",
|
|
53
53
|
"tar-stream": "3.2.2",
|
|
54
|
-
"@capybara-hq/kernel": "^0.1.0-rc.
|
|
55
|
-
"@capybara-hq/plugin-sdk": "^0.1.0-rc.
|
|
54
|
+
"@capybara-hq/kernel": "^0.1.0-rc.3",
|
|
55
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"zod": "^4.6.7",
|
|
59
|
-
"@capybara-hq/testing": "0.1.0-rc.
|
|
59
|
+
"@capybara-hq/testing": "0.1.0-rc.3"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "tsc -b"
|