fix(sessions): resolve blockers and warnings in handle_session_approve
Neuron Soul CI / build (pull_request) Failing after 9m3s
Neuron Soul CI / build (pull_request) Failing after 9m3s
BLOCKER 1 (sessions.el, modern path): Add guard that rejects allow
action when tool_name is missing from the body. Previously, omitting
tool_name caused dispatch_tool("", ...) to return "unknown tool: " and
silently inject a corrupted tool_result into the conversation.
BLOCKER 2 (sessions.el, modern path): Stop re-executing client-side
tools server-side. When the client provides body["content"], use it
directly as the tool result (matching the handle_tool_result contract).
Only fall back to dispatch_tool for builtin tools when no content is
present. Non-builtin tools with no client content now return a clear
error instead of a broken dispatch attempt.
WARNING 1 (chat.el, agentic_loop): Wire always_allow_<session_id> state
into the bridge-suspension decision. When a tool is in the session's
always-allow list, treat it as locally dispatchable (like a builtin)
and skip the bridge pause, so the approval UI is never shown again for
that tool in that session.
WARNING 2 (sessions.el, legacy path): Read a "tools_variant" field from
the legacy pending blob when present, and call the corresponding
agentic_tools_*() variant on resume. Falls back to agentic_tools_literal()
for blobs written before this field existed.
tests/test_sessions_approve.el: Add 10-case test suite covering:
- empty session_id / missing call_id / missing action guards
- no pending tool returns correct error
- missing tool_name on allow returns error (BLOCKER 1)
- deny action does not require tool_name
- legacy call_id mismatch returns mismatch error
- always action records tool_name in always_allow state
- allow with client content skips re-execution (BLOCKER 2)
This commit is contained in:
+40
-4
@@ -503,13 +503,42 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
true
|
||||
} else { false }
|
||||
|
||||
// BLOCKER: tool_name is required for allow — an empty approve_tool_name
|
||||
// would cause dispatch_tool("", ...) to silently return "unknown tool: "
|
||||
// and inject a corrupted result into the conversation. Reject early.
|
||||
if str_eq(approve_tool_name, "") && str_eq(eff_action, "allow") {
|
||||
return "{\"error\":\"tool_name is required for allow action\"}"
|
||||
}
|
||||
|
||||
// Build the content string the tool produced (or the denial message).
|
||||
// tool_input may be passed in the body by the client for re-execution.
|
||||
//
|
||||
// For MCP/client-side tools (non-builtin): the client has ALREADY executed
|
||||
// the tool and posts the result in body["content"]. Accept it directly
|
||||
// (matching the handle_tool_result contract) rather than re-running
|
||||
// server-side via dispatch_tool — that would make the client-side execution
|
||||
// irrelevant and would break mcp__* tools the soul cannot reach.
|
||||
//
|
||||
// For builtin tools with no client-provided content: fall back to
|
||||
// dispatch_tool so those tools still execute correctly.
|
||||
let client_content: String = json_get(body, "content")
|
||||
let use_client_content: Bool = !str_eq(client_content, "")
|
||||
let use_dispatch: Bool = is_builtin_tool(approve_tool_name) && !use_client_content
|
||||
let raw_input: String = json_get_raw(body, "tool_input")
|
||||
let eff_input: String = if str_eq(raw_input, "") { "{}" } else { raw_input }
|
||||
let content: String = if str_eq(eff_action, "allow") {
|
||||
let raw: String = dispatch_tool(approve_tool_name, eff_input)
|
||||
if str_len(raw) > 6000 { str_slice(raw, 0, 6000) + "...[truncated]" } else { raw }
|
||||
if use_client_content {
|
||||
let trimmed: String = if str_len(client_content) > 6000 {
|
||||
str_slice(client_content, 0, 6000) + "...[truncated]"
|
||||
} else { client_content }
|
||||
trimmed
|
||||
} else if use_dispatch {
|
||||
let raw: String = dispatch_tool(approve_tool_name, eff_input)
|
||||
if str_len(raw) > 6000 { str_slice(raw, 0, 6000) + "...[truncated]" } else { raw }
|
||||
} else {
|
||||
// Non-builtin tool, no client content — error rather than
|
||||
// silently dispatching a tool the soul cannot execute.
|
||||
"{\"error\":\"client content required for non-builtin tool: " + approve_tool_name + "\"}"
|
||||
}
|
||||
} else {
|
||||
"{\"error\":\"User denied this tool call\"}"
|
||||
}
|
||||
@@ -559,7 +588,14 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
// same agentic_resume path handles continuation (instead of an inline loop).
|
||||
// messages_so_far already includes the assistant turn that requested the tool.
|
||||
let legacy_messages: String = json_get_raw(pending_raw, "messages_so_far")
|
||||
let tools_json: String = agentic_tools_literal()
|
||||
// WARNING: the original session may have used agentic_tools_with_web() or
|
||||
// agentic_tools_all(). The old pending blob did not store the tools variant.
|
||||
// Read a "tools_variant" field if present (future suspensions record it);
|
||||
// fall back to agentic_tools_literal() for legacy blobs that lack this field.
|
||||
let stored_variant: String = json_get(pending_raw, "tools_variant")
|
||||
let tools_json: String = if str_eq(stored_variant, "web") { agentic_tools_with_web() }
|
||||
else if str_eq(stored_variant, "all") { agentic_tools_all() }
|
||||
else { agentic_tools_literal() }
|
||||
|
||||
// Write a synthetic bridge blob so agentic_resume can pick it up.
|
||||
let blob: String = "{\"model\":\"" + json_safe(model) + "\""
|
||||
|
||||
Reference in New Issue
Block a user