zubo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +35 -0
- package/README.md +149 -0
- package/bun.lock +216 -0
- package/desktop/README.md +57 -0
- package/desktop/package.json +12 -0
- package/desktop/src-tauri/Cargo.toml +25 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/icons/README.md +17 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/main.rs +189 -0
- package/desktop/src-tauri/tauri.conf.json +68 -0
- package/docs/ROADMAP.md +490 -0
- package/migrations/001_init.sql +9 -0
- package/migrations/002_memory.sql +33 -0
- package/migrations/003_cron.sql +24 -0
- package/migrations/004_usage.sql +12 -0
- package/migrations/005_secrets.sql +8 -0
- package/migrations/006_agents.sql +1 -0
- package/migrations/007_workflows.sql +22 -0
- package/migrations/008_proactive.sql +24 -0
- package/migrations/009_uploads.sql +9 -0
- package/migrations/010_observability.sql +22 -0
- package/migrations/011_api_keys.sql +7 -0
- package/migrations/012_indexes.sql +5 -0
- package/migrations/013_budget.sql +11 -0
- package/migrations/014_usage_session_idx.sql +2 -0
- package/package.json +39 -0
- package/site/404.html +156 -0
- package/site/CNAME +1 -0
- package/site/docs/agents.html +294 -0
- package/site/docs/api.html +446 -0
- package/site/docs/channels.html +345 -0
- package/site/docs/cli.html +238 -0
- package/site/docs/config.html +1034 -0
- package/site/docs/index.html +433 -0
- package/site/docs/integrations.html +381 -0
- package/site/docs/memory.html +254 -0
- package/site/docs/security.html +375 -0
- package/site/docs/skills.html +322 -0
- package/site/docs.css +412 -0
- package/site/index.html +638 -0
- package/site/install.sh +98 -0
- package/site/logo.svg +1 -0
- package/site/og-image.png +0 -0
- package/site/robots.txt +4 -0
- package/site/script.js +361 -0
- package/site/sitemap.xml +63 -0
- package/site/skills.html +532 -0
- package/site/style.css +1686 -0
- package/src/agent/agents.ts +159 -0
- package/src/agent/compaction.ts +53 -0
- package/src/agent/context.ts +18 -0
- package/src/agent/delegate.ts +118 -0
- package/src/agent/loop.ts +318 -0
- package/src/agent/prompts.ts +111 -0
- package/src/agent/session.ts +87 -0
- package/src/agent/teams.ts +116 -0
- package/src/agent/workflow-executor.ts +192 -0
- package/src/agent/workflow.ts +175 -0
- package/src/channels/adapter.ts +21 -0
- package/src/channels/dashboard.html.ts +2969 -0
- package/src/channels/discord.ts +137 -0
- package/src/channels/optional-deps.d.ts +17 -0
- package/src/channels/router.ts +199 -0
- package/src/channels/signal.ts +133 -0
- package/src/channels/slack.ts +101 -0
- package/src/channels/telegram.ts +102 -0
- package/src/channels/utils.ts +18 -0
- package/src/channels/webchat.ts +1797 -0
- package/src/channels/whatsapp.ts +119 -0
- package/src/config/loader.ts +22 -0
- package/src/config/paths.ts +43 -0
- package/src/config/schema.ts +121 -0
- package/src/db/connection.ts +20 -0
- package/src/db/export.ts +148 -0
- package/src/db/migrations.ts +42 -0
- package/src/index.ts +261 -0
- package/src/llm/claude.ts +193 -0
- package/src/llm/factory.ts +115 -0
- package/src/llm/failover.ts +101 -0
- package/src/llm/openai-compat.ts +409 -0
- package/src/llm/provider.ts +83 -0
- package/src/llm/smart-router.ts +241 -0
- package/src/logs.ts +53 -0
- package/src/memory/chunker.ts +58 -0
- package/src/memory/document-parser.ts +115 -0
- package/src/memory/embedder.ts +235 -0
- package/src/memory/engine.ts +170 -0
- package/src/memory/fts-index.ts +55 -0
- package/src/memory/hybrid-search.ts +72 -0
- package/src/memory/store.ts +56 -0
- package/src/memory/vector-index.ts +72 -0
- package/src/model.ts +118 -0
- package/src/registry/cli.ts +43 -0
- package/src/registry/client.ts +54 -0
- package/src/registry/installer.ts +67 -0
- package/src/scheduler/briefing.ts +71 -0
- package/src/scheduler/cron.ts +258 -0
- package/src/scheduler/heartbeat.ts +58 -0
- package/src/scheduler/memory-triggers.ts +100 -0
- package/src/scheduler/natural-cron.ts +163 -0
- package/src/scheduler/proactive.ts +25 -0
- package/src/scheduler/recipes.ts +110 -0
- package/src/secrets/store.ts +64 -0
- package/src/setup.ts +413 -0
- package/src/skills.ts +293 -0
- package/src/start.ts +373 -0
- package/src/status.ts +165 -0
- package/src/tools/builtin/connect-service.ts +205 -0
- package/src/tools/builtin/cron.ts +126 -0
- package/src/tools/builtin/datetime.ts +36 -0
- package/src/tools/builtin/delegate-task.ts +81 -0
- package/src/tools/builtin/delegate.ts +42 -0
- package/src/tools/builtin/diagnose.ts +41 -0
- package/src/tools/builtin/google-oauth.ts +379 -0
- package/src/tools/builtin/manage-agents.ts +149 -0
- package/src/tools/builtin/manage-skills.ts +294 -0
- package/src/tools/builtin/manage-teams.ts +89 -0
- package/src/tools/builtin/manage-triggers.ts +94 -0
- package/src/tools/builtin/manage-workflows.ts +119 -0
- package/src/tools/builtin/memory-search.ts +38 -0
- package/src/tools/builtin/memory-write.ts +30 -0
- package/src/tools/builtin/run-workflow.ts +36 -0
- package/src/tools/builtin/secrets.ts +122 -0
- package/src/tools/builtin/skill-registry.ts +75 -0
- package/src/tools/builtin-integrations/api-helpers.ts +26 -0
- package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
- package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
- package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
- package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
- package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
- package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
- package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
- package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
- package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
- package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
- package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
- package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
- package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
- package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
- package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
- package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
- package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
- package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
- package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
- package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
- package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
- package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
- package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
- package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
- package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
- package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
- package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
- package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
- package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
- package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
- package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
- package/src/tools/builtin-skills/file-read/handler.ts +66 -0
- package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
- package/src/tools/builtin-skills/file-write/handler.ts +64 -0
- package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
- package/src/tools/builtin-skills/http-request/handler.ts +87 -0
- package/src/tools/builtin-skills/shell/SKILL.md +26 -0
- package/src/tools/builtin-skills/shell/handler.ts +96 -0
- package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
- package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
- package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
- package/src/tools/builtin-skills/web-search/handler.ts +50 -0
- package/src/tools/executor.ts +205 -0
- package/src/tools/integration-installer.ts +106 -0
- package/src/tools/permissions.ts +45 -0
- package/src/tools/registry.ts +39 -0
- package/src/tools/sandbox-runner.ts +56 -0
- package/src/tools/sandbox.ts +82 -0
- package/src/tools/skill-installer.ts +52 -0
- package/src/tools/skill-loader.ts +259 -0
- package/src/types/optional-deps.d.ts +23 -0
- package/src/util/auth.ts +121 -0
- package/src/util/costs.ts +59 -0
- package/src/util/error-buffer.ts +32 -0
- package/src/util/google-tokens.ts +180 -0
- package/src/util/logger.ts +73 -0
- package/src/util/perf-collector.ts +35 -0
- package/src/util/rate-limiter.ts +70 -0
- package/src/util/tokens.ts +17 -0
- package/src/voice/stt.ts +57 -0
- package/src/voice/tts.ts +103 -0
- package/tests/agent/session.test.ts +109 -0
- package/tests/agent-loop.test.ts +54 -0
- package/tests/auth.test.ts +89 -0
- package/tests/channels.test.ts +67 -0
- package/tests/compaction.test.ts +44 -0
- package/tests/config.test.ts +51 -0
- package/tests/costs.test.ts +19 -0
- package/tests/cron.test.ts +55 -0
- package/tests/db/export.test.ts +219 -0
- package/tests/executor.test.ts +144 -0
- package/tests/export.test.ts +137 -0
- package/tests/helpers/mock-llm.ts +34 -0
- package/tests/helpers/test-db.ts +74 -0
- package/tests/integration/chat-flow.test.ts +48 -0
- package/tests/integrations.test.ts +97 -0
- package/tests/memory/engine.test.ts +114 -0
- package/tests/memory-engine.test.ts +57 -0
- package/tests/permissions.test.ts +21 -0
- package/tests/rate-limiter.test.ts +70 -0
- package/tests/registry.test.ts +67 -0
- package/tests/router.test.ts +36 -0
- package/tests/session.test.ts +58 -0
- package/tests/skill-loader.test.ts +44 -0
- package/tests/tokens.test.ts +30 -0
- package/tests/tools/executor.test.ts +130 -0
- package/tests/util/auth.test.ts +75 -0
- package/tests/util/rate-limiter.test.ts +73 -0
- package/tests/voice.test.ts +60 -0
- package/tests/webchat.test.ts +88 -0
- package/tests/workflow.test.ts +38 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export default async function (input: Record<string, unknown>): Promise<string> {
|
|
2
|
+
const query = input.query as string;
|
|
3
|
+
const maxResults = (input.maxResults as number) || 5;
|
|
4
|
+
|
|
5
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
6
|
+
|
|
7
|
+
const res = await fetch(url, {
|
|
8
|
+
headers: {
|
|
9
|
+
"User-Agent": "Zubo/1.0",
|
|
10
|
+
},
|
|
11
|
+
signal: AbortSignal.timeout(15000),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
throw new Error(`Search failed: ${res.status} ${res.statusText}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const html = await res.text();
|
|
19
|
+
|
|
20
|
+
// Parse results from DuckDuckGo HTML
|
|
21
|
+
const results: { title: string; url: string; snippet: string }[] = [];
|
|
22
|
+
const resultRegex =
|
|
23
|
+
/<a rel="nofollow" class="result__a" href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
24
|
+
|
|
25
|
+
let match;
|
|
26
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
|
|
27
|
+
const rawUrl = match[1];
|
|
28
|
+
const title = match[2].replace(/<[^>]+>/g, "").trim();
|
|
29
|
+
const snippet = match[3].replace(/<[^>]+>/g, "").trim();
|
|
30
|
+
|
|
31
|
+
// DuckDuckGo wraps URLs in a redirect — extract the actual URL
|
|
32
|
+
let finalUrl = rawUrl;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = new URL(rawUrl, "https://duckduckgo.com");
|
|
35
|
+
finalUrl = parsed.searchParams.get("uddg") || rawUrl;
|
|
36
|
+
} catch (err: any) {
|
|
37
|
+
// URL parsing is non-critical; fall back to rawUrl
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (title) {
|
|
41
|
+
results.push({ title, url: finalUrl, snippet });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (results.length === 0) {
|
|
46
|
+
return JSON.stringify({ query, results: [], message: "No results found." });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return JSON.stringify({ query, results });
|
|
50
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { getTool } from "./registry";
|
|
2
|
+
import { getToolPermission } from "./permissions";
|
|
3
|
+
import { logger } from "../util/logger";
|
|
4
|
+
import { recordError } from "../util/error-buffer";
|
|
5
|
+
import { executeSandboxed } from "./sandbox";
|
|
6
|
+
|
|
7
|
+
export interface ToolResult {
|
|
8
|
+
tool_use_id: string;
|
|
9
|
+
content: string;
|
|
10
|
+
is_error: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Server-side confirmation tracking — prevents LLM from spoofing _confirmed
|
|
14
|
+
const pendingConfirmations = new Map<string, { toolName: string; input: Record<string, unknown>; timestamp: number }>();
|
|
15
|
+
|
|
16
|
+
// Clean up stale confirmations older than 10 minutes
|
|
17
|
+
function cleanStaleConfirmations() {
|
|
18
|
+
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
19
|
+
for (const [token, entry] of pendingConfirmations) {
|
|
20
|
+
if (entry.timestamp < cutoff) pendingConfirmations.delete(token);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Determine if a tool should run in the sandbox (user-installed skills only)
|
|
25
|
+
async function shouldSandbox(
|
|
26
|
+
toolName: string
|
|
27
|
+
): Promise<{ handlerPath: string; timeoutMs: number; env: Record<string, string> } | null> {
|
|
28
|
+
try {
|
|
29
|
+
const { isUserInstalledSkill } = await import("./registry");
|
|
30
|
+
|
|
31
|
+
// Only sandbox tools that were loaded via the skill-loader from the user's skills directory
|
|
32
|
+
if (!isUserInstalledSkill(toolName)) return null;
|
|
33
|
+
|
|
34
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
35
|
+
const { join } = await import("path");
|
|
36
|
+
const { paths } = await import("../config/paths");
|
|
37
|
+
|
|
38
|
+
// Check if sandbox is enabled in config
|
|
39
|
+
let timeoutMs = 30_000;
|
|
40
|
+
try {
|
|
41
|
+
const config = JSON.parse(readFileSync(paths.config, "utf-8"));
|
|
42
|
+
if (config.sandbox?.enabled === false) return null;
|
|
43
|
+
if (config.sandbox?.timeoutMs) timeoutMs = config.sandbox.timeoutMs;
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
logger.warn("Failed to read sandbox config", { error: (err as Error).message });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Resolve the handler path and read SKILL.md for declared secrets
|
|
49
|
+
const skillDir = join(paths.skills, toolName);
|
|
50
|
+
const handlerPath = join(skillDir, "handler.ts");
|
|
51
|
+
if (!existsSync(handlerPath)) return null;
|
|
52
|
+
|
|
53
|
+
// If the handler uses getGoogleToken, it needs the main process globals — skip sandbox
|
|
54
|
+
const handlerCode = readFileSync(handlerPath, "utf-8");
|
|
55
|
+
if (handlerCode.includes("getGoogleToken")) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Only pass secrets that the skill actually references (grep handler for getSecret calls)
|
|
60
|
+
const env: Record<string, string> = {};
|
|
61
|
+
try {
|
|
62
|
+
const { getDb } = await import("../db/connection");
|
|
63
|
+
const db = getDb();
|
|
64
|
+
const rows = db.query("SELECT name, value FROM secrets").all() as { name: string; value: string }[];
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
// Only pass secrets referenced in the handler code
|
|
67
|
+
if (handlerCode.includes(`"${row.name}"`) || handlerCode.includes(`'${row.name}'`)) {
|
|
68
|
+
env[`ZUBO_SECRET_${row.name.toUpperCase()}`] = row.value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (err: any) {
|
|
72
|
+
logger.warn("Failed to resolve skill secrets for sandbox", { error: (err as Error).message });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { handlerPath, timeoutMs, env };
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function executeTool(
|
|
82
|
+
name: string,
|
|
83
|
+
toolUseId: string,
|
|
84
|
+
input: Record<string, unknown>,
|
|
85
|
+
allowedTools?: string[]
|
|
86
|
+
): Promise<ToolResult> {
|
|
87
|
+
// Defense-in-depth: if an allowedTools set is provided (sub-agents),
|
|
88
|
+
// reject any tool call not in the set, even if the LLM tries to call it.
|
|
89
|
+
if (allowedTools && !allowedTools.includes(name)) {
|
|
90
|
+
logger.warn(`Tool blocked by allowedTools: ${name}`);
|
|
91
|
+
return {
|
|
92
|
+
tool_use_id: toolUseId,
|
|
93
|
+
content: `Error: Tool '${name}' is not available in this agent context.`,
|
|
94
|
+
is_error: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const tool = getTool(name);
|
|
98
|
+
if (!tool) {
|
|
99
|
+
logger.error(`Tool not found: ${name}`);
|
|
100
|
+
return {
|
|
101
|
+
tool_use_id: toolUseId,
|
|
102
|
+
content: `Error: Unknown tool '${name}'`,
|
|
103
|
+
is_error: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const permission = getToolPermission(name);
|
|
108
|
+
|
|
109
|
+
if (permission === "deny") {
|
|
110
|
+
logger.warn(`Tool denied: ${name}`);
|
|
111
|
+
return {
|
|
112
|
+
tool_use_id: toolUseId,
|
|
113
|
+
content: `Error: Tool '${name}' is not permitted.`,
|
|
114
|
+
is_error: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (permission === "confirm") {
|
|
119
|
+
cleanStaleConfirmations();
|
|
120
|
+
|
|
121
|
+
// Check for a valid server-issued confirmation token
|
|
122
|
+
const confirmToken = input._confirmToken as string | undefined;
|
|
123
|
+
if (confirmToken) {
|
|
124
|
+
const pending = pendingConfirmations.get(confirmToken);
|
|
125
|
+
if (!pending || pending.toolName !== name) {
|
|
126
|
+
return {
|
|
127
|
+
tool_use_id: toolUseId,
|
|
128
|
+
content: `Error: Invalid or expired confirmation token. The action was NOT executed. Please ask the user for approval again.`,
|
|
129
|
+
is_error: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Valid token — clear it and proceed
|
|
133
|
+
pendingConfirmations.delete(confirmToken);
|
|
134
|
+
} else {
|
|
135
|
+
// No token — generate one and request confirmation
|
|
136
|
+
const { _confirmed, _confirmToken: _, ...displayInput } = input;
|
|
137
|
+
const desc = JSON.stringify(displayInput, null, 2);
|
|
138
|
+
const token = crypto.randomUUID();
|
|
139
|
+
pendingConfirmations.set(token, { toolName: name, input: displayInput, timestamp: Date.now() });
|
|
140
|
+
logger.info(`Tool requires confirmation: ${name}`);
|
|
141
|
+
return {
|
|
142
|
+
tool_use_id: toolUseId,
|
|
143
|
+
content: `CONFIRMATION REQUIRED — tool was NOT executed.\n\nTool: ${name}\nInput: ${desc}\nConfirmation Token: ${token}\n\nThis tool requires user approval before it can run. Describe this action to the user and ask for their permission. Once they approve, call this tool again with _confirmToken set to "${token}" in the input.`,
|
|
144
|
+
is_error: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const startTime = Date.now();
|
|
150
|
+
try {
|
|
151
|
+
const { _confirmed, _confirmToken, ...cleanInput } = input;
|
|
152
|
+
logger.info(`Executing tool: ${name}`);
|
|
153
|
+
|
|
154
|
+
// Check if this is a user-installed skill that should be sandboxed
|
|
155
|
+
let result: any;
|
|
156
|
+
const sandboxed = await shouldSandbox(name);
|
|
157
|
+
if (sandboxed) {
|
|
158
|
+
result = await executeSandboxed(sandboxed.handlerPath, cleanInput, {
|
|
159
|
+
timeoutMs: sandboxed.timeoutMs,
|
|
160
|
+
env: sandboxed.env,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
result = await tool.execute(cleanInput);
|
|
164
|
+
}
|
|
165
|
+
const durationMs = Date.now() - startTime;
|
|
166
|
+
|
|
167
|
+
// Record tool metrics
|
|
168
|
+
try {
|
|
169
|
+
const { getDb } = await import("../db/connection");
|
|
170
|
+
const db = getDb();
|
|
171
|
+
db.prepare(
|
|
172
|
+
"INSERT INTO tool_metrics (tool_name, duration_ms, success) VALUES (?, ?, 1)"
|
|
173
|
+
).run(name, durationMs);
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
logger.warn("Failed to record tool success metrics", { error: (err as Error).message });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
tool_use_id: toolUseId,
|
|
180
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
181
|
+
is_error: false,
|
|
182
|
+
};
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
const durationMs = Date.now() - startTime;
|
|
185
|
+
logger.error(`Tool error: ${name}`, { error: err.message });
|
|
186
|
+
recordError(`tool:${name}`, err.message);
|
|
187
|
+
|
|
188
|
+
// Record failed tool metrics
|
|
189
|
+
try {
|
|
190
|
+
const { getDb } = await import("../db/connection");
|
|
191
|
+
const db = getDb();
|
|
192
|
+
db.prepare(
|
|
193
|
+
"INSERT INTO tool_metrics (tool_name, duration_ms, success) VALUES (?, ?, 0)"
|
|
194
|
+
).run(name, durationMs);
|
|
195
|
+
} catch (metricsErr: any) {
|
|
196
|
+
logger.warn("Failed to record tool failure metrics", { error: (metricsErr as Error).message });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
tool_use_id: toolUseId,
|
|
201
|
+
content: `Error: ${err.message}`,
|
|
202
|
+
is_error: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const INTEGRATIONS_DIR = join(import.meta.dir, "builtin-integrations");
|
|
5
|
+
|
|
6
|
+
export interface IntegrationInfo {
|
|
7
|
+
service: string;
|
|
8
|
+
skills: string[];
|
|
9
|
+
secret_name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SERVICE_SECRETS: Record<string, string> = {
|
|
13
|
+
github: "github_token",
|
|
14
|
+
google: "google_oauth", // Google uses OAuth 2.0 — credentials are google_client_id + google_client_secret
|
|
15
|
+
notion: "notion_token",
|
|
16
|
+
slack: "slack_token",
|
|
17
|
+
linear: "linear_token",
|
|
18
|
+
jira: "jira_token",
|
|
19
|
+
twitter: "twitter_bearer_token",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lists available integrations by scanning the builtin-integrations directory.
|
|
24
|
+
*/
|
|
25
|
+
export function listAvailableIntegrations(): IntegrationInfo[] {
|
|
26
|
+
const integrations: IntegrationInfo[] = [];
|
|
27
|
+
|
|
28
|
+
let services: string[];
|
|
29
|
+
try {
|
|
30
|
+
services = readdirSync(INTEGRATIONS_DIR);
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const service of services) {
|
|
36
|
+
const serviceDir = join(INTEGRATIONS_DIR, service);
|
|
37
|
+
let entries: string[];
|
|
38
|
+
try {
|
|
39
|
+
entries = readdirSync(serviceDir);
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const skills: string[] = [];
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const skillDir = join(serviceDir, entry);
|
|
47
|
+
if (
|
|
48
|
+
existsSync(join(skillDir, "SKILL.md")) &&
|
|
49
|
+
existsSync(join(skillDir, "handler.ts"))
|
|
50
|
+
) {
|
|
51
|
+
skills.push(entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (skills.length > 0) {
|
|
56
|
+
integrations.push({
|
|
57
|
+
service,
|
|
58
|
+
skills,
|
|
59
|
+
secret_name: SERVICE_SECRETS[service] || `${service}_token`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return integrations;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Installs integration skill templates to the user's skills directory.
|
|
69
|
+
* Returns list of installed skill names.
|
|
70
|
+
*/
|
|
71
|
+
export function installIntegration(targetDir: string, service: string): string[] {
|
|
72
|
+
const serviceDir = join(INTEGRATIONS_DIR, service);
|
|
73
|
+
if (!existsSync(serviceDir)) return [];
|
|
74
|
+
|
|
75
|
+
let entries: string[];
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(serviceDir);
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const installed: string[] = [];
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const srcDir = join(serviceDir, entry);
|
|
86
|
+
const skillMd = join(srcDir, "SKILL.md");
|
|
87
|
+
const handlerTs = join(srcDir, "handler.ts");
|
|
88
|
+
|
|
89
|
+
if (!existsSync(skillMd) || !existsSync(handlerTs)) continue;
|
|
90
|
+
|
|
91
|
+
const destDir = join(targetDir, entry);
|
|
92
|
+
|
|
93
|
+
// Skip if already installed (preserve user edits)
|
|
94
|
+
if (existsSync(destDir)) {
|
|
95
|
+
installed.push(entry);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
mkdirSync(destDir, { recursive: true });
|
|
100
|
+
writeFileSync(join(destDir, "SKILL.md"), readFileSync(skillMd));
|
|
101
|
+
writeFileSync(join(destDir, "handler.ts"), readFileSync(handlerTs));
|
|
102
|
+
installed.push(entry);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return installed;
|
|
106
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type ToolPermission = "auto" | "confirm" | "deny";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PERMISSIONS: Record<string, ToolPermission> = {
|
|
4
|
+
// Built-in tools — always safe
|
|
5
|
+
datetime: "auto",
|
|
6
|
+
memory_write: "auto",
|
|
7
|
+
memory_search: "auto",
|
|
8
|
+
manage_skills: "auto",
|
|
9
|
+
cron_create: "auto",
|
|
10
|
+
cron_list: "auto",
|
|
11
|
+
cron_delete: "auto",
|
|
12
|
+
|
|
13
|
+
// Secrets — set/list are safe, delete requires confirmation
|
|
14
|
+
secret_set: "auto",
|
|
15
|
+
secret_list: "auto",
|
|
16
|
+
secret_delete: "confirm",
|
|
17
|
+
|
|
18
|
+
// Integrations
|
|
19
|
+
connect_service: "auto",
|
|
20
|
+
|
|
21
|
+
// Agent delegation
|
|
22
|
+
delegate: "auto",
|
|
23
|
+
manage_agents: "auto",
|
|
24
|
+
|
|
25
|
+
// Built-in skills — safe
|
|
26
|
+
web_search: "auto",
|
|
27
|
+
url_fetch: "auto",
|
|
28
|
+
file_read: "auto",
|
|
29
|
+
http_request: "auto",
|
|
30
|
+
|
|
31
|
+
// Built-in skills — potentially dangerous
|
|
32
|
+
shell: "confirm",
|
|
33
|
+
file_write: "confirm",
|
|
34
|
+
|
|
35
|
+
// Integration skills — posting requires confirmation
|
|
36
|
+
twitter_posts: "confirm",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns the permission level for a tool.
|
|
41
|
+
* Unknown tools (user-installed skills) default to "auto".
|
|
42
|
+
*/
|
|
43
|
+
export function getToolPermission(name: string): ToolPermission {
|
|
44
|
+
return DEFAULT_PERMISSIONS[name] ?? "auto";
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { LlmToolDef } from "../llm/provider";
|
|
2
|
+
|
|
3
|
+
export interface ToolHandler {
|
|
4
|
+
definition: LlmToolDef;
|
|
5
|
+
execute: (input: Record<string, unknown>) => Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const tools = new Map<string, ToolHandler>();
|
|
9
|
+
|
|
10
|
+
// Track which tools were loaded from the user skills directory (for sandboxing)
|
|
11
|
+
const userInstalledSkills = new Set<string>();
|
|
12
|
+
|
|
13
|
+
export function registerTool(handler: ToolHandler, isUserSkill: boolean = false) {
|
|
14
|
+
tools.set(handler.definition.name, handler);
|
|
15
|
+
if (isUserSkill) {
|
|
16
|
+
userInstalledSkills.add(handler.definition.name);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isUserInstalledSkill(name: string): boolean {
|
|
21
|
+
return userInstalledSkills.has(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getTool(name: string): ToolHandler | undefined {
|
|
25
|
+
return tools.get(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getAllToolDefs(): LlmToolDef[] {
|
|
29
|
+
return Array.from(tools.values()).map((t) => t.definition);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function unregisterTool(name: string): boolean {
|
|
33
|
+
userInstalledSkills.delete(name);
|
|
34
|
+
return tools.delete(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getAllTools(): Map<string, ToolHandler> {
|
|
38
|
+
return tools;
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Sandbox runner — entry point for isolated skill execution.
|
|
4
|
+
* Reads handler path + input from argv, runs the handler, writes JSON result to stdout.
|
|
5
|
+
* No access to parent process globals.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { realpathSync } from "fs";
|
|
11
|
+
|
|
12
|
+
const [handlerPath, inputJson] = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
if (!handlerPath || !inputJson) {
|
|
15
|
+
console.error("Usage: sandbox-runner.ts <handlerPath> <inputJson>");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate handler path is within the user's skills directory (resolve symlinks)
|
|
20
|
+
let resolvedHandler: string;
|
|
21
|
+
let skillsDir: string;
|
|
22
|
+
try {
|
|
23
|
+
resolvedHandler = realpathSync(handlerPath);
|
|
24
|
+
skillsDir = realpathSync(join(process.env.HOME ?? "", ".zubo", "workspace", "skills"));
|
|
25
|
+
} catch {
|
|
26
|
+
console.error("Handler path does not exist or is not accessible");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (!resolvedHandler.startsWith(skillsDir + "/")) {
|
|
30
|
+
console.error("Handler path must be within the skills directory");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Set up minimal globalThis.Zubo with secrets from env
|
|
35
|
+
(globalThis as any).Zubo = {
|
|
36
|
+
getSecret(name: string): string | undefined {
|
|
37
|
+
return process.env[`ZUBO_SECRET_${name.toUpperCase()}`];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const input = JSON.parse(inputJson);
|
|
43
|
+
const mod = await import(handlerPath);
|
|
44
|
+
const handler = mod.default;
|
|
45
|
+
|
|
46
|
+
if (typeof handler !== "function") {
|
|
47
|
+
console.error("Handler must export a default function");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await handler(input);
|
|
52
|
+
console.log(JSON.stringify(result));
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
console.error(err.message ?? String(err));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { logger } from "../util/logger";
|
|
3
|
+
|
|
4
|
+
const SANDBOX_RUNNER = join(import.meta.dir, "sandbox-runner.ts");
|
|
5
|
+
|
|
6
|
+
/** Resolve the directory containing the `bun` binary once at import time. */
|
|
7
|
+
const BUN_BIN_DIR = (() => {
|
|
8
|
+
try {
|
|
9
|
+
return dirname(Bun.which("bun") ?? process.execPath);
|
|
10
|
+
} catch {
|
|
11
|
+
return dirname(process.execPath);
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
export interface SandboxOptions {
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Execute a skill handler in a sandboxed subprocess.
|
|
22
|
+
* The handler runs in a separate Bun process with no access to the parent's globals.
|
|
23
|
+
* Secrets are passed via environment variables.
|
|
24
|
+
*/
|
|
25
|
+
export async function executeSandboxed(
|
|
26
|
+
handlerPath: string,
|
|
27
|
+
input: Record<string, unknown>,
|
|
28
|
+
options: SandboxOptions = {}
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
31
|
+
|
|
32
|
+
const proc = Bun.spawn(
|
|
33
|
+
["bun", "run", SANDBOX_RUNNER, handlerPath, JSON.stringify(input)],
|
|
34
|
+
{
|
|
35
|
+
stdout: "pipe",
|
|
36
|
+
stderr: "pipe",
|
|
37
|
+
env: {
|
|
38
|
+
...options.env,
|
|
39
|
+
// Minimal env — don't pass full parent env to prevent secret leakage
|
|
40
|
+
HOME: process.env.HOME ?? "",
|
|
41
|
+
PATH: `${BUN_BIN_DIR}:/usr/local/bin:/usr/bin:/bin`,
|
|
42
|
+
NODE_ENV: process.env.NODE_ENV ?? "production",
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Timeout enforcement
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
try {
|
|
50
|
+
proc.kill();
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
logger.warn("Failed to kill timed-out sandbox process", { error: (err as Error).message });
|
|
53
|
+
}
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const exitCode = await proc.exited;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
|
|
60
|
+
const stdout = await new Response(proc.stdout).text();
|
|
61
|
+
const stderr = await new Response(proc.stderr).text();
|
|
62
|
+
|
|
63
|
+
if (exitCode !== 0) {
|
|
64
|
+
const errMsg = stderr.trim() || `Process exited with code ${exitCode}`;
|
|
65
|
+
logger.error("Sandboxed skill error", { handlerPath, exitCode, stderr: errMsg.slice(0, 500) });
|
|
66
|
+
throw new Error(`Skill execution failed: ${errMsg.slice(0, 200)}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Parse the JSON result from stdout
|
|
70
|
+
try {
|
|
71
|
+
const result = JSON.parse(stdout.trim());
|
|
72
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
73
|
+
} catch {
|
|
74
|
+
// If not valid JSON, return raw stdout
|
|
75
|
+
return stdout.trim();
|
|
76
|
+
}
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
if (err.message?.includes("Skill execution failed")) throw err;
|
|
80
|
+
throw new Error(`Sandbox error: ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const BUILTIN_SKILLS_DIR = join(import.meta.dir, "builtin-skills");
|
|
5
|
+
|
|
6
|
+
export function getBuiltinSkillNames(): string[] {
|
|
7
|
+
try {
|
|
8
|
+
return readdirSync(BUILTIN_SKILLS_DIR).filter((entry) => {
|
|
9
|
+
const srcDir = join(BUILTIN_SKILLS_DIR, entry);
|
|
10
|
+
return existsSync(join(srcDir, "SKILL.md")) && existsSync(join(srcDir, "handler.ts"));
|
|
11
|
+
});
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function reinstallBuiltinSkill(targetDir: string, skillName: string): boolean {
|
|
18
|
+
const srcDir = join(BUILTIN_SKILLS_DIR, skillName);
|
|
19
|
+
const destDir = join(targetDir, skillName);
|
|
20
|
+
|
|
21
|
+
const skillMd = join(srcDir, "SKILL.md");
|
|
22
|
+
const handlerTs = join(srcDir, "handler.ts");
|
|
23
|
+
|
|
24
|
+
if (!existsSync(skillMd) || !existsSync(handlerTs)) return false;
|
|
25
|
+
|
|
26
|
+
mkdirSync(destDir, { recursive: true });
|
|
27
|
+
writeFileSync(join(destDir, "SKILL.md"), readFileSync(skillMd));
|
|
28
|
+
writeFileSync(join(destDir, "handler.ts"), readFileSync(handlerTs));
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function installBuiltinSkills(targetDir: string): string[] {
|
|
34
|
+
if (!existsSync(targetDir)) {
|
|
35
|
+
mkdirSync(targetDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const installed: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const entry of getBuiltinSkillNames()) {
|
|
41
|
+
const destDir = join(targetDir, entry);
|
|
42
|
+
|
|
43
|
+
// Skip if already exists (preserve user edits)
|
|
44
|
+
if (existsSync(destDir)) continue;
|
|
45
|
+
|
|
46
|
+
if (reinstallBuiltinSkill(targetDir, entry)) {
|
|
47
|
+
installed.push(entry);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return installed;
|
|
52
|
+
}
|