Merge pull request #260 from letta-ai/feat/disallow-interactive-plan-tools

feat: add disallowed interactive tools config for sessions
This commit is contained in:
Charles Packer
2026-02-10 16:20:33 -08:00
committed by GitHub
8 changed files with 115 additions and 136 deletions

View File

@@ -14,6 +14,10 @@ LETTA_API_KEY=your_letta_api_key
# Allowed tools (comma-separated)
# ALLOWED_TOOLS=Read,Glob,Grep,Task,web_search,conversation_search
# Disallowed tools (comma-separated)
# Default blocks plan-mode interactive tools that can stall headless agents
# DISALLOWED_TOOLS=EnterPlanMode,ExitPlanMode
# ============================================
# Telegram (required: at least one channel)
# ============================================

112
package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@clack/prompts": "^0.11.0",
"@hapi/boom": "^10.0.1",
"@letta-ai/letta-client": "^1.7.8",
"@letta-ai/letta-code-sdk": "^0.0.5",
"@letta-ai/letta-code-sdk": "^0.0.6",
"@types/express": "^5.0.6",
"@types/node": "^25.0.10",
"@types/node-schedule": "^2.1.8",
@@ -1283,13 +1283,13 @@
"license": "Apache-2.0"
},
"node_modules/@letta-ai/letta-code": {
"version": "0.14.8",
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.14.8.tgz",
"integrity": "sha512-sS5jsAcA1hLIuQXzlKGveA1IjFw6TTcjK/oZmtVq3suB2HUdML9RdNpqwMnDRz6NS4tT4nyWvbu732uyJEq7ig==",
"version": "0.14.16",
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code/-/letta-code-0.14.16.tgz",
"integrity": "sha512-4tl0QjA9AxZuk3phZPIfmzEM4TF0axfJpChbfXH31izsYOU+06iHK4cKNMM13E66hKg6Uz61RgO5mXJJEMZJLg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@letta-ai/letta-client": "^1.7.7",
"@letta-ai/letta-client": "^1.7.8",
"glob": "^13.0.0",
"ink-link": "^5.0.0",
"open": "^10.2.0",
@@ -1303,18 +1303,18 @@
}
},
"node_modules/@letta-ai/letta-code-sdk": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code-sdk/-/letta-code-sdk-0.0.5.tgz",
"integrity": "sha512-mBGZ9RFd2d4p4RffRctfE3yvEjM5AndIaXfqus6Nu3TNJcOs40c/AsdeeAFN61LLQ7gpqSea+Dh4/PE+zR+ABQ==",
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@letta-ai/letta-code-sdk/-/letta-code-sdk-0.0.6.tgz",
"integrity": "sha512-zW3sWaCYNH8tqiNrXVpfJezrjG3q18RgfpYeCU8e2f67UOB0YOgW1uY/BV/iQ9w4u5TSEfswU7G3dww5tGTRLQ==",
"license": "Apache-2.0",
"dependencies": {
"@letta-ai/letta-code": "latest"
"@letta-ai/letta-code": "0.14.16"
}
},
"node_modules/@letta-ai/letta-code/node_modules/glob": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz",
"integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==",
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.2.tgz",
"integrity": "sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.1.2",
@@ -1329,9 +1329,9 @@
}
},
"node_modules/@letta-ai/letta-code/node_modules/lru-cache": {
"version": "11.2.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
"integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==",
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
"integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
@@ -2614,9 +2614,9 @@
}
},
"node_modules/ansi-escapes": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz",
"integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==",
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
"integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
@@ -4441,14 +4441,14 @@
}
},
"node_modules/ink": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/ink/-/ink-6.6.0.tgz",
"integrity": "sha512-QDt6FgJxgmSxAelcOvOHUvFxbIUjVpCH5bx+Slvc5m7IEcpGt3dYwbz/L+oRnqEGeRvwy1tineKK4ect3nW1vQ==",
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/ink/-/ink-6.7.0.tgz",
"integrity": "sha512-dhB16KfdTO8yYwF2K0E4wPXpL88tdrjjB6w44AZ0ljSktYoUQQcxccq9KL1vpRhk8JIa0A7B7zvjajHqI42teA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.2.0",
"@alcalzone/ansi-tokenize": "^0.2.4",
"ansi-escapes": "^7.3.0",
"ansi-styles": "^6.2.1",
"auto-bind": "^5.0.1",
"chalk": "^5.6.0",
@@ -4461,12 +4461,14 @@
"is-in-ci": "^2.0.0",
"patch-console": "^2.0.0",
"react-reconciler": "^0.33.0",
"scheduler": "^0.27.0",
"signal-exit": "^3.0.7",
"slice-ansi": "^7.1.0",
"stack-utils": "^2.0.6",
"string-width": "^8.1.0",
"type-fest": "^4.27.0",
"widest-line": "^5.0.0",
"string-width": "^8.1.1",
"terminal-size": "^4.0.1",
"type-fest": "^5.4.1",
"widest-line": "^6.0.0",
"wrap-ansi": "^9.0.0",
"ws": "^8.18.0",
"yoga-layout": "~3.2.1"
@@ -4537,6 +4539,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/type-fest": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz",
"integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/widest-line": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz",
"integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==",
"license": "MIT",
"peer": true,
"dependencies": {
"string-width": "^8.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ink/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
@@ -7561,6 +7595,19 @@
"url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/telegramify-markdown": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/telegramify-markdown/-/telegramify-markdown-1.3.2.tgz",
@@ -8088,6 +8135,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/terminal-size": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz",
"integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",

View File

@@ -65,7 +65,7 @@
"@clack/prompts": "^0.11.0",
"@hapi/boom": "^10.0.1",
"@letta-ai/letta-client": "^1.7.8",
"@letta-ai/letta-code-sdk": "^0.0.5",
"@letta-ai/letta-code-sdk": "^0.0.6",
"@types/express": "^5.0.6",
"@types/node": "^25.0.10",
"@types/node-schedule": "^2.1.8",

View File

@@ -1,65 +0,0 @@
diff --git a/node_modules/@letta-ai/letta-code/letta.js b/node_modules/@letta-ai/letta-code/letta.js
index a74d551..459f60b 100755
--- a/node_modules/@letta-ai/letta-code/letta.js
+++ b/node_modules/@letta-ai/letta-code/letta.js
@@ -65184,10 +65184,12 @@ ${SYSTEM_REMINDER_CLOSE}
return { stopReason: "error", shouldAccumulate: true };
}
if (updatedApproval && !autoApprovalEmitted.has(updatedApproval.toolCallId)) {
+ if (process.env.DEBUG_SDK) console.error(`[CLI-StreamHook] updatedApproval: tool=${updatedApproval.toolName} callId=${updatedApproval.toolCallId} argsLen=${(updatedApproval.toolArgs || '').length}`);
const { autoAllowed } = await classifyApprovals([updatedApproval], {
requireArgsForAutoApprove: true,
missingNameReason: "Tool call incomplete - missing name"
});
+ if (process.env.DEBUG_SDK) console.error(`[CLI-StreamHook] classifyApprovals: allowed=${autoAllowed.length}`);
const [approval] = autoAllowed;
if (approval) {
const permission = approval.permission;
@@ -65238,6 +65240,7 @@ ${SYSTEM_REMINDER_CLOSE}
lastRunId = result.lastRunId || null;
if (lastRunId)
lastKnownRunId = lastRunId;
+ if (process.env.DEBUG_SDK) console.error(`[CLI-Headless] Stream drained: stopReason=${stopReason} approvals=${approvals.length} runId=${lastRunId}`);
} else {
const result = await drainStreamWithResume(stream2, buffers, () => {});
stopReason = result.stopReason;
@@ -65258,6 +65261,12 @@ ${SYSTEM_REMINDER_CLOSE}
break;
}
if (stopReason === "requires_approval") {
+ if (process.env.DEBUG_SDK) {
+ console.error(`[CLI-Headless] requires_approval: ${approvals.length} approval(s) pending`);
+ for (const a of approvals) {
+ console.error(`[CLI-Headless] tool=${a.toolName} callId=${a.toolCallId} argsLen=${(a.toolArgs || '').length}`);
+ }
+ }
if (approvals.length === 0) {
console.error("Unexpected empty approvals array");
process.exit(1);
@@ -65268,6 +65277,15 @@ ${SYSTEM_REMINDER_CLOSE}
requireArgsForAutoApprove: true,
missingNameReason: "Tool call incomplete - missing name"
});
+ if (process.env.DEBUG_SDK) {
+ console.error(`[CLI-Headless] classifyApprovals: allowed=${autoAllowed.length} denied=${autoDenied.length}`);
+ for (const a of autoAllowed) {
+ console.error(`[CLI-Headless] ALLOW: tool=${a.approval.toolName} rule=${a.permission.matchedRule || a.permission.reason}`);
+ }
+ for (const a of autoDenied) {
+ console.error(`[CLI-Headless] DENY: tool=${a.approval.toolName} reason=${a.denyReason || a.permission.reason}`);
+ }
+ }
const decisions = [
...autoAllowed.map((ac) => ({
type: "approve",
@@ -65282,8 +65300,10 @@ ${SYSTEM_REMINDER_CLOSE}
};
})
];
+ if (process.env.DEBUG_SDK) console.error(`[CLI-Headless] Executing ${decisions.length} approval decisions...`);
const { executeApprovalBatch: executeApprovalBatch2 } = await init_approval_execution().then(() => exports_approval_execution);
const executedResults = await executeApprovalBatch2(decisions);
+ if (process.env.DEBUG_SDK) console.error(`[CLI-Headless] Approval batch executed: ${executedResults.length} results`);
currentInput = [
{
type: "approval",

View File

@@ -1,43 +0,0 @@
diff --git a/node_modules/@letta-ai/letta-code-sdk/dist/index.js b/node_modules/@letta-ai/letta-code-sdk/dist/index.js
index 26c1b7a..db20689 100644
--- a/node_modules/@letta-ai/letta-code-sdk/dist/index.js
+++ b/node_modules/@letta-ai/letta-code-sdk/dist/index.js
@@ -101,6 +101,10 @@ class SubprocessTransport {
const msg = await this.read();
if (msg === null)
break;
+ if (process.env.DEBUG_SDK) {
+ const preview = JSON.stringify(msg).slice(0, 300);
+ console.error(`[SDK-Transport] wire: type=${msg.type} ${msg.message_type || ''} ${preview}`);
+ }
yield msg;
}
}
@@ -305,6 +309,7 @@ class Session {
for await (const wireMsg of this.transport.messages()) {
if (wireMsg.type === "control_request") {
const controlReq = wireMsg;
+ if (process.env.DEBUG_SDK) console.error(`[SDK-Session] control_request: subtype=${controlReq.request.subtype} tool=${controlReq.request.tool_name || 'N/A'}`);
if (controlReq.request.subtype === "can_use_tool") {
await this.handleCanUseTool(controlReq.request_id, controlReq.request);
continue;
@@ -314,14 +319,19 @@ class Session {
if (sdkMsg) {
yield sdkMsg;
if (sdkMsg.type === "result") {
+ if (process.env.DEBUG_SDK) console.error(`[SDK-Session] result: success=${sdkMsg.success} error=${sdkMsg.error || 'none'} resultLen=${sdkMsg.result?.length || 0}`);
break;
}
+ } else {
+ if (process.env.DEBUG_SDK) console.error(`[SDK-Session] DROPPED wire message: type=${wireMsg.type} message_type=${wireMsg.message_type || 'N/A'}`);
}
}
}
async handleCanUseTool(requestId, req) {
+ if (process.env.DEBUG_SDK) console.error(`[SDK-Session] handleCanUseTool: tool=${req.tool_name} mode=${this.options.permissionMode} requestId=${requestId}`);
let response;
if (this.options.permissionMode === "bypassPermissions") {
+ if (process.env.DEBUG_SDK) console.error(`[SDK-Session] AUTO-ALLOWING ${req.tool_name} (bypassPermissions)`);
response = {
behavior: "allow",
updatedInput: null,

View File

@@ -151,11 +151,20 @@ export class LettaBot implements AgentSession {
// =========================================================================
private get baseSessionOptions() {
const disallowedTools = this.config.disallowedTools || [];
return {
permissionMode: 'bypassPermissions' as const,
allowedTools: this.config.allowedTools,
disallowedTools,
cwd: this.config.workingDir,
canUseTool: (toolName: string, _toolInput: Record<string, unknown>) => {
if (disallowedTools.includes(toolName)) {
return {
behavior: 'deny' as const,
message: `Tool '${toolName}' is blocked by bot configuration`,
};
}
console.log(`[Bot] Tool approval requested: ${toolName} (should be auto-approved by bypassPermissions)`);
return { behavior: 'allow' as const };
},

View File

@@ -124,6 +124,7 @@ export interface BotConfig {
workingDir: string;
agentName?: string; // Name for the agent (set via API after creation)
allowedTools: string[];
disallowedTools?: string[];
// Display
displayName?: string; // Prefix outbound messages (e.g. "💜 Signo")

View File

@@ -410,10 +410,22 @@ function createGroupBatcher(
// Skills are installed to agent-scoped directory when agent is created (see core/bot.ts)
function parseCsvList(raw: string): string[] {
return raw
.split(',')
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
// Global config (shared across all agents)
const globalConfig = {
workingDir: getWorkingDir(),
allowedTools: (process.env.ALLOWED_TOOLS || 'Bash,Read,Edit,Write,Glob,Grep,Task,web_search,conversation_search').split(','),
allowedTools: parseCsvList(
process.env.ALLOWED_TOOLS || 'Bash,Read,Edit,Write,Glob,Grep,Task,web_search,conversation_search',
),
disallowedTools: parseCsvList(
process.env.DISALLOWED_TOOLS || 'EnterPlanMode,ExitPlanMode',
),
attachmentsMaxBytes: resolveAttachmentsMaxBytes(),
attachmentsMaxAgeDays: resolveAttachmentsMaxAgeDays(),
cronEnabled: process.env.CRON_ENABLED === 'true', // Legacy env var fallback
@@ -485,6 +497,7 @@ async function main() {
workingDir: globalConfig.workingDir,
agentName: agentConfig.name,
allowedTools: globalConfig.allowedTools,
disallowedTools: globalConfig.disallowedTools,
displayName: agentConfig.displayName,
maxToolCalls: agentConfig.features?.maxToolCalls,
skills: {