verybot 0.1.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.
Potentially problematic release.
This version of verybot might be problematic. Click here for more details.
- package/dist/brain/agent-registry.d.ts +75 -0
- package/dist/brain/agent-registry.js +124 -0
- package/dist/brain/agent.d.ts +146 -0
- package/dist/brain/agent.js +680 -0
- package/dist/brain/channel-store.d.ts +27 -0
- package/dist/brain/channel-store.js +78 -0
- package/dist/brain/compaction.d.ts +37 -0
- package/dist/brain/compaction.js +214 -0
- package/dist/brain/context.d.ts +33 -0
- package/dist/brain/context.js +77 -0
- package/dist/brain/delegation-store.d.ts +33 -0
- package/dist/brain/delegation-store.js +106 -0
- package/dist/brain/loop.d.ts +21 -0
- package/dist/brain/loop.js +161 -0
- package/dist/brain/mcp-adapter.d.ts +39 -0
- package/dist/brain/mcp-adapter.js +227 -0
- package/dist/brain/memory-extractor.d.ts +26 -0
- package/dist/brain/memory-extractor.js +82 -0
- package/dist/brain/providers.d.ts +10 -0
- package/dist/brain/providers.js +69 -0
- package/dist/brain/queue.d.ts +18 -0
- package/dist/brain/queue.js +84 -0
- package/dist/brain/run-tools.d.ts +47 -0
- package/dist/brain/run-tools.js +84 -0
- package/dist/brain/session-key.d.ts +23 -0
- package/dist/brain/session-key.js +41 -0
- package/dist/brain/session-state.d.ts +36 -0
- package/dist/brain/session-state.js +51 -0
- package/dist/brain/session-store.d.ts +50 -0
- package/dist/brain/session-store.js +207 -0
- package/dist/brain/session.d.ts +32 -0
- package/dist/brain/session.js +75 -0
- package/dist/brain/utils.d.ts +4 -0
- package/dist/brain/utils.js +26 -0
- package/dist/brain/worker-coordinator.d.ts +25 -0
- package/dist/brain/worker-coordinator.js +83 -0
- package/dist/channels/commands.d.ts +35 -0
- package/dist/channels/commands.js +65 -0
- package/dist/channels/discord/channel.d.ts +18 -0
- package/dist/channels/discord/channel.js +154 -0
- package/dist/channels/discord/markdown.d.ts +19 -0
- package/dist/channels/discord/markdown.js +62 -0
- package/dist/channels/manager.d.ts +29 -0
- package/dist/channels/manager.js +100 -0
- package/dist/channels/slack/channel.d.ts +26 -0
- package/dist/channels/slack/channel.js +207 -0
- package/dist/channels/slack/markdown.d.ts +19 -0
- package/dist/channels/slack/markdown.js +62 -0
- package/dist/channels/specs.d.ts +21 -0
- package/dist/channels/specs.js +96 -0
- package/dist/channels/telegram/channel.d.ts +18 -0
- package/dist/channels/telegram/channel.js +156 -0
- package/dist/channels/telegram/markdown.d.ts +17 -0
- package/dist/channels/telegram/markdown.js +66 -0
- package/dist/channels/types.d.ts +26 -0
- package/dist/channels/types.js +1 -0
- package/dist/channels/whatsapp/channel.d.ts +23 -0
- package/dist/channels/whatsapp/channel.js +242 -0
- package/dist/channels/whatsapp/markdown.d.ts +20 -0
- package/dist/channels/whatsapp/markdown.js +51 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +78 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +13 -0
- package/dist/computer/browser/actions.d.ts +31 -0
- package/dist/computer/browser/actions.js +148 -0
- package/dist/computer/browser/manager.d.ts +55 -0
- package/dist/computer/browser/manager.js +496 -0
- package/dist/computer/browser/profile-badge.d.ts +13 -0
- package/dist/computer/browser/profile-badge.js +67 -0
- package/dist/computer/browser/screenshot.d.ts +5 -0
- package/dist/computer/browser/screenshot.js +21 -0
- package/dist/computer/browser/snapshot.d.ts +30 -0
- package/dist/computer/browser/snapshot.js +242 -0
- package/dist/computer/browser/tools.d.ts +5 -0
- package/dist/computer/browser/tools.js +167 -0
- package/dist/computer/desktop/adapter.d.ts +25 -0
- package/dist/computer/desktop/adapter.js +11 -0
- package/dist/computer/desktop/macos.d.ts +24 -0
- package/dist/computer/desktop/macos.js +223 -0
- package/dist/computer/desktop/tools.d.ts +25 -0
- package/dist/computer/desktop/tools.js +114 -0
- package/dist/config/agent-config.d.ts +41 -0
- package/dist/config/agent-config.js +14 -0
- package/dist/config/model-catalog.d.ts +22 -0
- package/dist/config/model-catalog.js +99 -0
- package/dist/config/store.d.ts +25 -0
- package/dist/config/store.js +143 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.js +224 -0
- package/dist/control-ui/assets/index-BANXNUyt.js +143 -0
- package/dist/control-ui/assets/index-BSUFrP9R.css +1 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
- package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
- package/dist/control-ui/index.html +14 -0
- package/dist/control-ui/vite.svg +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +11 -0
- package/dist/gateway/broadcast.d.ts +5 -0
- package/dist/gateway/broadcast.js +33 -0
- package/dist/gateway/methods/chat.d.ts +24 -0
- package/dist/gateway/methods/chat.js +19 -0
- package/dist/gateway/methods/config.d.ts +13 -0
- package/dist/gateway/methods/config.js +14 -0
- package/dist/gateway/methods/models.d.ts +10 -0
- package/dist/gateway/methods/models.js +14 -0
- package/dist/gateway/methods/prompt-templates.d.ts +23 -0
- package/dist/gateway/methods/prompt-templates.js +82 -0
- package/dist/gateway/methods/scheduler.d.ts +62 -0
- package/dist/gateway/methods/scheduler.js +129 -0
- package/dist/gateway/methods/sessions.d.ts +26 -0
- package/dist/gateway/methods/sessions.js +54 -0
- package/dist/gateway/methods/skills.d.ts +35 -0
- package/dist/gateway/methods/skills.js +202 -0
- package/dist/gateway/methods/system.d.ts +12 -0
- package/dist/gateway/methods/system.js +39 -0
- package/dist/gateway/methods/tasks.d.ts +21 -0
- package/dist/gateway/methods/tasks.js +46 -0
- package/dist/gateway/methods/teams.d.ts +70 -0
- package/dist/gateway/methods/teams.js +374 -0
- package/dist/gateway/methods/tools.d.ts +6 -0
- package/dist/gateway/methods/tools.js +7 -0
- package/dist/gateway/methods/whatsapp.d.ts +19 -0
- package/dist/gateway/methods/whatsapp.js +35 -0
- package/dist/gateway/rpc.d.ts +38 -0
- package/dist/gateway/rpc.js +75 -0
- package/dist/gateway/server.d.ts +4 -0
- package/dist/gateway/server.js +133 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +212 -0
- package/dist/integrations/github.d.ts +7 -0
- package/dist/integrations/github.js +133 -0
- package/dist/integrations/mcp.d.ts +7 -0
- package/dist/integrations/mcp.js +106 -0
- package/dist/integrations/registry.d.ts +43 -0
- package/dist/integrations/registry.js +258 -0
- package/dist/integrations/scanner.d.ts +10 -0
- package/dist/integrations/scanner.js +122 -0
- package/dist/integrations/twitter.d.ts +10 -0
- package/dist/integrations/twitter.js +120 -0
- package/dist/integrations/types.d.ts +72 -0
- package/dist/integrations/types.js +1 -0
- package/dist/logger.d.ts +16 -0
- package/dist/logger.js +104 -0
- package/dist/markdown/chunk.d.ts +9 -0
- package/dist/markdown/chunk.js +52 -0
- package/dist/markdown/ir.d.ts +37 -0
- package/dist/markdown/ir.js +529 -0
- package/dist/markdown/render.d.ts +22 -0
- package/dist/markdown/render.js +148 -0
- package/dist/markdown/table-render.d.ts +43 -0
- package/dist/markdown/table-render.js +219 -0
- package/dist/markdown/tables.d.ts +17 -0
- package/dist/markdown/tables.js +27 -0
- package/dist/memory/embedding.d.ts +16 -0
- package/dist/memory/embedding.js +66 -0
- package/dist/memory/extractor.d.ts +6 -0
- package/dist/memory/extractor.js +72 -0
- package/dist/memory/search.d.ts +15 -0
- package/dist/memory/search.js +57 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.js +328 -0
- package/dist/memory/types.d.ts +9 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +29 -0
- package/dist/prompt-templates/builtins.d.ts +2 -0
- package/dist/prompt-templates/builtins.js +72 -0
- package/dist/prompt-templates/store.d.ts +39 -0
- package/dist/prompt-templates/store.js +174 -0
- package/dist/prompt-templates/types.d.ts +10 -0
- package/dist/prompt-templates/types.js +1 -0
- package/dist/scheduler/connected-channels.d.ts +24 -0
- package/dist/scheduler/connected-channels.js +57 -0
- package/dist/scheduler/scheduler.d.ts +22 -0
- package/dist/scheduler/scheduler.js +132 -0
- package/dist/scheduler/store.d.ts +27 -0
- package/dist/scheduler/store.js +205 -0
- package/dist/scheduler/types.d.ts +29 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/security/command-validator.d.ts +22 -0
- package/dist/security/command-validator.js +160 -0
- package/dist/security/docker-sandbox.d.ts +48 -0
- package/dist/security/docker-sandbox.js +218 -0
- package/dist/security/env-filter.d.ts +8 -0
- package/dist/security/env-filter.js +41 -0
- package/dist/skills/loader.d.ts +33 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/prompt.d.ts +6 -0
- package/dist/skills/prompt.js +17 -0
- package/dist/skills/read-tool.d.ts +7 -0
- package/dist/skills/read-tool.js +24 -0
- package/dist/skills/scanner.d.ts +6 -0
- package/dist/skills/scanner.js +73 -0
- package/dist/skills/types.d.ts +15 -0
- package/dist/skills/types.js +1 -0
- package/dist/tasks/store.d.ts +47 -0
- package/dist/tasks/store.js +193 -0
- package/dist/tasks/types.d.ts +75 -0
- package/dist/tasks/types.js +32 -0
- package/dist/teams/store.d.ts +78 -0
- package/dist/teams/store.js +420 -0
- package/dist/teams/types.d.ts +23 -0
- package/dist/teams/types.js +1 -0
- package/dist/tools/bash.d.ts +16 -0
- package/dist/tools/bash.js +62 -0
- package/dist/tools/channel-history.d.ts +10 -0
- package/dist/tools/channel-history.js +43 -0
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.js +216 -0
- package/dist/tools/fs.d.ts +4 -0
- package/dist/tools/fs.js +335 -0
- package/dist/tools/integration-toggle.d.ts +14 -0
- package/dist/tools/integration-toggle.js +47 -0
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.js +65 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/schedule.d.ts +8 -0
- package/dist/tools/schedule.js +219 -0
- package/dist/tools/speak.d.ts +10 -0
- package/dist/tools/speak.js +56 -0
- package/dist/tools/tasks.d.ts +29 -0
- package/dist/tools/tasks.js +92 -0
- package/dist/tools/teams.d.ts +7 -0
- package/dist/tools/teams.js +180 -0
- package/dist/tools/web-fetch.d.ts +3 -0
- package/dist/tools/web-fetch.js +22 -0
- package/dist/tts/edge.d.ts +10 -0
- package/dist/tts/edge.js +60 -0
- package/dist/tts/speak.d.ts +12 -0
- package/dist/tts/speak.js +81 -0
- package/dist/tts/transcribe.d.ts +5 -0
- package/dist/tts/transcribe.js +40 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +90 -0
- package/verybot.js +2 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command validation for bash tool security.
|
|
3
|
+
*
|
|
4
|
+
* Blocks dangerous shell syntax (quote-aware) and checks executables
|
|
5
|
+
* against safe-bin and allowlist rules.
|
|
6
|
+
*/
|
|
7
|
+
/** Shell tokens blocked outside of quotes. */
|
|
8
|
+
const BLOCKED_CHARS = new Set(["`", ">", "<", "(", ")"]);
|
|
9
|
+
/** Default read-only commands that always pass in allowlist mode. */
|
|
10
|
+
export const DEFAULT_SAFE_BINS = new Set([
|
|
11
|
+
"ls",
|
|
12
|
+
"cat",
|
|
13
|
+
"head",
|
|
14
|
+
"tail",
|
|
15
|
+
"grep",
|
|
16
|
+
"rg",
|
|
17
|
+
"find",
|
|
18
|
+
"wc",
|
|
19
|
+
"sort",
|
|
20
|
+
"uniq",
|
|
21
|
+
"cut",
|
|
22
|
+
"tr",
|
|
23
|
+
"jq",
|
|
24
|
+
"date",
|
|
25
|
+
"whoami",
|
|
26
|
+
"pwd",
|
|
27
|
+
"echo",
|
|
28
|
+
"which",
|
|
29
|
+
"file",
|
|
30
|
+
"stat",
|
|
31
|
+
"du",
|
|
32
|
+
"df",
|
|
33
|
+
"uname",
|
|
34
|
+
"env",
|
|
35
|
+
"printenv",
|
|
36
|
+
"ps",
|
|
37
|
+
"curl",
|
|
38
|
+
"wget",
|
|
39
|
+
]);
|
|
40
|
+
/**
|
|
41
|
+
* Check for dangerous shell syntax (quote-aware).
|
|
42
|
+
*
|
|
43
|
+
* Blocks backticks, command substitution `$(...)`, redirects `> <`,
|
|
44
|
+
* and subshells `( )` when they appear outside of quotes.
|
|
45
|
+
*/
|
|
46
|
+
export function validateCommand(command) {
|
|
47
|
+
let inSingle = false;
|
|
48
|
+
let inDouble = false;
|
|
49
|
+
for (let i = 0; i < command.length; i++) {
|
|
50
|
+
const ch = command[i];
|
|
51
|
+
// Track quote state
|
|
52
|
+
if (ch === "'" && !inDouble) {
|
|
53
|
+
inSingle = !inSingle;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (ch === '"' && !inSingle) {
|
|
57
|
+
inDouble = !inDouble;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Skip chars inside quotes
|
|
61
|
+
if (inSingle || inDouble)
|
|
62
|
+
continue;
|
|
63
|
+
// Check for $( command substitution
|
|
64
|
+
if (ch === "$" && i + 1 < command.length && command[i + 1] === "(") {
|
|
65
|
+
return { ok: false, reason: "command substitution $(...) is not allowed" };
|
|
66
|
+
}
|
|
67
|
+
if (BLOCKED_CHARS.has(ch)) {
|
|
68
|
+
const names = {
|
|
69
|
+
"`": "backtick execution",
|
|
70
|
+
">": "output redirect",
|
|
71
|
+
"<": "input redirect",
|
|
72
|
+
"(": "subshell",
|
|
73
|
+
")": "subshell",
|
|
74
|
+
};
|
|
75
|
+
return { ok: false, reason: `${names[ch] ?? ch} is not allowed` };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract the executable name from a command string.
|
|
82
|
+
* Handles env-prefix patterns like `VAR=val cmd` and paths like `/usr/bin/python3`.
|
|
83
|
+
*/
|
|
84
|
+
function extractExecutable(segment) {
|
|
85
|
+
const trimmed = segment.trim();
|
|
86
|
+
const tokens = trimmed.split(/\s+/);
|
|
87
|
+
// Skip leading env assignments (KEY=VALUE)
|
|
88
|
+
let idx = 0;
|
|
89
|
+
while (idx < tokens.length && /^[A-Za-z_]\w*=/.test(tokens[idx])) {
|
|
90
|
+
idx++;
|
|
91
|
+
}
|
|
92
|
+
const raw = tokens[idx] ?? "";
|
|
93
|
+
// Strip path: /usr/bin/python3 -> python3
|
|
94
|
+
const slashIdx = raw.lastIndexOf("/");
|
|
95
|
+
return slashIdx >= 0 ? raw.slice(slashIdx + 1) : raw;
|
|
96
|
+
}
|
|
97
|
+
/** Split a command into segments on `&&`, `||`, `;`, and `|`. */
|
|
98
|
+
function splitSegments(command) {
|
|
99
|
+
// Split on && || ; | while respecting quotes
|
|
100
|
+
const segments = [];
|
|
101
|
+
let current = "";
|
|
102
|
+
let inSingle = false;
|
|
103
|
+
let inDouble = false;
|
|
104
|
+
for (let i = 0; i < command.length; i++) {
|
|
105
|
+
const ch = command[i];
|
|
106
|
+
if (ch === "'" && !inDouble) {
|
|
107
|
+
inSingle = !inSingle;
|
|
108
|
+
current += ch;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ch === '"' && !inSingle) {
|
|
112
|
+
inDouble = !inDouble;
|
|
113
|
+
current += ch;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (inSingle || inDouble) {
|
|
117
|
+
current += ch;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// Check for && or ||
|
|
121
|
+
if ((ch === "&" || ch === "|") && i + 1 < command.length && command[i + 1] === ch) {
|
|
122
|
+
segments.push(current);
|
|
123
|
+
current = "";
|
|
124
|
+
i++; // skip second char
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Check for ; or single |
|
|
128
|
+
if (ch === ";" || ch === "|") {
|
|
129
|
+
segments.push(current);
|
|
130
|
+
current = "";
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
current += ch;
|
|
134
|
+
}
|
|
135
|
+
if (current.trim())
|
|
136
|
+
segments.push(current);
|
|
137
|
+
return segments.filter((s) => s.trim().length > 0);
|
|
138
|
+
}
|
|
139
|
+
/** Check if ALL segments of a command use only safe bins. */
|
|
140
|
+
function isSafeBin(command, safeBins) {
|
|
141
|
+
const segments = splitSegments(command);
|
|
142
|
+
return segments.every((seg) => safeBins.has(extractExecutable(seg)));
|
|
143
|
+
}
|
|
144
|
+
/** Convert a simple glob pattern to a RegExp (`*` -> `.*`). */
|
|
145
|
+
function globToRegExp(pattern) {
|
|
146
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
147
|
+
return new RegExp(`^${escaped}$`);
|
|
148
|
+
}
|
|
149
|
+
/** Check if an executable matches any allowlist glob pattern. */
|
|
150
|
+
function matchesAllowlist(executable, patterns) {
|
|
151
|
+
return patterns.some((pat) => globToRegExp(pat).test(executable));
|
|
152
|
+
}
|
|
153
|
+
/** Check if ALL segments of a command match the allowlist. */
|
|
154
|
+
export function isAllowed(command, safeBins, allowlist) {
|
|
155
|
+
const segments = splitSegments(command);
|
|
156
|
+
return segments.every((seg) => {
|
|
157
|
+
const exe = extractExecutable(seg);
|
|
158
|
+
return safeBins.has(exe) || matchesAllowlist(exe, allowlist);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker sandbox for bash tool execution.
|
|
3
|
+
*
|
|
4
|
+
* Commands run inside isolated Docker containers with:
|
|
5
|
+
* - Read-only root filesystem
|
|
6
|
+
* - No network access
|
|
7
|
+
* - All capabilities dropped
|
|
8
|
+
* - Resource limits (memory, pids)
|
|
9
|
+
* - Workspace mounted at /workspace (read-write)
|
|
10
|
+
*
|
|
11
|
+
* Containers are created lazily per session and reused for subsequent commands.
|
|
12
|
+
* Idle containers are pruned after a configurable timeout.
|
|
13
|
+
*
|
|
14
|
+
* If Docker is not available at startup, the sandbox polls in the background
|
|
15
|
+
* and returns an error to the user until Docker comes online.
|
|
16
|
+
*/
|
|
17
|
+
export interface SandboxConfig {
|
|
18
|
+
image: string;
|
|
19
|
+
memoryLimit: string;
|
|
20
|
+
pidsLimit: number;
|
|
21
|
+
idleTimeoutMs: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class DockerSandbox {
|
|
24
|
+
private containers;
|
|
25
|
+
private config;
|
|
26
|
+
private ready;
|
|
27
|
+
constructor(config: SandboxConfig);
|
|
28
|
+
/**
|
|
29
|
+
* Execute a command in the sandbox container for the given session.
|
|
30
|
+
* Checks Docker availability on first use (and after losing connection).
|
|
31
|
+
*/
|
|
32
|
+
exec(sessionKey: string, command: string): string;
|
|
33
|
+
/** Remove container for a session. */
|
|
34
|
+
cleanup(sessionKey: string): void;
|
|
35
|
+
/** Remove all containers (call on shutdown). */
|
|
36
|
+
cleanupAll(): void;
|
|
37
|
+
private getOrCreateContainer;
|
|
38
|
+
private resetIdleTimer;
|
|
39
|
+
private onIdle;
|
|
40
|
+
private removeContainer;
|
|
41
|
+
private containerName;
|
|
42
|
+
/**
|
|
43
|
+
* Distinguish Docker daemon errors (connection lost, daemon stopped) from
|
|
44
|
+
* normal command failures inside the container (non-zero exit code).
|
|
45
|
+
*/
|
|
46
|
+
private isDockerError;
|
|
47
|
+
private pruneOrphans;
|
|
48
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker sandbox for bash tool execution.
|
|
3
|
+
*
|
|
4
|
+
* Commands run inside isolated Docker containers with:
|
|
5
|
+
* - Read-only root filesystem
|
|
6
|
+
* - No network access
|
|
7
|
+
* - All capabilities dropped
|
|
8
|
+
* - Resource limits (memory, pids)
|
|
9
|
+
* - Workspace mounted at /workspace (read-write)
|
|
10
|
+
*
|
|
11
|
+
* Containers are created lazily per session and reused for subsequent commands.
|
|
12
|
+
* Idle containers are pruned after a configurable timeout.
|
|
13
|
+
*
|
|
14
|
+
* If Docker is not available at startup, the sandbox polls in the background
|
|
15
|
+
* and returns an error to the user until Docker comes online.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from "child_process";
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
import { mkdirSync } from "fs";
|
|
20
|
+
import { resolve } from "path";
|
|
21
|
+
import { logger } from "../logger.js";
|
|
22
|
+
import { SANDBOX_WORKSPACE } from "../paths.js";
|
|
23
|
+
const CONTAINER_PREFIX = "verybot-sandbox";
|
|
24
|
+
const LABEL = "verybot-sandbox=1";
|
|
25
|
+
const EXEC_TIMEOUT = 30_000;
|
|
26
|
+
const MAX_OUTPUT = 10_000;
|
|
27
|
+
const MAX_ERROR = 5_000;
|
|
28
|
+
/** Check if Docker is available by running `docker info`. */
|
|
29
|
+
function checkDocker() {
|
|
30
|
+
try {
|
|
31
|
+
execFileSync("docker", ["info"], { stdio: "ignore", timeout: 5_000 });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class DockerSandbox {
|
|
39
|
+
containers = new Map();
|
|
40
|
+
config;
|
|
41
|
+
ready = false;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
mkdirSync(resolve(SANDBOX_WORKSPACE), { recursive: true });
|
|
45
|
+
logger.info(`Docker sandbox: configured (image=${config.image}, memory=${config.memoryLimit}, ` +
|
|
46
|
+
`pids=${config.pidsLimit}, workspace=${SANDBOX_WORKSPACE}, idle=${config.idleTimeoutMs}ms)`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Execute a command in the sandbox container for the given session.
|
|
50
|
+
* Checks Docker availability on first use (and after losing connection).
|
|
51
|
+
*/
|
|
52
|
+
exec(sessionKey, command) {
|
|
53
|
+
if (!this.ready) {
|
|
54
|
+
if (!checkDocker()) {
|
|
55
|
+
return "Docker is not available. Please start Docker and try again.";
|
|
56
|
+
}
|
|
57
|
+
this.ready = true;
|
|
58
|
+
this.pruneOrphans();
|
|
59
|
+
logger.info("Docker sandbox: Docker is available, ready to execute");
|
|
60
|
+
}
|
|
61
|
+
const handle = this.getOrCreateContainer(sessionKey);
|
|
62
|
+
this.resetIdleTimer(sessionKey, handle);
|
|
63
|
+
try {
|
|
64
|
+
const output = execFileSync("docker", ["exec", handle.id, "sh", "-c", command], {
|
|
65
|
+
encoding: "utf-8",
|
|
66
|
+
timeout: EXEC_TIMEOUT,
|
|
67
|
+
maxBuffer: 1024 * 1024,
|
|
68
|
+
});
|
|
69
|
+
return output.slice(0, MAX_OUTPUT);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// If docker exec itself failed (not the command inside), reset ready flag
|
|
73
|
+
// so next call re-checks Docker availability
|
|
74
|
+
if (this.isDockerError(err)) {
|
|
75
|
+
logger.warn("Docker sandbox: Docker connection lost, will re-check on next exec");
|
|
76
|
+
this.ready = false;
|
|
77
|
+
this.containers.delete(sessionKey);
|
|
78
|
+
}
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
const stderr = err.stderr ?? "";
|
|
81
|
+
return `Error: ${msg}\n${stderr}`.slice(0, MAX_ERROR);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Remove container for a session. */
|
|
85
|
+
cleanup(sessionKey) {
|
|
86
|
+
const handle = this.containers.get(sessionKey);
|
|
87
|
+
if (!handle)
|
|
88
|
+
return;
|
|
89
|
+
clearTimeout(handle.timer);
|
|
90
|
+
this.removeContainer(handle);
|
|
91
|
+
this.containers.delete(sessionKey);
|
|
92
|
+
}
|
|
93
|
+
/** Remove all containers (call on shutdown). */
|
|
94
|
+
cleanupAll() {
|
|
95
|
+
for (const [key, handle] of this.containers) {
|
|
96
|
+
clearTimeout(handle.timer);
|
|
97
|
+
this.removeContainer(handle);
|
|
98
|
+
this.containers.delete(key);
|
|
99
|
+
}
|
|
100
|
+
logger.info("Docker sandbox: all containers cleaned up");
|
|
101
|
+
}
|
|
102
|
+
getOrCreateContainer(sessionKey) {
|
|
103
|
+
const existing = this.containers.get(sessionKey);
|
|
104
|
+
if (existing)
|
|
105
|
+
return existing;
|
|
106
|
+
const name = this.containerName(sessionKey);
|
|
107
|
+
const workspaceAbs = resolve(SANDBOX_WORKSPACE);
|
|
108
|
+
// Create container
|
|
109
|
+
const createArgs = [
|
|
110
|
+
"create",
|
|
111
|
+
"--name",
|
|
112
|
+
name,
|
|
113
|
+
"--label",
|
|
114
|
+
LABEL,
|
|
115
|
+
"--read-only",
|
|
116
|
+
"--tmpfs",
|
|
117
|
+
"/tmp:rw,noexec,nosuid,size=64m",
|
|
118
|
+
"--network",
|
|
119
|
+
"none",
|
|
120
|
+
"--cap-drop",
|
|
121
|
+
"ALL",
|
|
122
|
+
"--security-opt",
|
|
123
|
+
"no-new-privileges",
|
|
124
|
+
"--memory",
|
|
125
|
+
this.config.memoryLimit,
|
|
126
|
+
"--pids-limit",
|
|
127
|
+
String(this.config.pidsLimit),
|
|
128
|
+
"-v",
|
|
129
|
+
`${workspaceAbs}:/workspace:rw`,
|
|
130
|
+
"-w",
|
|
131
|
+
"/workspace",
|
|
132
|
+
this.config.image,
|
|
133
|
+
"sleep",
|
|
134
|
+
"infinity",
|
|
135
|
+
];
|
|
136
|
+
const id = execFileSync("docker", createArgs, {
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
timeout: 30_000,
|
|
139
|
+
}).trim();
|
|
140
|
+
// Start container
|
|
141
|
+
execFileSync("docker", ["start", id], {
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
timeout: 10_000,
|
|
144
|
+
});
|
|
145
|
+
logger.info(`Docker sandbox: created container ${name} (${id.slice(0, 12)}) for ${sessionKey}`);
|
|
146
|
+
const handle = {
|
|
147
|
+
id,
|
|
148
|
+
name,
|
|
149
|
+
timer: setTimeout(() => this.onIdle(sessionKey), this.config.idleTimeoutMs),
|
|
150
|
+
};
|
|
151
|
+
handle.timer.unref();
|
|
152
|
+
this.containers.set(sessionKey, handle);
|
|
153
|
+
return handle;
|
|
154
|
+
}
|
|
155
|
+
resetIdleTimer(sessionKey, handle) {
|
|
156
|
+
clearTimeout(handle.timer);
|
|
157
|
+
handle.timer = setTimeout(() => this.onIdle(sessionKey), this.config.idleTimeoutMs);
|
|
158
|
+
handle.timer.unref();
|
|
159
|
+
}
|
|
160
|
+
onIdle(sessionKey) {
|
|
161
|
+
const handle = this.containers.get(sessionKey);
|
|
162
|
+
if (!handle)
|
|
163
|
+
return;
|
|
164
|
+
logger.info(`Docker sandbox: pruning idle container ${handle.name} for ${sessionKey}`);
|
|
165
|
+
this.removeContainer(handle);
|
|
166
|
+
this.containers.delete(sessionKey);
|
|
167
|
+
}
|
|
168
|
+
removeContainer(handle) {
|
|
169
|
+
try {
|
|
170
|
+
execFileSync("docker", ["rm", "-f", handle.id], {
|
|
171
|
+
stdio: "ignore",
|
|
172
|
+
timeout: 10_000,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
logger.warn(`Docker sandbox: failed to remove ${handle.name}: ${err instanceof Error ? err.message : err}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
containerName(sessionKey) {
|
|
180
|
+
const hash = createHash("sha256").update(sessionKey).digest("hex").slice(0, 12);
|
|
181
|
+
return `${CONTAINER_PREFIX}-${hash}`;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Distinguish Docker daemon errors (connection lost, daemon stopped) from
|
|
185
|
+
* normal command failures inside the container (non-zero exit code).
|
|
186
|
+
*/
|
|
187
|
+
isDockerError(err) {
|
|
188
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
189
|
+
const stderr = err.stderr ?? "";
|
|
190
|
+
const combined = `${msg}\n${stderr}`.toLowerCase();
|
|
191
|
+
// These indicate Docker itself is unavailable, not a command failure
|
|
192
|
+
return (combined.includes("cannot connect to the docker daemon") ||
|
|
193
|
+
combined.includes("connection refused") ||
|
|
194
|
+
combined.includes("is the docker daemon running") ||
|
|
195
|
+
combined.includes("no such container") ||
|
|
196
|
+
combined.includes("is not running"));
|
|
197
|
+
}
|
|
198
|
+
pruneOrphans() {
|
|
199
|
+
try {
|
|
200
|
+
const ids = execFileSync("docker", ["ps", "-aq", "--filter", `label=${LABEL}`], { encoding: "utf-8", timeout: 5_000 }).trim();
|
|
201
|
+
if (!ids)
|
|
202
|
+
return;
|
|
203
|
+
const idList = ids.split("\n").filter(Boolean);
|
|
204
|
+
for (const id of idList) {
|
|
205
|
+
try {
|
|
206
|
+
execFileSync("docker", ["rm", "-f", id], { stdio: "ignore", timeout: 5_000 });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// ignore individual failures
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
logger.info(`Docker sandbox: pruned ${idList.length} orphan container(s)`);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// docker ps failed, ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable filtering for bash tool security.
|
|
3
|
+
*
|
|
4
|
+
* Strips dangerous env vars that could be used for code injection
|
|
5
|
+
* (LD_PRELOAD, DYLD_*, NODE_OPTIONS, etc.) before passing to child processes.
|
|
6
|
+
*/
|
|
7
|
+
/** Returns a copy of process.env with dangerous variables removed. */
|
|
8
|
+
export declare function sanitizeEnv(): Record<string, string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable filtering for bash tool security.
|
|
3
|
+
*
|
|
4
|
+
* Strips dangerous env vars that could be used for code injection
|
|
5
|
+
* (LD_PRELOAD, DYLD_*, NODE_OPTIONS, etc.) before passing to child processes.
|
|
6
|
+
*/
|
|
7
|
+
/** Exact env var names to strip (from Verybot's DANGEROUS_HOST_ENV_VARS). */
|
|
8
|
+
const BLOCKED_VARS = new Set([
|
|
9
|
+
"LD_PRELOAD",
|
|
10
|
+
"LD_LIBRARY_PATH",
|
|
11
|
+
"LD_AUDIT",
|
|
12
|
+
"DYLD_INSERT_LIBRARIES",
|
|
13
|
+
"DYLD_LIBRARY_PATH",
|
|
14
|
+
"NODE_OPTIONS",
|
|
15
|
+
"NODE_PATH",
|
|
16
|
+
"PYTHONPATH",
|
|
17
|
+
"PYTHONHOME",
|
|
18
|
+
"RUBYLIB",
|
|
19
|
+
"PERL5LIB",
|
|
20
|
+
"BASH_ENV",
|
|
21
|
+
"ENV",
|
|
22
|
+
"GCONV_PATH",
|
|
23
|
+
"IFS",
|
|
24
|
+
"SSLKEYLOGFILE",
|
|
25
|
+
]);
|
|
26
|
+
/** Env var prefixes to strip (catches all DYLD_* and LD_* variants). */
|
|
27
|
+
const BLOCKED_PREFIXES = ["DYLD_", "LD_"];
|
|
28
|
+
/** Returns a copy of process.env with dangerous variables removed. */
|
|
29
|
+
export function sanitizeEnv() {
|
|
30
|
+
const clean = {};
|
|
31
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
if (BLOCKED_VARS.has(key))
|
|
35
|
+
continue;
|
|
36
|
+
if (BLOCKED_PREFIXES.some((prefix) => key.startsWith(prefix)))
|
|
37
|
+
continue;
|
|
38
|
+
clean[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return clean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Tool } from "ai";
|
|
2
|
+
import type { SkillEntry } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Manages skills with optional file watching for hot-reload.
|
|
5
|
+
* The Agent reads `systemPrompt` and `readTool` on each run,
|
|
6
|
+
* so changes are picked up automatically.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SkillManager {
|
|
9
|
+
private _entries;
|
|
10
|
+
private _systemPrompt;
|
|
11
|
+
private _readTool;
|
|
12
|
+
private watcher;
|
|
13
|
+
private retryTimer;
|
|
14
|
+
private retryCount;
|
|
15
|
+
private stopped;
|
|
16
|
+
private dir;
|
|
17
|
+
constructor(dir: string);
|
|
18
|
+
get entries(): SkillEntry[];
|
|
19
|
+
get systemPrompt(): string;
|
|
20
|
+
get readTool(): Tool | null;
|
|
21
|
+
/** Scan the skills directory and update state. */
|
|
22
|
+
scan(): Promise<void>;
|
|
23
|
+
/** Start watching the skills directory for changes. Retries with backoff if directory is missing. */
|
|
24
|
+
startWatching(): void;
|
|
25
|
+
/** Stop the file watcher and cancel any pending retries. */
|
|
26
|
+
stopWatching(): void;
|
|
27
|
+
private attemptWatch;
|
|
28
|
+
private scheduleRetry;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load skills from the given directory and start watching for changes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadSkills(dir: string): Promise<SkillManager>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { scanSkills } from "./scanner.js";
|
|
4
|
+
import { buildSkillListing } from "./prompt.js";
|
|
5
|
+
import { createReadSkillTool } from "./read-tool.js";
|
|
6
|
+
import { logger } from "../logger.js";
|
|
7
|
+
/** Debounce delay for file watcher to avoid rapid rescans. */
|
|
8
|
+
const WATCH_DEBOUNCE_MS = 500;
|
|
9
|
+
/** Initial retry delay when watcher fails or directory is missing. */
|
|
10
|
+
const WATCH_RETRY_BASE_MS = 2_000;
|
|
11
|
+
/** Maximum retry delay (caps exponential backoff). */
|
|
12
|
+
const WATCH_RETRY_MAX_MS = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* Manages skills with optional file watching for hot-reload.
|
|
15
|
+
* The Agent reads `systemPrompt` and `readTool` on each run,
|
|
16
|
+
* so changes are picked up automatically.
|
|
17
|
+
*/
|
|
18
|
+
export class SkillManager {
|
|
19
|
+
_entries = [];
|
|
20
|
+
_systemPrompt = "";
|
|
21
|
+
_readTool = null;
|
|
22
|
+
watcher = null;
|
|
23
|
+
retryTimer = null;
|
|
24
|
+
retryCount = 0;
|
|
25
|
+
stopped = false;
|
|
26
|
+
dir;
|
|
27
|
+
constructor(dir) {
|
|
28
|
+
this.dir = resolve(dir);
|
|
29
|
+
}
|
|
30
|
+
get entries() {
|
|
31
|
+
return this._entries;
|
|
32
|
+
}
|
|
33
|
+
get systemPrompt() {
|
|
34
|
+
return this._systemPrompt;
|
|
35
|
+
}
|
|
36
|
+
get readTool() {
|
|
37
|
+
return this._readTool;
|
|
38
|
+
}
|
|
39
|
+
/** Scan the skills directory and update state. */
|
|
40
|
+
async scan() {
|
|
41
|
+
const entries = await scanSkills(this.dir);
|
|
42
|
+
this._entries = entries;
|
|
43
|
+
if (entries.length === 0) {
|
|
44
|
+
this._systemPrompt = "";
|
|
45
|
+
this._readTool = null;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this._systemPrompt = buildSkillListing(entries);
|
|
49
|
+
this._readTool = createReadSkillTool(entries);
|
|
50
|
+
logger.info(`Skills loaded: ${entries.map((s) => s.name).join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
/** Start watching the skills directory for changes. Retries with backoff if directory is missing. */
|
|
53
|
+
startWatching() {
|
|
54
|
+
this.stopped = false;
|
|
55
|
+
this.attemptWatch();
|
|
56
|
+
}
|
|
57
|
+
/** Stop the file watcher and cancel any pending retries. */
|
|
58
|
+
stopWatching() {
|
|
59
|
+
this.stopped = true;
|
|
60
|
+
if (this.retryTimer) {
|
|
61
|
+
clearTimeout(this.retryTimer);
|
|
62
|
+
this.retryTimer = null;
|
|
63
|
+
}
|
|
64
|
+
this.watcher?.close();
|
|
65
|
+
this.watcher = null;
|
|
66
|
+
}
|
|
67
|
+
attemptWatch() {
|
|
68
|
+
if (this.stopped)
|
|
69
|
+
return;
|
|
70
|
+
this.watcher?.close();
|
|
71
|
+
this.watcher = null;
|
|
72
|
+
let debounceTimer = null;
|
|
73
|
+
try {
|
|
74
|
+
this.watcher = chokidarWatch(this.dir, {
|
|
75
|
+
ignoreInitial: true,
|
|
76
|
+
persistent: true,
|
|
77
|
+
depth: 2,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
this.scheduleRetry();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const debouncedRescan = (path) => {
|
|
85
|
+
if (debounceTimer)
|
|
86
|
+
clearTimeout(debounceTimer);
|
|
87
|
+
debounceTimer = setTimeout(() => {
|
|
88
|
+
logger.info(`Skills directory changed (${path}), rescanning...`);
|
|
89
|
+
this.scan().catch((err) => {
|
|
90
|
+
logger.warn(`Skills rescan failed: ${err instanceof Error ? err.message : err}`);
|
|
91
|
+
});
|
|
92
|
+
}, WATCH_DEBOUNCE_MS);
|
|
93
|
+
};
|
|
94
|
+
this.watcher
|
|
95
|
+
.on("add", debouncedRescan)
|
|
96
|
+
.on("change", debouncedRescan)
|
|
97
|
+
.on("unlink", debouncedRescan)
|
|
98
|
+
.on("unlinkDir", debouncedRescan)
|
|
99
|
+
.on("error", (err) => {
|
|
100
|
+
logger.warn(`Skills watcher error: ${err instanceof Error ? err.message : err}`);
|
|
101
|
+
this.scheduleRetry();
|
|
102
|
+
});
|
|
103
|
+
this.retryCount = 0;
|
|
104
|
+
logger.info(`Watching skills directory: ${this.dir}`);
|
|
105
|
+
}
|
|
106
|
+
scheduleRetry() {
|
|
107
|
+
if (this.stopped)
|
|
108
|
+
return;
|
|
109
|
+
this.watcher?.close();
|
|
110
|
+
this.watcher = null;
|
|
111
|
+
const delay = Math.min(WATCH_RETRY_BASE_MS * 2 ** this.retryCount, WATCH_RETRY_MAX_MS);
|
|
112
|
+
this.retryCount++;
|
|
113
|
+
logger.info(`Skills watcher will retry in ${delay / 1000}s (attempt ${this.retryCount})`);
|
|
114
|
+
this.retryTimer = setTimeout(() => {
|
|
115
|
+
this.retryTimer = null;
|
|
116
|
+
this.scan()
|
|
117
|
+
.catch((err) => {
|
|
118
|
+
logger.warn(`Skills rescan on retry failed: ${err instanceof Error ? err.message : err}`);
|
|
119
|
+
})
|
|
120
|
+
.finally(() => this.attemptWatch());
|
|
121
|
+
}, delay);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Load skills from the given directory and start watching for changes.
|
|
126
|
+
*/
|
|
127
|
+
export async function loadSkills(dir) {
|
|
128
|
+
const manager = new SkillManager(dir);
|
|
129
|
+
await manager.scan();
|
|
130
|
+
manager.startWatching();
|
|
131
|
+
return manager;
|
|
132
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a compact system prompt section listing available skills.
|
|
3
|
+
* The model uses `read_skill` to fetch full content on demand.
|
|
4
|
+
*/
|
|
5
|
+
export function buildSkillListing(skills) {
|
|
6
|
+
if (skills.length === 0)
|
|
7
|
+
return "";
|
|
8
|
+
const lines = skills.map((s) => ` - **${s.name}**: ${s.description || "(no description)"}`);
|
|
9
|
+
return `## Available Skills
|
|
10
|
+
You have access to specialized skills. Use the \`read_skill\` tool to read the full instructions for a skill before applying it.
|
|
11
|
+
|
|
12
|
+
<available_skills>
|
|
13
|
+
${lines.join("\n")}
|
|
14
|
+
</available_skills>
|
|
15
|
+
|
|
16
|
+
When a user's request matches a skill, read it first, then follow its instructions.`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Tool } from "ai";
|
|
2
|
+
import type { SkillEntry } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create the read_skill tool with the given skill entries baked in.
|
|
5
|
+
* Returns the full SKILL.md content for the requested skill name.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createReadSkillTool(skills: SkillEntry[]): Tool;
|