@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.
@@ -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";
@@ -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.2",
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.1",
55
- "@capybara-hq/plugin-sdk": "^0.1.0-rc.0"
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.2"
59
+ "@capybara-hq/testing": "0.1.0-rc.3"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc -b"