@capybara-hq/kernel 0.1.0-rc.2 → 0.1.0-rc.4

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.
Files changed (312) hide show
  1. package/dist/admin/admin.controller.d.ts +9 -4
  2. package/dist/admin/admin.controller.js +13 -4
  3. package/dist/admin/extensions.controller.d.ts +8 -1
  4. package/dist/admin/extensions.controller.js +22 -2
  5. package/dist/agents/agent-lookup.d.ts +3 -0
  6. package/dist/agents/agent-lookup.js +20 -2
  7. package/dist/agents/agent-profile.service.d.ts +29 -0
  8. package/dist/agents/agent-profile.service.js +117 -0
  9. package/dist/agents/agents.controller.d.ts +93 -3
  10. package/dist/agents/agents.controller.js +114 -4
  11. package/dist/agents/agents.module.js +6 -2
  12. package/dist/agents/agents.service.d.ts +13 -5
  13. package/dist/agents/agents.service.js +49 -13
  14. package/dist/agents/definition.d.ts +35 -8
  15. package/dist/agents/definition.js +203 -17
  16. package/dist/agents/instruction-files.d.ts +60 -0
  17. package/dist/agents/instruction-files.js +111 -0
  18. package/dist/agents/models.controller.d.ts +9 -3
  19. package/dist/agents/models.controller.js +19 -4
  20. package/dist/agents/profile.d.ts +96 -0
  21. package/dist/agents/profile.js +51 -0
  22. package/dist/agents/readiness.controller.d.ts +26 -0
  23. package/dist/agents/readiness.controller.js +56 -0
  24. package/dist/agents/templates.js +20 -5
  25. package/dist/background/background.controller.d.ts +38 -0
  26. package/dist/background/background.controller.js +87 -0
  27. package/dist/background/background.module.d.ts +10 -0
  28. package/dist/background/background.module.js +32 -0
  29. package/dist/background/finish.d.ts +43 -0
  30. package/dist/background/finish.js +94 -0
  31. package/dist/background/launch-lock.d.ts +14 -0
  32. package/dist/background/launch-lock.js +39 -0
  33. package/dist/background/launch.d.ts +36 -0
  34. package/dist/background/launch.js +70 -0
  35. package/dist/background/poll.job.d.ts +53 -0
  36. package/dist/background/poll.job.js +182 -0
  37. package/dist/background/render.d.ts +23 -0
  38. package/dist/background/render.js +48 -0
  39. package/dist/background/session.d.ts +37 -0
  40. package/dist/background/session.js +66 -0
  41. package/dist/background/status.d.ts +35 -0
  42. package/dist/background/status.js +57 -0
  43. package/dist/background/stop.d.ts +62 -0
  44. package/dist/background/stop.js +225 -0
  45. package/dist/background/store.d.ts +150 -0
  46. package/dist/background/store.js +351 -0
  47. package/dist/background/store.module.d.ts +6 -0
  48. package/dist/background/store.module.js +22 -0
  49. package/dist/background/tasks.service.d.ts +67 -0
  50. package/dist/background/tasks.service.js +186 -0
  51. package/dist/background/wake-rules.d.ts +57 -0
  52. package/dist/background/wake-rules.js +83 -0
  53. package/dist/background/wake.d.ts +28 -0
  54. package/dist/background/wake.js +57 -0
  55. package/dist/catalog/catalog-content.js +4 -1
  56. package/dist/catalog/claude/agents.js +15 -10
  57. package/dist/catalog/marketplaces.service.d.ts +1 -1
  58. package/dist/catalog/plugin-import.js +1 -1
  59. package/dist/catalog/plugin-upgrade.js +1 -1
  60. package/dist/channels/cards.d.ts +8 -2
  61. package/dist/channels/cards.js +17 -9
  62. package/dist/channels/connections.service.d.ts +16 -1
  63. package/dist/channels/connections.service.js +38 -4
  64. package/dist/channels/threads.d.ts +5 -0
  65. package/dist/channels/threads.js +8 -0
  66. package/dist/config/schema.d.ts +18 -0
  67. package/dist/config/schema.js +104 -2
  68. package/dist/conversations/access.js +2 -0
  69. package/dist/conversations/artifacts.d.ts +1 -1
  70. package/dist/conversations/artifacts.js +1 -1
  71. package/dist/conversations/attachments.service.d.ts +30 -0
  72. package/dist/conversations/attachments.service.js +130 -5
  73. package/dist/conversations/conversations.controller.d.ts +36 -5
  74. package/dist/conversations/conversations.controller.js +62 -12
  75. package/dist/conversations/conversations.module.js +3 -0
  76. package/dist/conversations/conversations.service.d.ts +44 -2
  77. package/dist/conversations/conversations.service.js +140 -12
  78. package/dist/conversations/event-log.d.ts +12 -1
  79. package/dist/conversations/event-log.js +29 -1
  80. package/dist/conversations/events.d.ts +81 -3
  81. package/dist/conversations/events.js +83 -16
  82. package/dist/conversations/queue.d.ts +3 -1
  83. package/dist/conversations/queue.js +13 -2
  84. package/dist/conversations/queue.service.js +3 -1
  85. package/dist/conversations/titles.d.ts +7 -0
  86. package/dist/conversations/titles.js +15 -0
  87. package/dist/db/kernel-schema.d.ts +60 -4
  88. package/dist/db/pool.d.ts +5 -0
  89. package/dist/db/pool.js +3 -0
  90. package/dist/errors.d.ts +8 -0
  91. package/dist/errors.js +8 -0
  92. package/dist/evals/recorded-tools.d.ts +27 -1
  93. package/dist/evals/recorded-tools.js +85 -10
  94. package/dist/evals/run-replay.d.ts +10 -2
  95. package/dist/evals/run-replay.js +49 -12
  96. package/dist/evals/step-replay.js +12 -6
  97. package/dist/evals/trajectory.d.ts +2 -2
  98. package/dist/evals/trajectory.js +1 -1
  99. package/dist/extensions/admin-ops.js +2 -0
  100. package/dist/extensions/admin-view.d.ts +3 -1
  101. package/dist/extensions/admin-view.js +10 -2
  102. package/dist/extensions/blobs.js +1 -0
  103. package/dist/extensions/config-issues.d.ts +13 -1
  104. package/dist/extensions/config-issues.js +22 -5
  105. package/dist/extensions/config-json-schema.d.ts +43 -7
  106. package/dist/extensions/config-json-schema.js +124 -58
  107. package/dist/extensions/deployment-config-store.d.ts +6 -0
  108. package/dist/extensions/deployment-config.d.ts +50 -3
  109. package/dist/extensions/deployment-config.js +125 -28
  110. package/dist/extensions/display-name.d.ts +22 -0
  111. package/dist/extensions/display-name.js +77 -0
  112. package/dist/extensions/extensions.module.js +6 -0
  113. package/dist/extensions/host.d.ts +26 -1
  114. package/dist/extensions/host.js +82 -9
  115. package/dist/extensions/org-config.controller.d.ts +11 -3
  116. package/dist/extensions/org-config.controller.js +14 -6
  117. package/dist/extensions/org-config.d.ts +6 -0
  118. package/dist/extensions/org-config.js +12 -13
  119. package/dist/extensions/package.d.ts +14 -1
  120. package/dist/extensions/package.js +51 -2
  121. package/dist/extensions/permission-view.d.ts +3 -0
  122. package/dist/extensions/point-defaults.controller.d.ts +34 -0
  123. package/dist/extensions/point-defaults.controller.js +71 -0
  124. package/dist/extensions/point-defaults.d.ts +64 -0
  125. package/dist/extensions/point-defaults.js +206 -0
  126. package/dist/extensions/registry.d.ts +1 -1
  127. package/dist/extensions/registry.js +2 -0
  128. package/dist/http/api.module.js +8 -0
  129. package/dist/http/nest-logger.d.ts +6 -5
  130. package/dist/http/nest-logger.js +24 -10
  131. package/dist/identity/auth.controller.js +9 -5
  132. package/dist/identity/cookies.d.ts +20 -5
  133. package/dist/identity/cookies.js +44 -6
  134. package/dist/identity/me.controller.d.ts +5 -1
  135. package/dist/identity/me.controller.js +12 -3
  136. package/dist/identity/sessions.d.ts +12 -0
  137. package/dist/identity/sessions.js +45 -0
  138. package/dist/identity/settings.js +1 -1
  139. package/dist/index.d.ts +1 -1
  140. package/dist/infra/config/kernel-config.service.js +6 -0
  141. package/dist/infra/database/database.module.js +1 -0
  142. package/dist/infra/events/kernel-events.d.ts +8 -0
  143. package/dist/infra/lifecycle/process-guards.js +4 -0
  144. package/dist/infra/logging/logging.module.d.ts +4 -2
  145. package/dist/infra/logging/logging.module.js +24 -6
  146. package/dist/infra/queue/redis-connection.d.ts +6 -0
  147. package/dist/infra/queue/redis-connection.js +7 -0
  148. package/dist/infra/redis/redis.module.js +6 -2
  149. package/dist/infra/tokens.d.ts +6 -1
  150. package/dist/infra/tokens.js +6 -1
  151. package/dist/lifecycle/kernel-lifecycle.js +5 -0
  152. package/dist/log.d.ts +13 -1
  153. package/dist/log.js +93 -17
  154. package/dist/models/purposes.d.ts +23 -0
  155. package/dist/models/purposes.js +31 -10
  156. package/dist/orgs/offboarding.d.ts +3 -1
  157. package/dist/orgs/offboarding.js +26 -16
  158. package/dist/orgs/settings.service.d.ts +7 -2
  159. package/dist/orgs/settings.service.js +15 -4
  160. package/dist/roles/worker.module.js +6 -0
  161. package/dist/runtime/context/build.d.ts +15 -13
  162. package/dist/runtime/context/build.js +45 -27
  163. package/dist/runtime/context/claude-tools.d.ts +3 -1
  164. package/dist/runtime/context/claude-tools.js +12 -12
  165. package/dist/runtime/context/compaction.js +4 -0
  166. package/dist/runtime/context/legacy-text.d.ts +22 -0
  167. package/dist/runtime/context/legacy-text.js +37 -0
  168. package/dist/runtime/context/skills.d.ts +29 -11
  169. package/dist/runtime/context/skills.js +37 -30
  170. package/dist/runtime/langgraph-loop.js +3 -2
  171. package/dist/runtime/loop.d.ts +5 -4
  172. package/dist/runtime/maintenance.d.ts +4 -2
  173. package/dist/runtime/maintenance.js +17 -5
  174. package/dist/runtime/model-call.d.ts +17 -1
  175. package/dist/runtime/model-call.js +4 -3
  176. package/dist/runtime/providers.d.ts +6 -0
  177. package/dist/runtime/providers.js +8 -0
  178. package/dist/runtime/run-finish.d.ts +1 -1
  179. package/dist/runtime/run-queue.d.ts +28 -5
  180. package/dist/runtime/run-queue.js +131 -35
  181. package/dist/runtime/run-worker.d.ts +2 -2
  182. package/dist/runtime/run-worker.js +20 -7
  183. package/dist/runtime/runtime.module.js +8 -3
  184. package/dist/runtime/step-log.d.ts +6 -0
  185. package/dist/runtime/step-log.js +16 -2
  186. package/dist/runtime/subagents/definition.d.ts +1 -6
  187. package/dist/runtime/subagents/definition.js +14 -13
  188. package/dist/runtime/subagents/dispatch.d.ts +2 -2
  189. package/dist/runtime/subagents/dispatch.js +2 -2
  190. package/dist/runtime/subagents/org-agents.js +2 -1
  191. package/dist/runtime/subagents/roster.d.ts +1 -1
  192. package/dist/runtime/subagents/roster.js +1 -1
  193. package/dist/runtime/subagents/skill-fork.d.ts +2 -2
  194. package/dist/runtime/subagents/skill-fork.js +2 -2
  195. package/dist/runtime/subagents/subagents.service.d.ts +3 -3
  196. package/dist/runtime/subagents/subagents.service.js +3 -3
  197. package/dist/runtime/usage-report.d.ts +2 -1
  198. package/dist/runtime/usage-report.js +2 -1
  199. package/dist/runtime/waits.d.ts +13 -7
  200. package/dist/runtime/waits.js +42 -16
  201. package/dist/sandbox/sandbox.module.js +6 -1
  202. package/dist/sandbox/sandbox.service.d.ts +61 -3
  203. package/dist/sandbox/sandbox.service.js +141 -7
  204. package/dist/skills/catalog.d.ts +4 -4
  205. package/dist/skills/catalog.js +4 -4
  206. package/dist/skills/changes.service.d.ts +2 -2
  207. package/dist/skills/changes.service.js +5 -4
  208. package/dist/skills/invocation-keys.d.ts +1 -1
  209. package/dist/skills/invocation-keys.js +1 -1
  210. package/dist/skills/invoke.d.ts +1 -1
  211. package/dist/skills/invoke.js +5 -4
  212. package/dist/tools/approvals.service.d.ts +6 -0
  213. package/dist/tools/approvals.service.js +21 -1
  214. package/dist/tools/builtin-names.d.ts +91 -0
  215. package/dist/tools/builtin-names.js +143 -0
  216. package/dist/tools/builtins/agent.d.ts +7 -7
  217. package/dist/tools/builtins/agent.js +7 -11
  218. package/dist/tools/builtins/ask-user.d.ts +1 -1
  219. package/dist/tools/builtins/ask-user.js +2 -2
  220. package/dist/tools/builtins/builtin-tools.d.ts +17 -3
  221. package/dist/tools/builtins/builtin-tools.js +36 -4
  222. package/dist/tools/builtins/read-tool-output.d.ts +1 -1
  223. package/dist/tools/builtins/read-tool-output.js +2 -2
  224. package/dist/tools/builtins/sandbox/bash.d.ts +4 -4
  225. package/dist/tools/builtins/sandbox/bash.js +259 -29
  226. package/dist/tools/builtins/sandbox/common.d.ts +30 -7
  227. package/dist/tools/builtins/sandbox/common.js +9 -5
  228. package/dist/tools/builtins/sandbox/deliver.d.ts +1 -1
  229. package/dist/tools/builtins/sandbox/deliver.js +12 -12
  230. package/dist/tools/builtins/sandbox/edit.d.ts +1 -1
  231. package/dist/tools/builtins/sandbox/edit.js +41 -30
  232. package/dist/tools/builtins/sandbox/file-state.d.ts +34 -0
  233. package/dist/tools/builtins/sandbox/file-state.js +34 -0
  234. package/dist/tools/builtins/sandbox/glob-match.d.ts +1 -1
  235. package/dist/tools/builtins/sandbox/glob-match.js +1 -1
  236. package/dist/tools/builtins/sandbox/glob.d.ts +1 -1
  237. package/dist/tools/builtins/sandbox/glob.js +4 -4
  238. package/dist/tools/builtins/sandbox/grep.d.ts +7 -4
  239. package/dist/tools/builtins/sandbox/grep.js +147 -30
  240. package/dist/tools/builtins/sandbox/index.d.ts +3 -1
  241. package/dist/tools/builtins/sandbox/index.js +7 -1
  242. package/dist/tools/builtins/sandbox/read.d.ts +2 -2
  243. package/dist/tools/builtins/sandbox/read.js +21 -14
  244. package/dist/tools/builtins/sandbox/task-output.d.ts +63 -0
  245. package/dist/tools/builtins/sandbox/task-output.js +304 -0
  246. package/dist/tools/builtins/sandbox/task-stop.d.ts +10 -0
  247. package/dist/tools/builtins/sandbox/task-stop.js +112 -0
  248. package/dist/tools/builtins/sandbox/write.d.ts +1 -1
  249. package/dist/tools/builtins/sandbox/write.js +37 -33
  250. package/dist/tools/builtins/skill-load.d.ts +3 -3
  251. package/dist/tools/builtins/skill-load.js +32 -16
  252. package/dist/tools/builtins/skill-write.d.ts +4 -4
  253. package/dist/tools/builtins/skill-write.js +10 -10
  254. package/dist/tools/builtins/tool-search.d.ts +13 -9
  255. package/dist/tools/builtins/tool-search.js +25 -14
  256. package/dist/tools/builtins/web/errors.d.ts +8 -0
  257. package/dist/tools/builtins/web/errors.js +53 -0
  258. package/dist/tools/builtins/web/extract.d.ts +62 -0
  259. package/dist/tools/builtins/web/extract.js +267 -0
  260. package/dist/tools/builtins/web/fetch.d.ts +55 -0
  261. package/dist/tools/builtins/web/fetch.js +174 -0
  262. package/dist/tools/builtins/web/index.d.ts +13 -0
  263. package/dist/tools/builtins/web/index.js +12 -0
  264. package/dist/tools/builtins/web/page-cache.d.ts +25 -0
  265. package/dist/tools/builtins/web/page-cache.js +77 -0
  266. package/dist/tools/builtins/web/search.d.ts +25 -0
  267. package/dist/tools/builtins/web/search.js +209 -0
  268. package/dist/tools/builtins/web/url.d.ts +17 -0
  269. package/dist/tools/builtins/web/url.js +43 -0
  270. package/dist/tools/connectors.service.js +4 -4
  271. package/dist/tools/display-names.d.ts +9 -0
  272. package/dist/tools/display-names.js +24 -0
  273. package/dist/tools/drift.js +5 -5
  274. package/dist/tools/elicitation.js +1 -1
  275. package/dist/tools/info.d.ts +20 -4
  276. package/dist/tools/info.js +55 -32
  277. package/dist/tools/legacy-data.d.ts +34 -0
  278. package/dist/tools/legacy-data.js +76 -0
  279. package/dist/tools/legacy-keys.d.ts +51 -0
  280. package/dist/tools/legacy-keys.js +115 -0
  281. package/dist/tools/naming.d.ts +1 -5
  282. package/dist/tools/naming.js +1 -7
  283. package/dist/tools/oauth/flow-paths.d.ts +1 -1
  284. package/dist/tools/oauth/flow-paths.js +1 -1
  285. package/dist/tools/outputs.js +1 -1
  286. package/dist/tools/pipeline.d.ts +13 -7
  287. package/dist/tools/pipeline.js +112 -39
  288. package/dist/tools/policy.d.ts +1 -14
  289. package/dist/tools/policy.js +13 -19
  290. package/dist/tools/questions.controller.d.ts +1 -1
  291. package/dist/tools/questions.controller.js +1 -1
  292. package/dist/tools/questions.service.d.ts +1 -1
  293. package/dist/tools/registry.d.ts +38 -24
  294. package/dist/tools/registry.js +54 -47
  295. package/dist/tools/resources.d.ts +1 -1
  296. package/dist/tools/resources.js +1 -1
  297. package/dist/tools/tools.module.js +13 -1
  298. package/dist/tools/validate.js +1 -1
  299. package/package.json +3 -3
  300. package/src/migrations/0063_tool_keys_claude_code.sql +220 -0
  301. package/src/migrations/0064_account_last_org.sql +7 -0
  302. package/src/migrations/0065_conversation_archive.sql +8 -0
  303. package/src/migrations/0066_extension_display_name.sql +4 -0
  304. package/src/migrations/0067_agent_profiles.sql +20 -0
  305. package/src/migrations/0068_agent_avatar_uploads.sql +7 -0
  306. package/src/migrations/0069_attachments_agent_idx.sql +3 -0
  307. package/src/migrations/0070_background_tasks.sql +37 -0
  308. package/src/migrations/0071_background_tasks_reserve.sql +6 -0
  309. package/src/migrations/0072_background_tasks_stopping.sql +5 -0
  310. package/src/migrations/0073_org_extension_point_defaults.sql +11 -0
  311. package/dist/tools/builtins/sandbox/names.d.ts +0 -7
  312. package/dist/tools/builtins/sandbox/names.js +0 -20
@@ -0,0 +1,77 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { createHash } from "node:crypto";
11
+ import { Inject, Injectable } from "@nestjs/common";
12
+ import { CacheService } from "../../../infra/cache/cache.service.js";
13
+ import { KernelConfigService } from "../../../infra/config/kernel-config.service.js";
14
+ /**
15
+ * The cache key of a fetched page: per org, provider and URL (the URL hashed, it may be long).
16
+ * `CacheService` stores it under `cache:<key>`.
17
+ */
18
+ export const webPageKey = (orgId, provider, url) => `web_page:${orgId}:${provider}:${createHash("sha256").update(url).digest("hex")}`;
19
+ /** A stored value read back as a page, or null when it is not one. */
20
+ function asPage(raw) {
21
+ let v;
22
+ try {
23
+ v = JSON.parse(raw);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ if (v === null || typeof v !== "object")
29
+ return null;
30
+ const p = v;
31
+ if (typeof p.finalUrl !== "string" || typeof p.markdown !== "string")
32
+ return null;
33
+ return {
34
+ finalUrl: p.finalUrl,
35
+ markdown: p.markdown,
36
+ ...(typeof p.title === "string" && { title: p.title }),
37
+ ...(typeof p.contentType === "string" && { contentType: p.contentType }),
38
+ };
39
+ }
40
+ /**
41
+ * The `WebFetch` page cache (web tools spec §10): the provider's page (never the extraction,
42
+ * which depends on the prompt) in Redis under {@link webPageKey}, for `web.fetch.cacheSeconds`
43
+ * (read at every call; 0 turns the cache off). It saves the org's provider quota when an agent
44
+ * reads the same page twice. Through `CacheService`, so it fails open: a Redis error is logged and
45
+ * behaves like a miss or a skipped write. (Not named `*Cache`: that name is `infra/cache`'s.)
46
+ */
47
+ let WebPages = class WebPages {
48
+ cache;
49
+ config;
50
+ constructor(cache, config) {
51
+ this.cache = cache;
52
+ this.config = config;
53
+ }
54
+ ttl() {
55
+ return this.config.get().web.fetch.cacheSeconds;
56
+ }
57
+ /** The cached page of `url`, or null on a miss, when the cache is off or on a Redis error. */
58
+ async get(orgId, provider, url) {
59
+ if (this.ttl() <= 0)
60
+ return null;
61
+ const raw = await this.cache.get(webPageKey(orgId, provider, url));
62
+ return raw === undefined ? null : asPage(raw);
63
+ }
64
+ /** Caches `page` for `url`; nothing when the cache is off. */
65
+ async set(orgId, provider, url, page) {
66
+ const ttl = this.ttl();
67
+ if (ttl <= 0)
68
+ return;
69
+ await this.cache.setJson(webPageKey(orgId, provider, url), page, ttl);
70
+ }
71
+ };
72
+ WebPages = __decorate([
73
+ Injectable(),
74
+ __param(0, Inject(CacheService)),
75
+ __param(1, Inject(KernelConfigService))
76
+ ], WebPages);
77
+ export { WebPages };
@@ -0,0 +1,25 @@
1
+ import { type ToolSpec } from "@capybara-hq/plugin-sdk";
2
+ import type { ExtensionPointDefaults } from "../../../extensions/point-defaults.js";
3
+ import type { KernelConfigService } from "../../../infra/config/kernel-config.service.js";
4
+ import type { BuiltinTool } from "../../registry.js";
5
+ /**
6
+ * The fixed spec (web tools spec §3.1). No date and nothing per org or provider: `specHash` stays
7
+ * the same across days and providers, so remembered grants survive both and replays match the log.
8
+ * The date is the first line of every result instead.
9
+ */
10
+ export declare const WEB_SEARCH_SPEC: ToolSpec;
11
+ /** What {@link webSearchTool} needs. */
12
+ export interface WebSearchDeps {
13
+ points: Pick<ExtensionPointDefaults, "resolve">;
14
+ config: Pick<KernelConfigService, "get">;
15
+ /** The clock of the result's date line (tests fix it). */
16
+ now?: () => Date;
17
+ }
18
+ /**
19
+ * The builtin `WebSearch` (web tools spec §3.1): searches with the org's `web-search@1` provider
20
+ * (`ExtensionPointDefaults`, resolved per call and per step, never cached), asking it for
21
+ * `web.search.maxResults` results with the domain lists as hints and the org's config, then
22
+ * filters the results again. Default `ask`; unavailable when the org has no provider or several
23
+ * without a default.
24
+ */
25
+ export declare function webSearchTool(deps: WebSearchDeps): BuiltinTool;
@@ -0,0 +1,209 @@
1
+ import { domainToASCII } from "node:url";
2
+ import { ToolError, } from "@capybara-hq/plugin-sdk";
3
+ import { BUILTIN } from "../../builtin-names.js";
4
+ import { webToolError } from "./errors.js";
5
+ /** The extension point name (without the major) the provider is resolved from. */
6
+ const POINT = "web-search";
7
+ /**
8
+ * The fixed spec (web tools spec §3.1). No date and nothing per org or provider: `specHash` stays
9
+ * the same across days and providers, so remembered grants survive both and replays match the log.
10
+ * The date is the first line of every result instead.
11
+ */
12
+ export const WEB_SEARCH_SPEC = {
13
+ name: BUILTIN.WebSearch,
14
+ // Display only (the tool row; not in the fingerprint, never sent to the model).
15
+ keyArgs: ["query"],
16
+ description: "Search the web and get a list of results with title, URL and a short snippet. Use it for " +
17
+ "information newer than your training data, or to check whether something you know is still " +
18
+ "current. Then use WebFetch to read the pages you need. Do not use it for questions you can " +
19
+ "answer reliably without the web. Cite the sources you use.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ query: {
24
+ type: "string",
25
+ minLength: 2,
26
+ maxLength: 500,
27
+ description: "The search query.",
28
+ },
29
+ allowed_domains: {
30
+ type: "array",
31
+ items: { type: "string", minLength: 1, maxLength: 253 },
32
+ maxItems: 50,
33
+ description: "Only return results from these domains (hostnames such as example.com; subdomains " +
34
+ "are included). Do not combine with blocked_domains.",
35
+ },
36
+ blocked_domains: {
37
+ type: "array",
38
+ items: { type: "string", minLength: 1, maxLength: 253 },
39
+ maxItems: 50,
40
+ description: "Never return results from these domains (hostnames such as example.com; subdomains " +
41
+ "are included). Do not combine with allowed_domains.",
42
+ },
43
+ },
44
+ required: ["query"],
45
+ additionalProperties: false,
46
+ },
47
+ annotations: { readOnlyHint: true, openWorldHint: true },
48
+ };
49
+ /** The last line of a result with entries (Claude Code tells the model to list its sources). */
50
+ const CITE_LINE = "Cite the sources you use as markdown links in your answer.";
51
+ /** Why the tool is unavailable in an org (web tools spec §3.4; English, actionable). */
52
+ function unavailableText(reason) {
53
+ if (reason === "none") {
54
+ return "No web search provider is enabled in this organization; ask an org admin to enable one.";
55
+ }
56
+ return "Several web search providers are enabled and none is the default; ask an org admin to choose one in the organization settings.";
57
+ }
58
+ /**
59
+ * A domain entry as a bare lowercase ASCII hostname: tolerates a scheme, a path, a port, a leading
60
+ * `*.` / `.` and a trailing dot; a Unicode (IDN) name becomes its punycode form, as result hosts
61
+ * are (WHATWG URL parsing). Empty when nothing is left.
62
+ */
63
+ function normalizeDomain(entry) {
64
+ let d = entry.trim().toLowerCase();
65
+ d = d.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
66
+ d = d.replace(/[/?#].*$/, "").replace(/:\d+$/, "");
67
+ d = d.replace(/^\*\./, "").replace(/^\.+/, "").replace(/\.+$/, "");
68
+ if (d === "")
69
+ return d;
70
+ // domainToASCII is "" for a name IDNA rejects: keep the entry as given (it matches no host)
71
+ return domainToASCII(d) || d;
72
+ }
73
+ /**
74
+ * A domain list normalized; undefined when absent or empty (no filter). A list whose entries all
75
+ * normalize to nothing stays an empty list, which no host matches.
76
+ */
77
+ const normalizeDomains = (list) => list === undefined || list.length === 0
78
+ ? undefined
79
+ : [...new Set(list.map(normalizeDomain).filter((d) => d !== ""))];
80
+ /** Whether `host` is `domain` or one of its subdomains. */
81
+ const covers = (domain, host) => host === domain || host.endsWith(`.${domain}`);
82
+ const oneLine = (s) => s.replace(/\s+/g, " ").trim();
83
+ /**
84
+ * The provider's results the model may see (web tools spec §7.2): an absolute http(s) URL, a
85
+ * non-empty title, each URL once, within the domain lists, at most `max`. The kernel filters even
86
+ * when the provider honored the domain hints.
87
+ */
88
+ function usable(results, allowed, blocked, max) {
89
+ const seen = new Set();
90
+ const out = [];
91
+ for (const r of results) {
92
+ if (out.length >= max)
93
+ break;
94
+ if (typeof r?.url !== "string" || typeof r.title !== "string")
95
+ continue;
96
+ const title = oneLine(r.title);
97
+ if (title === "")
98
+ continue;
99
+ let u;
100
+ try {
101
+ // no base: a relative URL throws
102
+ u = new URL(r.url.trim());
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ if (u.protocol !== "https:" && u.protocol !== "http:")
108
+ continue;
109
+ const host = u.hostname.toLowerCase().replace(/\.$/, "");
110
+ if (host === "")
111
+ continue;
112
+ if (allowed !== undefined && !allowed.some((d) => covers(d, host)))
113
+ continue;
114
+ if (blocked?.some((d) => covers(d, host)))
115
+ continue;
116
+ if (seen.has(u.href))
117
+ continue;
118
+ seen.add(u.href);
119
+ const date = typeof r.publishedAt === "string" ? /^\d{4}-\d{2}-\d{2}/.exec(r.publishedAt.trim()) : null;
120
+ const snippet = typeof r.snippet === "string" ? oneLine(r.snippet) : "";
121
+ out.push({
122
+ title,
123
+ url: u.href,
124
+ ...(date !== null && { date: date[0] }),
125
+ ...(snippet !== "" && { snippet }),
126
+ });
127
+ }
128
+ return out;
129
+ }
130
+ /** The model-visible result (web tools spec §3.1): the date line first, then the entries. */
131
+ function resultText(day, entries) {
132
+ const head = `Search date: ${day} (UTC).`;
133
+ if (entries.length === 0)
134
+ return `${head}\n\nNo results.`;
135
+ const blocks = entries.map((e, i) => [
136
+ `${i + 1}. ${e.title}`,
137
+ ` URL: ${e.url}`,
138
+ ...(e.date !== undefined ? [` Published: ${e.date}`] : []),
139
+ ...(e.snippet !== undefined ? [` ${e.snippet}`] : []),
140
+ ].join("\n"));
141
+ return [head, ...blocks, CITE_LINE].join("\n\n");
142
+ }
143
+ /** The query in the display summary: one line, at most this many code points. */
144
+ const DISPLAY_QUERY_MAX = 120;
145
+ function displayQuery(query) {
146
+ const cps = [...oneLine(query)];
147
+ return cps.length > DISPLAY_QUERY_MAX
148
+ ? `${cps.slice(0, DISPLAY_QUERY_MAX - 1).join("")}…`
149
+ : cps.join("");
150
+ }
151
+ /**
152
+ * The builtin `WebSearch` (web tools spec §3.1): searches with the org's `web-search@1` provider
153
+ * (`ExtensionPointDefaults`, resolved per call and per step, never cached), asking it for
154
+ * `web.search.maxResults` results with the domain lists as hints and the org's config, then
155
+ * filters the results again. Default `ask`; unavailable when the org has no provider or several
156
+ * without a default.
157
+ */
158
+ export function webSearchTool(deps) {
159
+ const now = deps.now ?? (() => new Date());
160
+ return {
161
+ spec: WEB_SEARCH_SPEC,
162
+ defaultPolicy: "ask",
163
+ async unavailable({ orgId }) {
164
+ const r = await deps.points.resolve(orgId, POINT);
165
+ return r.ok ? null : unavailableText(r.reason);
166
+ },
167
+ async run(input, ctx) {
168
+ const { query, allowed_domains, blocked_domains } = input;
169
+ if (allowed_domains !== undefined && blocked_domains !== undefined) {
170
+ throw new ToolError("Pass either allowed_domains or blocked_domains, not both; call WebSearch again with one of them.");
171
+ }
172
+ const allowed = normalizeDomains(allowed_domains);
173
+ const blocked = normalizeDomains(blocked_domains);
174
+ // the provider may have changed since the step resolved the tool: resolve again
175
+ const resolved = await deps.points.resolve(ctx.orgId, POINT);
176
+ if (!resolved.ok)
177
+ throw new ToolError(unavailableText(resolved.reason));
178
+ const max = deps.config.get().web.search.maxResults;
179
+ const day = now().toISOString().slice(0, 10);
180
+ let results;
181
+ try {
182
+ const out = await resolved.impl.search({
183
+ query,
184
+ ...(allowed !== undefined && allowed.length > 0 && { allowedDomains: allowed }),
185
+ ...(blocked !== undefined && blocked.length > 0 && { blockedDomains: blocked }),
186
+ maxResults: max,
187
+ orgConfig: resolved.orgConfig,
188
+ signal: ctx.signal,
189
+ });
190
+ results = Array.isArray(out?.results) ? out.results : [];
191
+ }
192
+ catch (e) {
193
+ webToolError(e, "search");
194
+ }
195
+ const entries = usable(results, allowed, blocked, max);
196
+ const q = displayQuery(query);
197
+ const n = entries.length;
198
+ return {
199
+ content: [{ type: "text", text: resultText(day, entries) }],
200
+ display: {
201
+ summary: {
202
+ zh: `搜索网页:${q}(${n} 条结果)`,
203
+ en: `Searched the web: ${q} (${n} result${n === 1 ? "" : "s"})`,
204
+ },
205
+ },
206
+ };
207
+ },
208
+ };
209
+ }
@@ -0,0 +1,17 @@
1
+ /** The longest URL `WebFetch` accepts, in characters (Claude Code's limit; web tools spec §7.1). */
2
+ export declare const MAX_FETCH_URL_CHARS = 2000;
3
+ /** The outcome of {@link normalizeFetchUrl}: the URL to fetch, or a model-visible error. */
4
+ export type FetchUrlCheck = {
5
+ ok: true;
6
+ url: string;
7
+ } | {
8
+ ok: false;
9
+ error: string;
10
+ };
11
+ /**
12
+ * A `WebFetch` URL checked and normalized (web tools spec §7.1): absolute http(s), `http:`
13
+ * upgraded to `https:`, no user name or password, at most {@link MAX_FETCH_URL_CHARS} characters
14
+ * (before and after normalization), and a host that can name a public internet host. Pure, no DNS: a public name that resolves to a
15
+ * private address is the provider's to refuse. Errors are English and tell the model what to pass.
16
+ */
17
+ export declare function normalizeFetchUrl(raw: string): FetchUrlCheck;
@@ -0,0 +1,43 @@
1
+ import { isPublicHostname } from "@capybara-hq/plugin-sdk";
2
+ /** The longest URL `WebFetch` accepts, in characters (Claude Code's limit; web tools spec §7.1). */
3
+ export const MAX_FETCH_URL_CHARS = 2000;
4
+ const fail = (error) => ({ ok: false, error });
5
+ /**
6
+ * A `WebFetch` URL checked and normalized (web tools spec §7.1): absolute http(s), `http:`
7
+ * upgraded to `https:`, no user name or password, at most {@link MAX_FETCH_URL_CHARS} characters
8
+ * (before and after normalization), and a host that can name a public internet host. Pure, no DNS: a public name that resolves to a
9
+ * private address is the provider's to refuse. Errors are English and tell the model what to pass.
10
+ */
11
+ export function normalizeFetchUrl(raw) {
12
+ const input = raw.trim();
13
+ // a cheap guard before parsing; the normalized URL is checked again below
14
+ if (input.length > MAX_FETCH_URL_CHARS) {
15
+ return fail(`Pass a URL of at most ${MAX_FETCH_URL_CHARS} characters.`);
16
+ }
17
+ let u;
18
+ try {
19
+ // no base: a relative URL throws
20
+ u = new URL(input);
21
+ }
22
+ catch {
23
+ return fail("Pass a full absolute URL, such as https://example.com/page.");
24
+ }
25
+ if (u.protocol !== "https:" && u.protocol !== "http:") {
26
+ return fail("Pass an https URL; only http and https URLs can be fetched.");
27
+ }
28
+ if (u.username !== "" || u.password !== "") {
29
+ return fail("Pass the URL without a user name or password.");
30
+ }
31
+ u.protocol = "https:";
32
+ // `u.hostname` is WHATWG-normalized (lowercased, IDNA, numeric IPv4 forms such as `0x7f.0.0.1`
33
+ // rewritten to dotted decimal, IPv6 in brackets), which is what isPublicHostname expects: never
34
+ // pass it a hand-parsed host.
35
+ if (!isPublicHostname(u.hostname)) {
36
+ return fail("Pass a full public https URL; this host is private or local.");
37
+ }
38
+ // the limit is on the URL as fetched: the upgrade, IDNA and percent-encoding can lengthen it
39
+ if (u.href.length > MAX_FETCH_URL_CHARS) {
40
+ return fail(`Pass a URL of at most ${MAX_FETCH_URL_CHARS} characters.`);
41
+ }
42
+ return { ok: true, url: u.href };
43
+ }
@@ -8,7 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { randomUUID } from "node:crypto";
11
- import { ToolError, } from "@capybara-hq/plugin-sdk";
11
+ import { connectorToolKey, ToolError, } from "@capybara-hq/plugin-sdk";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql } from "kysely";
14
14
  import { z } from "zod";
@@ -25,7 +25,7 @@ import { OrgLock } from "../orgs/org-lock.js";
25
25
  import { CredentialMissing, Credentials, isOAuth, } from "./credentials.js";
26
26
  import { snapshot, ToolDrift, toolIssue, } from "./drift.js";
27
27
  import { endpointCredentialIssues, endpointView } from "./endpoint.js";
28
- import { deriveSlug, isValidSlug, SLUG_RULE, toolKey } from "./naming.js";
28
+ import { deriveSlug, isValidSlug, SLUG_RULE } from "./naming.js";
29
29
  import { checkOAuthUrl, discoverMcp, OAuthSetupError, oauthHttpOptions, pickAuthMethod, registerClient, } from "./oauth/discovery.js";
30
30
  import { callbackUrl, publicUrlRequired } from "./oauth/flow-paths.js";
31
31
  import { OAuthGrantError, OAuthUnavailable, tokenRequest, } from "./oauth/tokens.js";
@@ -619,7 +619,7 @@ let ConnectorsService = class ConnectorsService {
619
619
  issues = snap.issues;
620
620
  tools = snap.rows.map((r) => ({
621
621
  name: r.name,
622
- key: toolKey(slug, r.name),
622
+ key: connectorToolKey(slug, r.name),
623
623
  description: r.spec.description,
624
624
  inputSchema: r.spec.inputSchema,
625
625
  annotations: r.spec.annotations ?? null,
@@ -720,7 +720,7 @@ let ConnectorsService = class ConnectorsService {
720
720
  const next = t.pending_spec;
721
721
  return {
722
722
  name: t.name,
723
- key: toolKey(r.slug, t.name),
723
+ key: connectorToolKey(r.slug, t.name),
724
724
  description: spec.description,
725
725
  inputSchema: spec.inputSchema,
726
726
  annotations: spec.annotations ?? null,
@@ -0,0 +1,9 @@
1
+ import type { LocalizedName } from "@capybara-hq/plugin-sdk";
2
+ import type { KernelSchemaDb } from "../db/kernel-schema.js";
3
+ /**
4
+ * The display names of plugins, for naming their extensions' tools (`<display name> [<tool>]`,
5
+ * tool naming spec §7): the live catalog item's `displayName`, else the plugin name in both
6
+ * languages (a bootstrap or uploaded plugin without a catalog item, or one without a display name).
7
+ * Display only.
8
+ */
9
+ export declare function pluginDisplayNames(db: KernelSchemaDb, names: readonly string[]): Promise<Map<string, LocalizedName>>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The display names of plugins, for naming their extensions' tools (`<display name> [<tool>]`,
3
+ * tool naming spec §7): the live catalog item's `displayName`, else the plugin name in both
4
+ * languages (a bootstrap or uploaded plugin without a catalog item, or one without a display name).
5
+ * Display only.
6
+ */
7
+ export async function pluginDisplayNames(db, names) {
8
+ const unique = [...new Set(names)];
9
+ const out = new Map(unique.map((n) => [n, { zh: n, en: n }]));
10
+ if (unique.length === 0)
11
+ return out;
12
+ const rows = await db
13
+ .selectFrom("catalog_items")
14
+ .select(["name", "display_name"])
15
+ .where("name", "in", unique)
16
+ .where("deleted_at", "is", null)
17
+ .execute();
18
+ for (const r of rows) {
19
+ const d = r.display_name;
20
+ if (d && typeof d.zh === "string" && typeof d.en === "string" && d.zh !== "" && d.en !== "")
21
+ out.set(r.name, { zh: d.zh, en: d.en });
22
+ }
23
+ return out;
24
+ }
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { TOOL_TITLE_MAX, ToolError, validateToolPresentation, } from "@capybara-hq/plugin-sdk";
10
+ import { connectorToolKey, TOOL_TITLE_MAX, ToolError, validateToolPresentation, } from "@capybara-hq/plugin-sdk";
11
11
  import { getQueueToken } from "@nestjs/bullmq";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql } from "kysely";
@@ -21,7 +21,7 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
21
21
  import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
22
22
  import { HIDDEN_FORMAT_RE } from "../text.js";
23
23
  import { CredentialMissing, Credentials, isOAuth } from "./credentials.js";
24
- import { toolKey, toolKeyIssue } from "./naming.js";
24
+ import { toolKeyIssue } from "./naming.js";
25
25
  import { redactDeep, redactorFor } from "./redact.js";
26
26
  import { canonicalJson, policyFromAnnotations, specHash, ToolRegistry } from "./registry.js";
27
27
  import { resourceToolSpecs } from "./resources.js";
@@ -105,7 +105,7 @@ export function snapshot(slug, specs, resources) {
105
105
  ...(isObj(s.connectorData) && { connectorData: s.connectorData }),
106
106
  ...displayFields(s),
107
107
  };
108
- let issue = toolKeyIssue(toolKey(slug, name));
108
+ let issue = toolKeyIssue(connectorToolKey(slug, name));
109
109
  if (!issue && !isObj(s.inputSchema))
110
110
  issue = "Tool inputSchema must be an object";
111
111
  if (issue)
@@ -122,7 +122,7 @@ export function snapshot(slug, specs, resources) {
122
122
  });
123
123
  continue;
124
124
  }
125
- const issue = toolKeyIssue(toolKey(slug, spec.name));
125
+ const issue = toolKeyIssue(connectorToolKey(slug, spec.name));
126
126
  if (issue)
127
127
  issues.push({ name: spec.name, reason: issue });
128
128
  rows.push({ name: spec.name, spec, specHash: specHash(spec), issue, source: "resources" });
@@ -172,7 +172,7 @@ const displayOf = (spec) => {
172
172
  };
173
173
  /** 工具当前不能启用的原因(视图、启用与复核校验共用)。 */
174
174
  export function toolIssue(slug, spec) {
175
- const keyIssue = toolKeyIssue(toolKey(slug, spec.name));
175
+ const keyIssue = toolKeyIssue(connectorToolKey(slug, spec.name));
176
176
  if (keyIssue)
177
177
  return keyIssue;
178
178
  if (!isObj(spec.inputSchema))
@@ -33,7 +33,7 @@ export class ElicitationSuspended extends Error {
33
33
  }
34
34
  /** 不能展示的 elicitation 被拒绝时附在工具结果后的说明(模型可见,英文)。 */
35
35
  export const UNSUPPORTED_ELICITATION_NOTE = "The app asked the user for input that cannot be shown in this client, so the request was declined";
36
- const unsupportedNote = (why) => `[${UNSUPPORTED_ELICITATION_NOTE} (${why}). If the call needs that input, ask the user for it with ask_user and pass it as tool arguments, or try another approach.]`;
36
+ const unsupportedNote = (why) => `[${UNSUPPORTED_ELICITATION_NOTE} (${why}). If the call needs that input, ask the user for it with AskUserQuestion and pass it as tool arguments, or try another approach.]`;
37
37
  /** URL 模式表单里「已完成」开关的字段名。 */
38
38
  export const URL_COMPLETED_FIELD = "completed";
39
39
  /** 表单标题(远端给的 message)最多字符数。 */
@@ -1,3 +1,4 @@
1
+ import { type LocalizedName } from "@capybara-hq/plugin-sdk";
1
2
  import { ConversationAccess } from "../conversations/access.js";
2
3
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
3
4
  import { KernelDb } from "../infra/database/kernel-db.js";
@@ -11,16 +12,32 @@ export type ToolKind = "command" | "read" | "edit" | "search" | "connector" | "m
11
12
  export interface ToolInfo {
12
13
  /** The model-visible tool key, as asked for. */
13
14
  key: string;
15
+ /**
16
+ * Set only for a release-candidate builtin key (old history, tool naming spec §9): the current
17
+ * tool it stands for. Everything else describes that tool, with `keyArgs` in the old parameter
18
+ * names the logged input carries. Pages map a call's name through it (the `Agent` row, labels).
19
+ */
20
+ aliasOf?: string;
14
21
  /** The declared display title (`ToolSpec.title`), or null. */
15
22
  title: string | null;
16
23
  kind: ToolKind;
24
+ /** True exactly for the builtin tools that run in the conversation's sandbox. */
25
+ sandbox: boolean;
17
26
  /** The declared key arguments (`ToolSpec.keyArgs`), or []. */
18
27
  keyArgs: string[];
28
+ /**
29
+ * The local tool name parsed from a connector or extension key (`mcp__crm__delete` -> `delete`),
30
+ * shown in brackets after the source's display name; null for builtins and unparsable keys.
31
+ */
32
+ tool: string | null;
19
33
  source: {
20
34
  kind: "builtin";
21
- } | {
35
+ }
36
+ /** `displayName`: the plugin's catalog display name, else its name. */
37
+ | {
22
38
  kind: "extension";
23
39
  name: string;
40
+ displayName: LocalizedName;
24
41
  } | {
25
42
  kind: "connector";
26
43
  id: string;
@@ -31,8 +48,7 @@ export interface ToolInfo {
31
48
  kind: "unknown";
32
49
  };
33
50
  }
34
- /** Kinds of the kernel builtin tools; a builtin missing here is a plain `tool`. */
35
- export declare const BUILTIN_TOOL_KINDS: Readonly<Record<string, ToolKind>>;
51
+ export { BUILTIN_TOOL_KINDS } from "./builtin-names.js";
36
52
  /** At most this many keys per request, each at most {@link TOOL_INFO_KEY_MAX} characters. */
37
53
  export declare const TOOL_INFO_MAX_KEYS = 100;
38
54
  export declare const TOOL_INFO_KEY_MAX = 200;
@@ -42,7 +58,7 @@ export interface ToolInfoDeps {
42
58
  }
43
59
  /**
44
60
  * Presentation metadata for `keys` in this org, in request order with repeats dropped. Builtins
45
- * by name; `<prefix>__<tool>` keys first as a loaded extension's registration, then as a live
61
+ * by name (a release-candidate builtin key as its current tool, with `aliasOf`); `plugin__<plugin>__<tool>` keys as a loaded extension's registration, and `mcp__<slug>__<tool>` keys as a live
46
62
  * connector's current stored tool (its reviewed definition, never a pending one). Anything else
47
63
  * comes back as an unknown plain tool.
48
64
  *