lemura 1.5.2 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.3] - 2026-05-30
9
+
10
+ ### Fixed
11
+
12
+ - **Tool firewall `ask` decision could execute denied tools** (critical): when a `ToolFirewallConfig.onAsk` handler returned anything other than the exact string `'deny'` — including the boolean `false`, `undefined`, or by throwing — the tool was executed anyway. A user pressing "Stop"/deny could not actually stop the tool call. The `ask` path is now **fail-safe**: only an explicit accept signal (`'accept'` or `true`) allows execution; `'deny'`, `false`, `undefined`, `void`, and thrown errors all block the tool and inject a `Blocked by tool firewall` observation.
13
+ - `ToolFirewallConfig.onAsk` now accepts `'accept' | 'deny' | boolean | void` (and the `Promise` thereof) for ergonomics. The previous `'accept' | 'deny'` return remains valid.
14
+
8
15
  ## [1.5.2] - 2026-05-30
9
16
 
10
17
  ### Changed
@@ -199,9 +199,14 @@ interface ToolFirewallConfig {
199
199
  rules?: ToolFirewallRule[];
200
200
  /**
201
201
  * Called when a tool hits the 'ask' decision.
202
- * Return 'accept' or 'deny'. If omitted, 'ask' behaves like 'deny'.
202
+ *
203
+ * Return `'accept'` (or `true`) to allow the tool to run; return `'deny'`
204
+ * (or `false`) to block it. The decision is **fail-safe**: only an explicit
205
+ * accept signal allows execution — any other value (`'deny'`, `false`,
206
+ * `undefined`, `null`, a thrown error) blocks the tool. If `onAsk` is
207
+ * omitted entirely, an `'ask'` decision behaves like `'deny'`.
203
208
  */
204
- onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny'> | 'accept' | 'deny';
209
+ onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny' | boolean | void> | 'accept' | 'deny' | boolean | void;
205
210
  }
206
211
  /**
207
212
  * Execution budget constraints for tool calls within a session.
@@ -199,9 +199,14 @@ interface ToolFirewallConfig {
199
199
  rules?: ToolFirewallRule[];
200
200
  /**
201
201
  * Called when a tool hits the 'ask' decision.
202
- * Return 'accept' or 'deny'. If omitted, 'ask' behaves like 'deny'.
202
+ *
203
+ * Return `'accept'` (or `true`) to allow the tool to run; return `'deny'`
204
+ * (or `false`) to block it. The decision is **fail-safe**: only an explicit
205
+ * accept signal allows execution — any other value (`'deny'`, `false`,
206
+ * `undefined`, `null`, a thrown error) blocks the tool. If `onAsk` is
207
+ * omitted entirely, an `'ask'` decision behaves like `'deny'`.
203
208
  */
204
- onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny'> | 'accept' | 'deny';
209
+ onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny' | boolean | void> | 'accept' | 'deny' | boolean | void;
205
210
  }
206
211
  /**
207
212
  * Execution budget constraints for tool calls within a session.
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as IProviderAdapter, d as TranscriptionRequest, e as TranscriptionResponse, f as SynthesisRequest, A as AudioChunk, V as VisionRequest, g as VisionResponse, h as ImageGenRequest, i as ImageGenResponse, M as ModelInfo, C as ContextWindow, T as Turn, j as ContentBlock, I as IToolDefinition } from './adapters-DAzmrg4l.mjs';
2
2
  export { k as CompletionChunk, l as CompletionRequest, m as CompletionResponse, b as IContextStrategy, c as IScratchpadAdapter, n as IStorageAdapter, N as NormalizedMessage, o as STMItem, p as STMRegistryConfig, S as ShortTermMemoryRegistry, q as TokenUsage, r as ToolCall, s as ToolContext, t as ToolResult } from './adapters-DAzmrg4l.mjs';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-DPSUNlK6.mjs';
4
- export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-DPSUNlK6.mjs';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-DFh31XIA.mjs';
4
+ export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-DFh31XIA.mjs';
5
5
  export { LemuraAdapterError, LemuraContextOverflowError, LemuraError, LemuraMCPConnectionError, LemuraMCPError, LemuraMCPTimeoutError, LemuraMaxIterationsError, LemuraSkillInjectionError, LemuraToolNotFoundError, LemuraToolTimeoutError, LemuraToolValidationError } from './types/index.mjs';
6
6
  import { I as ILogger } from './logger-DxvKliuk.mjs';
7
7
  export { L as LogLevel, a as LogMetadata, S as Severity } from './logger-DxvKliuk.mjs';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as IProviderAdapter, d as TranscriptionRequest, e as TranscriptionResponse, f as SynthesisRequest, A as AudioChunk, V as VisionRequest, g as VisionResponse, h as ImageGenRequest, i as ImageGenResponse, M as ModelInfo, C as ContextWindow, T as Turn, j as ContentBlock, I as IToolDefinition } from './adapters-CIRkrCHl.js';
2
2
  export { k as CompletionChunk, l as CompletionRequest, m as CompletionResponse, b as IContextStrategy, c as IScratchpadAdapter, n as IStorageAdapter, N as NormalizedMessage, o as STMItem, p as STMRegistryConfig, S as ShortTermMemoryRegistry, q as TokenUsage, r as ToolCall, s as ToolContext, t as ToolResult } from './adapters-CIRkrCHl.js';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-Cq_oRvoc.js';
4
- export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-Cq_oRvoc.js';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-Cd704J0J.js';
4
+ export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-Cd704J0J.js';
5
5
  export { LemuraAdapterError, LemuraContextOverflowError, LemuraError, LemuraMCPConnectionError, LemuraMCPError, LemuraMCPTimeoutError, LemuraMaxIterationsError, LemuraSkillInjectionError, LemuraToolNotFoundError, LemuraToolTimeoutError, LemuraToolValidationError } from './types/index.js';
6
6
  import { I as ILogger } from './logger-DxvKliuk.js';
7
7
  export { L as LogLevel, a as LogMetadata, S as Severity } from './logger-DxvKliuk.js';
package/dist/index.js CHANGED
@@ -3260,8 +3260,17 @@ ${blocks.join("\n\n")}
3260
3260
  }
3261
3261
  if (firewall.decision === "ask") {
3262
3262
  if (this.config.toolFirewall?.onAsk) {
3263
- const userDecision = await this.config.toolFirewall.onAsk(toolName, argsJson);
3264
- if (userDecision === "deny") {
3263
+ let accepted = false;
3264
+ try {
3265
+ const userDecision = await this.config.toolFirewall.onAsk(toolName, argsJson);
3266
+ accepted = userDecision === "accept" || userDecision === true;
3267
+ } catch (e) {
3268
+ this.logger.warn(`Tool firewall onAsk handler threw \u2014 treating as deny: ${toolName}`, {
3269
+ error: e instanceof Error ? e.message : String(e)
3270
+ });
3271
+ accepted = false;
3272
+ }
3273
+ if (!accepted) {
3265
3274
  this.logger.warn(`Tool blocked by firewall (ask \u2192 deny): ${toolName}`, { reason: firewall.reason });
3266
3275
  toolResults.push({ toolCallId, content: `Blocked by tool firewall: ${firewall.reason}` });
3267
3276
  return false;