zubo 0.1.8 → 0.1.11
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/package.json +1 -1
- package/site/index.html +3 -3
- package/src/agent/delegate.ts +2 -2
- package/src/agent/prompts.ts +33 -3
- package/src/channels/email.ts +6 -2
- package/src/channels/webchat.ts +24 -9
- package/src/channels/whatsapp.ts +8 -3
- package/src/llm/claude-code.ts +10 -5
- package/src/llm/codex.ts +10 -5
- package/src/llm/factory.ts +1 -0
- package/src/llm/openai-compat.ts +2 -1
- package/src/llm/smart-router.ts +0 -10
- package/src/setup.ts +18 -3
- package/src/tools/builtin/delegate.ts +3 -1
- package/src/tools/builtin/google-oauth.ts +4 -0
- package/src/tools/builtin/manage-agents.ts +1 -0
- package/src/tools/oauth.ts +5 -0
- package/src/tools/permissions.ts +14 -0
- package/src/util/costs.ts +4 -0
- package/tests/executor.test.ts +18 -4
- package/tests/permissions.test.ts +3 -3
- package/tests/tools/executor.test.ts +33 -13
package/package.json
CHANGED
package/site/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Zubo — Your AI Agent. Your Machine. Your Rules.</title>
|
|
7
|
-
<meta name="description" content="Open-source AI agent that runs on your machine. One command to install, one file to configure. Persistent memory, 25+ tools, 7 messaging channels,
|
|
7
|
+
<meta name="description" content="Open-source AI agent that runs on your machine. One command to install, one file to configure. Persistent memory, 25+ tools, 7 messaging channels, 12+ LLM providers. MCP compatible. Zero complexity.">
|
|
8
8
|
<meta name="theme-color" content="#060608">
|
|
9
9
|
<link rel="canonical" href="https://zubo.bot/">
|
|
10
10
|
<meta property="og:title" content="Zubo — Your AI Agent. Your Machine. Your Rules.">
|
|
@@ -305,11 +305,11 @@
|
|
|
305
305
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="7.5 4.21 12 6.81 16.5 4.21"/><polyline points="7.5 19.79 7.5 14.6 3 12"/><polyline points="21 12 16.5 14.6 16.5 19.79"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
|
|
306
306
|
</div>
|
|
307
307
|
<h3>Any LLM, Your Choice</h3>
|
|
308
|
-
<p>
|
|
308
|
+
<p>12+ providers including Anthropic, OpenAI, MiniMax, Ollama, Groq, DeepSeek, and more. Smart routing sends simple queries to fast, cheap models automatically — so you save money without thinking about it.</p>
|
|
309
309
|
<div class="bento-models">
|
|
310
310
|
<span class="model-tag">Claude</span>
|
|
311
311
|
<span class="model-tag">GPT-4o</span>
|
|
312
|
-
<span class="model-tag">
|
|
312
|
+
<span class="model-tag">MiniMax</span>
|
|
313
313
|
<span class="model-tag">DeepSeek</span>
|
|
314
314
|
<span class="model-tag">Llama</span>
|
|
315
315
|
<span class="model-tag">Mistral</span>
|
package/src/agent/delegate.ts
CHANGED
|
@@ -104,8 +104,8 @@ export async function delegateToAgent(
|
|
|
104
104
|
|
|
105
105
|
// Filter out privileged tools from sub-agents to prevent escalation
|
|
106
106
|
const FORBIDDEN_DELEGATION_TOOLS = new Set([
|
|
107
|
-
"delegate", "
|
|
108
|
-
"secret_set", "secret_delete", "manage_skills",
|
|
107
|
+
"delegate", "delegate_task", "manage_agents",
|
|
108
|
+
"config_update", "secret_set", "secret_delete", "manage_skills",
|
|
109
109
|
]);
|
|
110
110
|
const filteredTools = agent.tools.filter(
|
|
111
111
|
(t) => !FORBIDDEN_DELEGATION_TOOLS.has(t)
|
package/src/agent/prompts.ts
CHANGED
|
@@ -5,7 +5,7 @@ const DEFAULT_PERSONALITY = `You are Zubo, a personal AI agent. You are friendly
|
|
|
5
5
|
|
|
6
6
|
## How you behave
|
|
7
7
|
|
|
8
|
-
**Act first.** When the user asks you to do something, do it. Don't describe what you could do — use your tools and make it happen. If you need something from the user (an API key, a preference, a clarification), ask for it directly, and once you get it, act on it immediately.
|
|
8
|
+
**Act first.** When the user asks you to do something, do it immediately. Don't describe what you could do — use your tools and make it happen. Don't ask for permission to do what the user just asked you to do (e.g. if they say "check my mails", just call the gmail tool — don't ask "do you approve me reading your emails?"). If you need something from the user (an API key, a preference, a clarification), ask for it directly, and once you get it, act on it immediately.
|
|
9
9
|
|
|
10
10
|
**Be concise.** Answer in the fewest words that fully address the question. No filler, no preamble. Long explanations only when explicitly asked.
|
|
11
11
|
|
|
@@ -47,9 +47,15 @@ const DEFAULT_PERSONALITY = `You are Zubo, a personal AI agent. You are friendly
|
|
|
47
47
|
- Delegate tasks using the delegate tool. Sub-agents share your memory but have scoped tools.
|
|
48
48
|
- Keep the main conversation lightweight. Offload complex, self-contained tasks.
|
|
49
49
|
|
|
50
|
-
## Connecting services
|
|
50
|
+
## Connecting services (integrations)
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Service integrations and LLM providers are COMPLETELY SEPARATE concepts. Never confuse them:
|
|
53
|
+
- **Integrations** = external services like Gmail, GitHub, Notion (connected via OAuth or API tokens)
|
|
54
|
+
- **LLM providers** = the AI model you use for thinking (Anthropic, OpenAI, etc.)
|
|
55
|
+
- If Gmail access is broken, the fix is to re-authenticate Google OAuth — NOT to change the LLM provider. Never suggest changing LLM providers as a fix for integration issues.
|
|
56
|
+
|
|
57
|
+
How to connect services:
|
|
58
|
+
- **Google** (Gmail, Calendar, Drive): Requires OAuth 2.0. Need both client_id (ends with .apps.googleusercontent.com) and client_secret (starts with GOCSPX-) from Google Cloud Console. The OAuth app type should be "Desktop app" (NOT "Web application"). Use google_oauth tool to manage the full flow.
|
|
53
59
|
- **GitHub**: Personal Access Token. Store as github_token via secret_set.
|
|
54
60
|
- **Notion**: Internal Integration Token from notion.so/my-integrations. Store as notion_token.
|
|
55
61
|
- **Linear**: Personal API Key from Linear > Settings > API. Store as linear_token.
|
|
@@ -58,6 +64,30 @@ const DEFAULT_PERSONALITY = `You are Zubo, a personal AI agent. You are friendly
|
|
|
58
64
|
- **Twitter/X**: Bearer token for reading, full OAuth keys for posting. Store as twitter_bearer_token.
|
|
59
65
|
- When the user says "connect my GitHub" or similar, ask for the credentials, store them with secret_set, then call connect_service.
|
|
60
66
|
|
|
67
|
+
When Google OAuth expires or a Google tool returns an auth error:
|
|
68
|
+
- Call google_oauth with action "start" (with NO parameters). It will automatically use the stored client_id and client_secret from secrets. Do NOT ask the user to re-provide credentials that are already saved.
|
|
69
|
+
- Only ask the user for credentials if google_oauth returns an error saying credentials are missing.
|
|
70
|
+
- For non-Google services, ask the user to provide a new token and store it with secret_set.
|
|
71
|
+
- NEVER suggest unrelated solutions like changing the LLM provider.
|
|
72
|
+
- NEVER try to guide the user through OAuth setup manually — always use the google_oauth tool.
|
|
73
|
+
|
|
74
|
+
## LLM providers
|
|
75
|
+
|
|
76
|
+
You support 12+ LLM providers. The user can switch at any time using config_update.
|
|
77
|
+
|
|
78
|
+
**API-based providers** (need an API key):
|
|
79
|
+
- Anthropic (Claude), OpenAI (GPT), MiniMax (M2.5), Groq, Together, DeepSeek, xAI (Grok), Fireworks, Cerebras, Perplexity, OpenRouter
|
|
80
|
+
- To set up: config_update with path "providers.<name>" value {"apiKey":"...","model":"..."}, then set "activeProvider" to "<name>".
|
|
81
|
+
|
|
82
|
+
**Local providers** (no API key needed):
|
|
83
|
+
- Ollama, LM Studio — run models locally
|
|
84
|
+
|
|
85
|
+
**CLI-based providers** (NO API key needed — they use the user's own CLI authentication):
|
|
86
|
+
- **Claude Code** (provider name: "claude-code"): Spawns the Claude Code CLI. The user must have it installed and authenticated on their machine. To activate: config_update with path "providers.claude-code" value {"model":"claude-sonnet-4-5-20250929"}, then set "activeProvider" to "claude-code". NO apiKey field needed.
|
|
87
|
+
- **OpenAI Codex** (provider name: "codex"): Spawns the Codex CLI. The user must have it installed and authenticated on their machine. To activate: config_update with path "providers.codex" value {"model":"o4-mini"}, then set "activeProvider" to "codex". NO apiKey field needed.
|
|
88
|
+
|
|
89
|
+
IMPORTANT: When a user says "use codex" or "use claude code", do NOT ask for an API key. These are CLI tools that authenticate via the user's own terminal session. Just set the provider config and activate it.
|
|
90
|
+
|
|
61
91
|
## Tool confirmation
|
|
62
92
|
|
|
63
93
|
Some tools (shell, file_write) require user confirmation. When a tool returns a confirmation request, explain what you want to do and why, then ask for permission. Never set _confirmed without explicit user approval.
|
package/src/channels/email.ts
CHANGED
|
@@ -154,8 +154,12 @@ class ImapClient {
|
|
|
154
154
|
const bodyMatch = result.match(/\r\n\r\n([\s\S]*?)(?:\r\n\)|\r\nA\d+)/);
|
|
155
155
|
if (bodyMatch) body = bodyMatch[1].trim();
|
|
156
156
|
|
|
157
|
-
// Strip HTML tags
|
|
158
|
-
body = body
|
|
157
|
+
// Strip script/style blocks and all HTML tags
|
|
158
|
+
body = body
|
|
159
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
|
|
160
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "")
|
|
161
|
+
.replace(/<[^>]+>/g, "")
|
|
162
|
+
.trim();
|
|
159
163
|
// Decode basic quoted-printable
|
|
160
164
|
body = body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
161
165
|
|
package/src/channels/webchat.ts
CHANGED
|
@@ -1109,29 +1109,32 @@ function handleDashboardApi(url: URL, req: Request): Response | null {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
|
-
// GET /api/dashboard/secrets/:name —
|
|
1112
|
+
// GET /api/dashboard/secrets/:name — reveal a secret value (dashboard is localhost-only + session-authenticated)
|
|
1113
1113
|
if (path.startsWith("/secrets/") && req.method === "GET") {
|
|
1114
1114
|
const secretName = decodeURIComponent(path.replace("/secrets/", ""));
|
|
1115
1115
|
if (!secretName || !/^[a-z0-9_]+$/.test(secretName)) {
|
|
1116
1116
|
return Response.json({ error: "Invalid secret name" }, { status: 400 });
|
|
1117
1117
|
}
|
|
1118
1118
|
try {
|
|
1119
|
+
const { maskToken } = require("../util/mask") as { maskToken: (s: string) => string };
|
|
1119
1120
|
// Check config provider keys first
|
|
1120
1121
|
if (secretName.endsWith("_api_key")) {
|
|
1121
1122
|
const provider = secretName.replace(/_api_key$/, "");
|
|
1122
1123
|
try {
|
|
1123
1124
|
const cfg = JSON.parse(readFileSync(paths.config, "utf-8"));
|
|
1124
1125
|
if (cfg.providers?.[provider]?.apiKey) {
|
|
1125
|
-
|
|
1126
|
+
const key = cfg.providers[provider].apiKey;
|
|
1127
|
+
return Response.json({ name: secretName, value: maskToken(key), source: "config" });
|
|
1126
1128
|
}
|
|
1127
1129
|
} catch (err: any) {
|
|
1128
1130
|
logger.warn("Failed to read provider secret from config", { error: (err as Error).message });
|
|
1129
1131
|
}
|
|
1130
1132
|
}
|
|
1131
1133
|
const db = getDb();
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
return Response.json({
|
|
1134
|
+
const { getSecret } = require("../secrets/store") as { getSecret: (name: string) => string | null };
|
|
1135
|
+
const value = getSecret(secretName);
|
|
1136
|
+
if (!value) return Response.json({ error: "Not found" }, { status: 404 });
|
|
1137
|
+
return Response.json({ name: secretName, value: maskToken(value), source: "secrets" });
|
|
1135
1138
|
} catch (err: any) {
|
|
1136
1139
|
return Response.json({ error: err.message }, { status: 500 });
|
|
1137
1140
|
}
|
|
@@ -2013,8 +2016,15 @@ async function handleRequest(
|
|
|
2013
2016
|
// CORS protection: reject cross-origin API requests
|
|
2014
2017
|
if (url.pathname.startsWith("/api/")) {
|
|
2015
2018
|
const origin = req.headers.get("origin");
|
|
2016
|
-
if (origin
|
|
2017
|
-
|
|
2019
|
+
if (origin) {
|
|
2020
|
+
const actualPort = server?.port ?? port;
|
|
2021
|
+
const allowed = [
|
|
2022
|
+
`http://localhost:${actualPort}`,
|
|
2023
|
+
`http://127.0.0.1:${actualPort}`,
|
|
2024
|
+
];
|
|
2025
|
+
if (!allowed.includes(origin)) {
|
|
2026
|
+
return Response.json({ error: "Cross-origin requests are not allowed" }, { status: 403 });
|
|
2027
|
+
}
|
|
2018
2028
|
}
|
|
2019
2029
|
}
|
|
2020
2030
|
|
|
@@ -2038,15 +2048,20 @@ async function handleRequest(
|
|
|
2038
2048
|
});
|
|
2039
2049
|
}
|
|
2040
2050
|
|
|
2041
|
-
// --- OAuth routes
|
|
2051
|
+
// --- OAuth routes ---
|
|
2042
2052
|
|
|
2043
|
-
// GET /oauth/:provider/authorize — redirect user to provider's auth page
|
|
2053
|
+
// GET /oauth/:provider/authorize — redirect user to provider's auth page (requires session key)
|
|
2044
2054
|
const ALLOWED_OAUTH_PROVIDERS = new Set(["google", "github", "notion", "linear", "slack"]);
|
|
2045
2055
|
if (url.pathname.match(/^\/oauth\/[a-z]+\/authorize$/) && req.method === "GET") {
|
|
2046
2056
|
const provider = url.pathname.split("/")[2];
|
|
2047
2057
|
if (!ALLOWED_OAUTH_PROVIDERS.has(provider)) {
|
|
2048
2058
|
return Response.json({ error: "Unknown OAuth provider" }, { status: 400 });
|
|
2049
2059
|
}
|
|
2060
|
+
// Require session key to prevent unauthorized OAuth initiation
|
|
2061
|
+
const sk = url.searchParams.get("sk");
|
|
2062
|
+
if (sk !== sessionKey) {
|
|
2063
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2064
|
+
}
|
|
2050
2065
|
try {
|
|
2051
2066
|
const { getAuthUrl } = await import("../tools/oauth");
|
|
2052
2067
|
const actualPort = server?.port ?? port;
|
package/src/channels/whatsapp.ts
CHANGED
|
@@ -13,7 +13,7 @@ export function createWhatsAppAdapter(
|
|
|
13
13
|
let sock: any = null;
|
|
14
14
|
const sessionDir = authDir ?? join(paths.root, "whatsapp-auth");
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const adapter: ChannelAdapter = {
|
|
17
17
|
channelName: "whatsapp",
|
|
18
18
|
|
|
19
19
|
start() {
|
|
@@ -40,8 +40,11 @@ export function createWhatsAppAdapter(
|
|
|
40
40
|
const shouldReconnect =
|
|
41
41
|
lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
|
|
42
42
|
if (shouldReconnect) {
|
|
43
|
-
logger.info("WhatsApp reconnecting...");
|
|
44
|
-
|
|
43
|
+
logger.info("WhatsApp reconnecting in 3s...");
|
|
44
|
+
sock = null;
|
|
45
|
+
setTimeout(() => adapter.start(), 3000);
|
|
46
|
+
} else {
|
|
47
|
+
logger.info("WhatsApp logged out — not reconnecting");
|
|
45
48
|
}
|
|
46
49
|
} else if (connection === "open") {
|
|
47
50
|
logger.info("WhatsApp connected");
|
|
@@ -116,4 +119,6 @@ export function createWhatsAppAdapter(
|
|
|
116
119
|
}
|
|
117
120
|
},
|
|
118
121
|
};
|
|
122
|
+
|
|
123
|
+
return adapter;
|
|
119
124
|
}
|
package/src/llm/claude-code.ts
CHANGED
|
@@ -67,11 +67,16 @@ export class ClaudeCodeProvider implements LlmProvider {
|
|
|
67
67
|
try { proc.kill(); } catch {}
|
|
68
68
|
}, 300000);
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
let exitCode: number;
|
|
71
|
+
let stdout: string;
|
|
72
|
+
let stderr: string;
|
|
73
|
+
try {
|
|
74
|
+
exitCode = await proc.exited;
|
|
75
|
+
stdout = await new Response(proc.stdout as ReadableStream).text();
|
|
76
|
+
stderr = await new Response(proc.stderr as ReadableStream).text();
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
if (exitCode !== 0) {
|
|
77
82
|
throw new Error(`Claude Code CLI exited with code ${exitCode}: ${stderr.slice(0, 500)}`);
|
package/src/llm/codex.ts
CHANGED
|
@@ -64,11 +64,16 @@ export class CodexProvider implements LlmProvider {
|
|
|
64
64
|
try { proc.kill(); } catch {}
|
|
65
65
|
}, 300000);
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
let exitCode: number;
|
|
68
|
+
let stdout: string;
|
|
69
|
+
let stderr: string;
|
|
70
|
+
try {
|
|
71
|
+
exitCode = await proc.exited;
|
|
72
|
+
stdout = await new Response(proc.stdout as ReadableStream).text();
|
|
73
|
+
stderr = await new Response(proc.stderr as ReadableStream).text();
|
|
74
|
+
} finally {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
}
|
|
72
77
|
|
|
73
78
|
if (exitCode !== 0) {
|
|
74
79
|
throw new Error(`Codex CLI exited with code ${exitCode}: ${stderr.slice(0, 500)}`);
|
package/src/llm/factory.ts
CHANGED
|
@@ -18,6 +18,7 @@ const KNOWN_BASE_URLS: Record<string, string> = {
|
|
|
18
18
|
cerebras: "https://api.cerebras.ai/v1",
|
|
19
19
|
perplexity: "https://api.perplexity.ai",
|
|
20
20
|
xai: "https://api.x.ai/v1",
|
|
21
|
+
minimax: "https://api.minimax.io/v1",
|
|
21
22
|
ollama: "http://localhost:11434/v1",
|
|
22
23
|
lmstudio: "http://localhost:1234/v1",
|
|
23
24
|
};
|
package/src/llm/openai-compat.ts
CHANGED
|
@@ -28,6 +28,7 @@ const DEFAULT_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
28
28
|
cerebras: 128_000,
|
|
29
29
|
perplexity: 128_000,
|
|
30
30
|
xai: 131_072, // Grok 4.1 supports up to 2M; safe default
|
|
31
|
+
minimax: 204_800, // MiniMax M2.5
|
|
31
32
|
ollama: 8_000,
|
|
32
33
|
lmstudio: 8_000,
|
|
33
34
|
};
|
|
@@ -375,7 +376,7 @@ export class OpenAICompatProvider implements LlmProvider {
|
|
|
375
376
|
} finally {
|
|
376
377
|
reader.releaseLock();
|
|
377
378
|
// Ensure the response body is fully consumed/cancelled to free resources
|
|
378
|
-
try { await res.body
|
|
379
|
+
try { await res.body?.cancel(); } catch (err: any) { logger.warn("Failed to cancel response body stream", { error: (err as Error).message }); }
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
// Emit tool_use_end for all tool calls
|
package/src/llm/smart-router.ts
CHANGED
|
@@ -147,8 +147,6 @@ export class SmartRouterProvider implements LlmProvider {
|
|
|
147
147
|
model: this.fast.model,
|
|
148
148
|
reason: "simple query",
|
|
149
149
|
});
|
|
150
|
-
this.providerName = this.fast.providerName;
|
|
151
|
-
this.model = this.fast.model;
|
|
152
150
|
return this.fast;
|
|
153
151
|
}
|
|
154
152
|
|
|
@@ -157,8 +155,6 @@ export class SmartRouterProvider implements LlmProvider {
|
|
|
157
155
|
model: this.primary.model,
|
|
158
156
|
reason: "complex query",
|
|
159
157
|
});
|
|
160
|
-
this.providerName = this.primary.providerName;
|
|
161
|
-
this.model = this.primary.model;
|
|
162
158
|
return this.primary;
|
|
163
159
|
}
|
|
164
160
|
|
|
@@ -172,8 +168,6 @@ export class SmartRouterProvider implements LlmProvider {
|
|
|
172
168
|
logger.warn("Fast model failed, falling back to primary", {
|
|
173
169
|
error: err.message,
|
|
174
170
|
});
|
|
175
|
-
this.providerName = this.primary.providerName;
|
|
176
|
-
this.model = this.primary.model;
|
|
177
171
|
return this.primary.chat(request);
|
|
178
172
|
}
|
|
179
173
|
}
|
|
@@ -212,13 +206,9 @@ export class SmartRouterProvider implements LlmProvider {
|
|
|
212
206
|
}
|
|
213
207
|
|
|
214
208
|
// Fallback to primary stream
|
|
215
|
-
this.providerName = this.primary.providerName;
|
|
216
|
-
this.model = this.primary.model;
|
|
217
209
|
} else {
|
|
218
210
|
// Fast model has no streaming, fall back to primary
|
|
219
211
|
logger.info("Fast model has no streaming support, using primary");
|
|
220
|
-
this.providerName = this.primary.providerName;
|
|
221
|
-
this.model = this.primary.model;
|
|
222
212
|
}
|
|
223
213
|
}
|
|
224
214
|
|
package/src/setup.ts
CHANGED
|
@@ -168,6 +168,21 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
|
|
|
168
168
|
},
|
|
169
169
|
{
|
|
170
170
|
key: "9",
|
|
171
|
+
label: "MiniMax (M2.5)",
|
|
172
|
+
setup: async () => {
|
|
173
|
+
const apiKey = await prompt(" MiniMax API key: ");
|
|
174
|
+
const model = await prompt(" Model [MiniMax-M2.5] (or MiniMax-M2.5-highspeed): ");
|
|
175
|
+
return {
|
|
176
|
+
name: "minimax",
|
|
177
|
+
config: {
|
|
178
|
+
apiKey,
|
|
179
|
+
model: model || "MiniMax-M2.5",
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: "10",
|
|
171
186
|
label: "Fireworks AI",
|
|
172
187
|
setup: async () => {
|
|
173
188
|
const apiKey = await prompt(" Fireworks API key: ");
|
|
@@ -183,7 +198,7 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
|
|
|
183
198
|
},
|
|
184
199
|
},
|
|
185
200
|
{
|
|
186
|
-
key: "
|
|
201
|
+
key: "11",
|
|
187
202
|
label: "Cerebras",
|
|
188
203
|
setup: async () => {
|
|
189
204
|
const apiKey = await prompt(" Cerebras API key: ");
|
|
@@ -199,7 +214,7 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
|
|
|
199
214
|
},
|
|
200
215
|
},
|
|
201
216
|
{
|
|
202
|
-
key: "
|
|
217
|
+
key: "12",
|
|
203
218
|
label: "LM Studio (local)",
|
|
204
219
|
setup: async () => {
|
|
205
220
|
const baseUrl = await prompt(" LM Studio URL [http://localhost:1234/v1]: ");
|
|
@@ -215,7 +230,7 @@ const PROVIDER_OPTIONS: ProviderOption[] = [
|
|
|
215
230
|
},
|
|
216
231
|
},
|
|
217
232
|
{
|
|
218
|
-
key: "
|
|
233
|
+
key: "13",
|
|
219
234
|
label: "Other (OpenAI-compatible)",
|
|
220
235
|
setup: async () => {
|
|
221
236
|
const name = await prompt(" Provider name: ");
|
|
@@ -35,7 +35,9 @@ export function registerDelegateTool(llm: LlmProvider) {
|
|
|
35
35
|
|
|
36
36
|
// Dynamic import to avoid circular dependency
|
|
37
37
|
const { delegateToAgent } = await import("../../agent/delegate");
|
|
38
|
-
const
|
|
38
|
+
const crypto = await import("crypto");
|
|
39
|
+
const contextId = "delegation-" + crypto.randomUUID().slice(0, 8);
|
|
40
|
+
const result = await delegateToAgent(llm, agent, task, contextId);
|
|
39
41
|
return result;
|
|
40
42
|
},
|
|
41
43
|
});
|
|
@@ -80,6 +80,10 @@ async function handleStart(
|
|
|
80
80
|
clientId?: string,
|
|
81
81
|
clientSecret?: string
|
|
82
82
|
): Promise<string> {
|
|
83
|
+
// If credentials not provided as parameters, check stored secrets
|
|
84
|
+
if (!clientId) clientId = getSecret("google_client_id") ?? undefined;
|
|
85
|
+
if (!clientSecret) clientSecret = getSecret("google_client_secret") ?? undefined;
|
|
86
|
+
|
|
83
87
|
if (!clientId || !clientSecret) {
|
|
84
88
|
return JSON.stringify({
|
|
85
89
|
error:
|
|
@@ -75,6 +75,7 @@ export function registerManageAgentsTool() {
|
|
|
75
75
|
const FORBIDDEN_SUBAGENT_TOOLS = [
|
|
76
76
|
"manage_agents", // Prevents recursive agent creation
|
|
77
77
|
"delegate", // Prevents delegation loops
|
|
78
|
+
"delegate_task", // Prevents ad-hoc delegation loops
|
|
78
79
|
"config_update", // Prevents config tampering
|
|
79
80
|
"secret_set", // Prevents secret manipulation
|
|
80
81
|
"secret_delete", // Prevents secret deletion
|
package/src/tools/oauth.ts
CHANGED
|
@@ -433,6 +433,11 @@ export async function getToken(provider: string): Promise<string | null> {
|
|
|
433
433
|
return await refreshPromise;
|
|
434
434
|
} catch (err: any) {
|
|
435
435
|
logger.warn(`[OAuth] Token refresh failed for ${provider}`, { error: err.message });
|
|
436
|
+
// If refresh token is invalid/revoked, remove stale token to stop retry loops
|
|
437
|
+
if (err.message?.includes("400") || err.message?.includes("401") || err.message?.includes("invalid")) {
|
|
438
|
+
logger.warn(`[OAuth] Removing stale token for ${provider} — user must re-authenticate`);
|
|
439
|
+
try { revokeToken(provider); } catch {}
|
|
440
|
+
}
|
|
436
441
|
return null;
|
|
437
442
|
}
|
|
438
443
|
}
|
package/src/tools/permissions.ts
CHANGED
|
@@ -39,6 +39,20 @@ const DEFAULT_PERMISSIONS: Record<string, ToolPermission> = {
|
|
|
39
39
|
image_generate: "auto",
|
|
40
40
|
google_oauth: "auto",
|
|
41
41
|
|
|
42
|
+
// Integration skills — auto because user explicitly requests these actions
|
|
43
|
+
gmail: "auto",
|
|
44
|
+
google_calendar: "auto",
|
|
45
|
+
google_sheets: "auto",
|
|
46
|
+
google_docs: "auto",
|
|
47
|
+
google_drive: "auto",
|
|
48
|
+
github_issues: "auto",
|
|
49
|
+
github_repos: "auto",
|
|
50
|
+
github_prs: "auto",
|
|
51
|
+
notion_pages: "auto",
|
|
52
|
+
linear_issues: "auto",
|
|
53
|
+
jira_issues: "auto",
|
|
54
|
+
slack_messages: "auto",
|
|
55
|
+
|
|
42
56
|
// Built-in skills — require confirmation (network writes, code execution, system access)
|
|
43
57
|
http_request: "confirm",
|
|
44
58
|
code_interpreter: "confirm",
|
package/src/util/costs.ts
CHANGED
|
@@ -24,6 +24,10 @@ const MODEL_PRICING: Record<string, { input: number; output: number }> = {
|
|
|
24
24
|
"deepseek-chat": { input: 0.56, output: 1.68 },
|
|
25
25
|
"deepseek-reasoner": { input: 0.55, output: 2.19 },
|
|
26
26
|
|
|
27
|
+
// MiniMax
|
|
28
|
+
"MiniMax-M2.5": { input: 0.3, output: 1.2 },
|
|
29
|
+
"MiniMax-M2.5-highspeed": { input: 0.3, output: 2.4 },
|
|
30
|
+
|
|
27
31
|
// Groq
|
|
28
32
|
"llama-3.3-70b-versatile": { input: 0.59, output: 0.79 },
|
|
29
33
|
"llama-3.1-8b-instant": { input: 0.05, output: 0.08 },
|
package/tests/executor.test.ts
CHANGED
|
@@ -22,11 +22,18 @@ describe("Tool Executor", () => {
|
|
|
22
22
|
unregisterTool("error_tool");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it("should execute a registered tool", async () => {
|
|
26
|
-
|
|
25
|
+
it("should execute a registered tool (with confirmation)", async () => {
|
|
26
|
+
// Unknown tools default to "confirm" — first call returns confirmation token
|
|
27
|
+
const confirmResult = await executeTool("test_tool", "t1", { msg: "hello" });
|
|
28
|
+
expect(confirmResult.content).toContain("Confirmation Token:");
|
|
29
|
+
const tokenMatch = confirmResult.content.match(/Confirmation Token: ([a-f0-9-]+)/);
|
|
30
|
+
expect(tokenMatch).toBeTruthy();
|
|
31
|
+
|
|
32
|
+
// Second call with token executes the tool
|
|
33
|
+
const result = await executeTool("test_tool", "t1b", { msg: "hello", _confirmToken: tokenMatch![1] });
|
|
27
34
|
expect(result.content).toBe("echo: hello");
|
|
28
35
|
expect(result.is_error).toBe(false);
|
|
29
|
-
expect(result.tool_use_id).toBe("
|
|
36
|
+
expect(result.tool_use_id).toBe("t1b");
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
it("should return error for unknown tool", async () => {
|
|
@@ -53,7 +60,14 @@ describe("Tool Executor", () => {
|
|
|
53
60
|
},
|
|
54
61
|
});
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
// Unknown tools default to "confirm" — first call returns confirmation token
|
|
64
|
+
const confirmResult = await executeTool("error_tool", "t4", {});
|
|
65
|
+
expect(confirmResult.content).toContain("Confirmation Token:");
|
|
66
|
+
const tokenMatch = confirmResult.content.match(/Confirmation Token: ([a-f0-9-]+)/);
|
|
67
|
+
expect(tokenMatch).toBeTruthy();
|
|
68
|
+
|
|
69
|
+
// Second call with token — tool throws
|
|
70
|
+
const result = await executeTool("error_tool", "t4b", { _confirmToken: tokenMatch![1] });
|
|
57
71
|
expect(result.is_error).toBe(true);
|
|
58
72
|
expect(result.content).toContain("intentional failure");
|
|
59
73
|
});
|
|
@@ -14,8 +14,8 @@ describe("getToolPermission", () => {
|
|
|
14
14
|
expect(getToolPermission("file_write")).toBe("confirm");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it("returns '
|
|
18
|
-
expect(getToolPermission("my_custom_skill")).toBe("
|
|
19
|
-
expect(getToolPermission("weather")).toBe("
|
|
17
|
+
it("returns 'confirm' for unknown tools (user-installed skills, MCP)", () => {
|
|
18
|
+
expect(getToolPermission("my_custom_skill")).toBe("confirm");
|
|
19
|
+
expect(getToolPermission("weather")).toBe("confirm");
|
|
20
20
|
});
|
|
21
21
|
});
|
|
@@ -41,13 +41,24 @@ describe("executeTool", () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
-
|
|
44
|
+
// Unknown tools default to "confirm" — first call returns confirmation token
|
|
45
|
+
const confirmResult = await executeTool(
|
|
45
46
|
testToolName,
|
|
46
47
|
"call-789",
|
|
47
48
|
{},
|
|
48
49
|
[testToolName]
|
|
49
50
|
);
|
|
51
|
+
expect(confirmResult.content).toContain("Confirmation Token:");
|
|
52
|
+
const tokenMatch = confirmResult.content.match(/Confirmation Token: ([a-f0-9-]+)/);
|
|
53
|
+
expect(tokenMatch).toBeTruthy();
|
|
50
54
|
|
|
55
|
+
// Second call with token executes the tool
|
|
56
|
+
const result = await executeTool(
|
|
57
|
+
testToolName,
|
|
58
|
+
"call-789b",
|
|
59
|
+
{ _confirmToken: tokenMatch![1] },
|
|
60
|
+
[testToolName]
|
|
61
|
+
);
|
|
51
62
|
expect(result.is_error).toBe(false);
|
|
52
63
|
expect(result.content).toBe("success");
|
|
53
64
|
} finally {
|
|
@@ -56,15 +67,6 @@ describe("executeTool", () => {
|
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
test("returns a denied error for tools with deny permission", async () => {
|
|
59
|
-
// Register a tool and give it the "deny" permission by temporarily
|
|
60
|
-
// adding it to the permissions map. Since getToolPermission defaults
|
|
61
|
-
// to "auto" for unknown tools, we test with the "shell" tool which
|
|
62
|
-
// has "confirm" permission instead. Let's test the confirm flow.
|
|
63
|
-
// We'll test denied tools by checking the mechanism works correctly.
|
|
64
|
-
|
|
65
|
-
// The "shell" tool has "confirm" permission in the default map.
|
|
66
|
-
// For a truly denied tool, we'd need to modify the permissions map.
|
|
67
|
-
// Instead, let's verify that when allowedTools blocks a tool, it works.
|
|
68
70
|
const result = await executeTool(
|
|
69
71
|
"shell",
|
|
70
72
|
"call-deny",
|
|
@@ -92,13 +94,23 @@ describe("executeTool", () => {
|
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
try {
|
|
95
|
-
|
|
97
|
+
// First call: get confirmation token (unknown tools default to "confirm")
|
|
98
|
+
const confirmResult = await executeTool(testToolName, "call-exec", {
|
|
96
99
|
message: "hello",
|
|
97
100
|
});
|
|
101
|
+
expect(confirmResult.content).toContain("Confirmation Token:");
|
|
102
|
+
const tokenMatch = confirmResult.content.match(/Confirmation Token: ([a-f0-9-]+)/);
|
|
103
|
+
expect(tokenMatch).toBeTruthy();
|
|
104
|
+
|
|
105
|
+
// Second call with token: actually execute
|
|
106
|
+
const result = await executeTool(testToolName, "call-exec2", {
|
|
107
|
+
message: "hello",
|
|
108
|
+
_confirmToken: tokenMatch![1],
|
|
109
|
+
});
|
|
98
110
|
|
|
99
111
|
expect(result.is_error).toBe(false);
|
|
100
112
|
expect(result.content).toBe("echo: hello");
|
|
101
|
-
expect(result.tool_use_id).toBe("call-
|
|
113
|
+
expect(result.tool_use_id).toBe("call-exec2");
|
|
102
114
|
} finally {
|
|
103
115
|
unregisterTool(testToolName);
|
|
104
116
|
}
|
|
@@ -119,7 +131,15 @@ describe("executeTool", () => {
|
|
|
119
131
|
});
|
|
120
132
|
|
|
121
133
|
try {
|
|
122
|
-
|
|
134
|
+
// First call: get confirmation token
|
|
135
|
+
const confirmResult = await executeTool(testToolName, "call-throw", {});
|
|
136
|
+
const tokenMatch = confirmResult.content.match(/Confirmation Token: ([a-f0-9-]+)/);
|
|
137
|
+
expect(tokenMatch).toBeTruthy();
|
|
138
|
+
|
|
139
|
+
// Second call with token: tool throws
|
|
140
|
+
const result = await executeTool(testToolName, "call-throw2", {
|
|
141
|
+
_confirmToken: tokenMatch![1],
|
|
142
|
+
});
|
|
123
143
|
|
|
124
144
|
expect(result.is_error).toBe(true);
|
|
125
145
|
expect(result.content).toContain("Something went wrong");
|