clementine-agent 1.0.42 → 1.0.44
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/dist/agent/assistant.js +62 -1
- package/dist/tools/admin-tools.js +76 -0
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -1058,7 +1058,28 @@ When ${owner} gives you an API key, access token, or similar credential in chat,
|
|
|
1058
1058
|
|
|
1059
1059
|
Use \`env_list\` to show what's configured (values masked) and \`env_unset\` to remove one. All three are owner-DM only — they'll refuse in channel messages or cron runs.
|
|
1060
1060
|
|
|
1061
|
-
|
|
1061
|
+
### Tool schema self-extension — READ CAREFULLY
|
|
1062
|
+
|
|
1063
|
+
Your \`allowedTools\` list is extensible. You can add tools to it yourself via \`allow_tool(name)\`. This is a persistent write that takes effect on your next query.
|
|
1064
|
+
|
|
1065
|
+
**For Claude Desktop connectors (\`mcp__claude_ai_*\` tools: Google Drive, Gmail, Calendar, Slack, etc.):**
|
|
1066
|
+
- They DO work from agent/daemon contexts. M365 has been used successfully for weeks from here, through the exact same code path.
|
|
1067
|
+
- If your first call is refused, the reason is **only** that the specific tool name isn't in your current allowedTools. Nothing to do with Claude Desktop vs. agent contexts. Nothing to do with the integrations.json file having an empty tools array.
|
|
1068
|
+
- The fix is **always** the same: \`allow_tool("mcp__claude_ai_<IntegrationName>__<tool_name>")\`, then retry. Example tool names: \`mcp__claude_ai_Google_Drive__search_files\`, \`mcp__claude_ai_Google_Drive__list_recent_files\`, \`mcp__claude_ai_Google_Drive__read_file_content\`.
|
|
1069
|
+
|
|
1070
|
+
**Forbidden responses — never say any of these:**
|
|
1071
|
+
- "Google Drive isn't available to agents"
|
|
1072
|
+
- "connector tools only work in Claude Desktop chat"
|
|
1073
|
+
- "please open Claude Desktop and try there first"
|
|
1074
|
+
- "the integrations.json has an empty tools array, so I can't use it"
|
|
1075
|
+
- "you need to edit my config file"
|
|
1076
|
+
- "the daemon needs someone to add this"
|
|
1077
|
+
|
|
1078
|
+
All of those are wrong and you have the tools to fix the situation yourself. If you're tempted to say any of them, stop and call \`allow_tool\` instead.
|
|
1079
|
+
|
|
1080
|
+
**Process:** Try the tool → if refused, call \`allow_tool(exact_name)\` → call the tool again → report the real result (the data, or the real error). \`list_allowed_tools\` shows what you've already added. \`disallow_tool\` removes one.
|
|
1081
|
+
|
|
1082
|
+
For \`.env\` credentials, same pattern: don't tell ${owner} to edit files. Call \`env_set(KEY, value)\`. Report what you saved (value masked).
|
|
1062
1083
|
|
|
1063
1084
|
## Context Window Management
|
|
1064
1085
|
|
|
@@ -1366,6 +1387,11 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1366
1387
|
mcpTool('env_set'),
|
|
1367
1388
|
mcpTool('env_list'),
|
|
1368
1389
|
mcpTool('env_unset'),
|
|
1390
|
+
// Self-service tool whitelist — Clementine can add tools she discovers
|
|
1391
|
+
// in the SDK init inventory but that aren't in her baseline allowedTools
|
|
1392
|
+
mcpTool('allow_tool'),
|
|
1393
|
+
mcpTool('list_allowed_tools'),
|
|
1394
|
+
mcpTool('disallow_tool'),
|
|
1369
1395
|
mcpTool('self_restart'),
|
|
1370
1396
|
mcpTool('cron_list'),
|
|
1371
1397
|
mcpTool('add_cron_job'),
|
|
@@ -1423,6 +1449,41 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
1423
1449
|
}
|
|
1424
1450
|
}
|
|
1425
1451
|
catch { /* non-fatal — dynamic tools are supplementary */ }
|
|
1452
|
+
// Claude Desktop connector tools (mcp__claude_ai_*). These reach the SDK
|
|
1453
|
+
// subprocess via Claude Code's runtime but are NOT added to allowedTools
|
|
1454
|
+
// automatically — which caused the model to see them in the init
|
|
1455
|
+
// inventory but get refused when it tried to call one ("not in my
|
|
1456
|
+
// function schema"). Add every tool from every auto-registered
|
|
1457
|
+
// integration (populated from the SDK init message on prior queries) so
|
|
1458
|
+
// the whitelist matches reality.
|
|
1459
|
+
try {
|
|
1460
|
+
const integrations = _mcpBridge?.getClaudeIntegrations() ?? [];
|
|
1461
|
+
for (const ig of integrations) {
|
|
1462
|
+
for (const tool of ig.tools) {
|
|
1463
|
+
const fullName = `mcp__claude_ai_${ig.name}__${tool}`;
|
|
1464
|
+
if (!allowedTools.includes(fullName))
|
|
1465
|
+
allowedTools.push(fullName);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
catch { /* non-fatal */ }
|
|
1470
|
+
// Self-service extension — Clementine can add tools to her own whitelist
|
|
1471
|
+
// at runtime via the `allow_tool` MCP tool, writing to allowed-tools-
|
|
1472
|
+
// extra.json. This eliminates the "tool not in schema → dead end → tell
|
|
1473
|
+
// owner to edit config" failure pattern. See admin-tools.ts:allow_tool.
|
|
1474
|
+
try {
|
|
1475
|
+
const extraPath = path.join(BASE_DIR, 'allowed-tools-extra.json');
|
|
1476
|
+
if (fs.existsSync(extraPath)) {
|
|
1477
|
+
const extras = JSON.parse(fs.readFileSync(extraPath, 'utf-8'));
|
|
1478
|
+
if (Array.isArray(extras)) {
|
|
1479
|
+
for (const t of extras) {
|
|
1480
|
+
if (typeof t === 'string' && !allowedTools.includes(t))
|
|
1481
|
+
allowedTools.push(t);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
catch { /* non-fatal */ }
|
|
1426
1487
|
// Agent tool whitelist: filter down to only allowed tools
|
|
1427
1488
|
if (profile?.team?.allowedTools?.length) {
|
|
1428
1489
|
const whitelist = new Set(profile.team.allowedTools.flatMap(t => [t, mcpTool(t)]));
|
|
@@ -170,6 +170,82 @@ export function registerAdminTools(server) {
|
|
|
170
170
|
logger.info({ key: normalizedKey }, 'env_unset');
|
|
171
171
|
return textResult(`Removed ${normalizedKey} from ~/.clementine/.env`);
|
|
172
172
|
});
|
|
173
|
+
// ── Self-service tool whitelist ────────────────────────────────────────
|
|
174
|
+
const ALLOWED_TOOLS_EXTRA = path.join(BASE_DIR, 'allowed-tools-extra.json');
|
|
175
|
+
function readExtraAllowedTools() {
|
|
176
|
+
if (!existsSync(ALLOWED_TOOLS_EXTRA))
|
|
177
|
+
return [];
|
|
178
|
+
try {
|
|
179
|
+
const arr = JSON.parse(readFileSync(ALLOWED_TOOLS_EXTRA, 'utf-8'));
|
|
180
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string') : [];
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function writeExtraAllowedTools(tools) {
|
|
187
|
+
if (!existsSync(BASE_DIR))
|
|
188
|
+
mkdirSync(BASE_DIR, { recursive: true });
|
|
189
|
+
const unique = [...new Set(tools)].sort();
|
|
190
|
+
writeFileSync(ALLOWED_TOOLS_EXTRA, JSON.stringify(unique, null, 2));
|
|
191
|
+
}
|
|
192
|
+
server.tool('allow_tool', 'Add a tool name to your self-managed allowedTools list. Use when you see a tool in the SDK inventory but get "not in function schema" when you try to call it. Writes to ~/.clementine/allowed-tools-extra.json; takes effect on your NEXT query. Owner-DM only. Common case: Claude Desktop connector tools like mcp__claude_ai_Google_Drive__search_files that appear in the init inventory but aren\'t in your baseline whitelist.', {
|
|
193
|
+
name: z.string().describe('Exact tool name (e.g. "mcp__claude_ai_Google_Drive__search_files")'),
|
|
194
|
+
reason: z.string().optional().describe('Brief note: why you need this tool. For audit trail.'),
|
|
195
|
+
}, async ({ name, reason }) => {
|
|
196
|
+
const gate = requireOwnerDm();
|
|
197
|
+
if (!gate.ok)
|
|
198
|
+
return textResult(gate.message);
|
|
199
|
+
const trimmed = name.trim();
|
|
200
|
+
if (!trimmed)
|
|
201
|
+
return textResult('Refused: empty tool name.');
|
|
202
|
+
// Loose format check — MCP tool names, built-in tool names, or namespaced patterns.
|
|
203
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed.replace(/__[A-Za-z0-9_-]+/g, ''))) {
|
|
204
|
+
return textResult(`Refused: "${trimmed}" doesn't look like a valid tool name. Use a literal name like "mcp__claude_ai_Google_Drive__search_files" or a built-in like "WebFetch".`);
|
|
205
|
+
}
|
|
206
|
+
const current = readExtraAllowedTools();
|
|
207
|
+
if (current.includes(trimmed)) {
|
|
208
|
+
return textResult(`${trimmed} is already in your extra-allowed list. If it's still being refused, it may be disallowed by a higher-priority block (heartbeat/cron disallowed tools, profile tier).`);
|
|
209
|
+
}
|
|
210
|
+
current.push(trimmed);
|
|
211
|
+
try {
|
|
212
|
+
writeExtraAllowedTools(current);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return textResult(`Failed to persist: ${String(err).slice(0, 200)}`);
|
|
216
|
+
}
|
|
217
|
+
logger.info({ name: trimmed, reason, totalExtras: current.length }, 'allow_tool');
|
|
218
|
+
return textResult(`Added ${trimmed} to ~/.clementine/allowed-tools-extra.json (${current.length} total extras). Active on your next query — no daemon restart needed.${reason ? ` Reason: ${reason}` : ''}`);
|
|
219
|
+
});
|
|
220
|
+
server.tool('list_allowed_tools', 'Show the current self-managed allowedTools extras (tools you added via allow_tool on top of the built-in whitelist). Owner-DM only.', {}, async () => {
|
|
221
|
+
const gate = requireOwnerDm();
|
|
222
|
+
if (!gate.ok)
|
|
223
|
+
return textResult(gate.message);
|
|
224
|
+
const current = readExtraAllowedTools();
|
|
225
|
+
if (current.length === 0)
|
|
226
|
+
return textResult('No extra tools registered. The built-in whitelist is the only source; add entries via `allow_tool` when you encounter "not in function schema" errors.');
|
|
227
|
+
return textResult(`Extra allowed tools (${current.length}):\n${current.map(t => `- ${t}`).join('\n')}\n\nStored in ~/.clementine/allowed-tools-extra.json; merged into allowedTools on every query.`);
|
|
228
|
+
});
|
|
229
|
+
server.tool('disallow_tool', 'Remove a tool from your self-managed allowedTools extras. Takes effect on next query. Owner-DM only.', {
|
|
230
|
+
name: z.string().describe('Exact tool name to remove'),
|
|
231
|
+
}, async ({ name }) => {
|
|
232
|
+
const gate = requireOwnerDm();
|
|
233
|
+
if (!gate.ok)
|
|
234
|
+
return textResult(gate.message);
|
|
235
|
+
const trimmed = name.trim();
|
|
236
|
+
const current = readExtraAllowedTools();
|
|
237
|
+
if (!current.includes(trimmed))
|
|
238
|
+
return textResult(`${trimmed} isn't in the extras list. Nothing to remove.`);
|
|
239
|
+
const next = current.filter(t => t !== trimmed);
|
|
240
|
+
try {
|
|
241
|
+
writeExtraAllowedTools(next);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
return textResult(`Failed to persist: ${String(err).slice(0, 200)}`);
|
|
245
|
+
}
|
|
246
|
+
logger.info({ name: trimmed, totalExtras: next.length }, 'disallow_tool');
|
|
247
|
+
return textResult(`Removed ${trimmed} from extras (${next.length} remaining).`);
|
|
248
|
+
});
|
|
173
249
|
// ── Workspace Tools ─────────────────────────────────────────────────────
|
|
174
250
|
/** Common developer directories to auto-scan (relative to home). */
|
|
175
251
|
const DEFAULT_WORKSPACE_CANDIDATES = [
|