- Coordinator tools in its own chat (super admin only): list_extensions,
set_extension_access, delete_extension, and update_skill /
update_mcp_server / update_custom_tool for what can't change where
anything is sent (on/off, names, arguments, left-out tools)
- Adding a skill, MCP server or tool, or changing a skill's words, a
server's URL or headers or a tool's kind or config, goes through
propose_skill / propose_mcp_server / propose_custom_tool: stored in
extension_proposals with the bot's background check, shown as a
[proposal:ID] chip that opens a card with Confirm and Reject
- The card's facts are worked out by the app from what Confirm saves: the
host, every {{env.KEY}} and whether it's saved, the config; for a skill
its links, hosts, tool names, mentions of secrets and a diff against
the saved runbook
- Confirming decides once and checks again; a server or tool missing a key
is saved switched off, and the outcome is posted to the bot in chat
- Never from a bot: stdio servers, a URL whose host a template decides,
envs in a prompt tool, a key's value
Co-Authored-By: Claude Opus 5.5 <[email protected]>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import literalUrlHost from "./literal-url-host";
|
|
|
|
type Params = {
|
|
text: string;
|
|
};
|
|
|
|
const UrlPattern = /https?:\/\/[^\s)<>\]"'`]+/gi;
|
|
const ToolPattern = /\b[a-z][a-z0-9]*(?:__?[a-z0-9]+)+\b/g;
|
|
const SecretPatterns = [
|
|
/\{\{\s*env\s*\.[^}]*\}\}/gi,
|
|
/~?\/?\.cobble\/[\w.\/-]*/gi,
|
|
/\benv\.json\b/gi,
|
|
/\$\{?[A-Z][A-Z0-9_]{2,}\}?/g,
|
|
/\b(?:process\.env|os\.environ)\b/g,
|
|
/\b(?:api[ _-]?keys?|access[ _-]?tokens?|tokens?|passwords?|secrets?|credentials?|cookies?|private[ _-]?keys?)\b/gi,
|
|
];
|
|
const MaxEach = 30;
|
|
|
|
/**
|
|
* What a skill's runbook would have bots reach for, read from its text
|
|
* by the app so the confirm card never depends on the bot's account of
|
|
* it: the links in it and their hosts, every snake_case name (what tools
|
|
* are called, e.g. `web_fetch_exa`, `exa_search__web_fetch_exa`), and
|
|
* anything that touches secrets (envs, `~/.cobble/env`, `$API_KEY`,
|
|
* words like token or password). Each list is deduplicated and capped.
|
|
*/
|
|
export default function findSkillMentions({ text }: Params) {
|
|
const urls = unique({ values: [...text.matchAll(UrlPattern)].map((m) => m[0].replace(/[.,;:!?]+$/, "")) });
|
|
const hosts = unique({
|
|
values: urls.map((url) => literalUrlHost({ url })).filter((host): host is string => Boolean(host)),
|
|
});
|
|
const tools = unique({ values: [...text.matchAll(ToolPattern)].map((m) => m[0]) });
|
|
const secrets = unique({
|
|
values: SecretPatterns.flatMap((pattern) => [...text.matchAll(pattern)].map((m) => m[0].trim())),
|
|
fold: true,
|
|
});
|
|
|
|
return { urls, hosts, tools, secrets };
|
|
}
|
|
|
|
function unique({ values, fold }: { values: string[]; fold?: boolean }) {
|
|
const seen = new Set<string>();
|
|
const kept: string[] = [];
|
|
|
|
for (const value of values) {
|
|
const id = fold ? value.toLowerCase() : value;
|
|
|
|
if (!value || seen.has(id)) continue;
|
|
|
|
seen.add(id);
|
|
kept.push(value);
|
|
}
|
|
|
|
return kept.slice(0, MaxEach);
|
|
}
|