feat(13-12): D-10 untrusted-content boundary — wrap_untrusted, per-call random token
assistant/untrusted.rs: wrap_untrusted(label, text) wraps peer-supplied text (filenames, log lines, mesh/peer status) in a delimiter block whose token is freshly randomized on every call via the in-tree rand crate — never a module constant, never derived from content. A forged closing boundary using a guessed/fixed token cannot terminate the real block early (EV-11). tools.rs: wrap_tool_result_if_untrusted wires this in for content_list, app_logs and mesh_status (the tools whose results carry peer-authored text); every other tool result passes through unwrapped. loop_.rs's execute_tool calls it at the exact point a successful ToolResult is constructed, before that content ever becomes part of a ChatMessage. No pattern-stripping or keyword-blocklist filter was added (D-10 rejects that approach by name) — the delimiter and D-11's confirm gate are two independent layers. Four scripted-worst-case tests in mod.rs prove the gate still holds even when a compromised model acts on an injected imperative (injected_instruction_does_not_grant_authority), a forged closing delimiter plus fake operator turn (forged_closing_delimiter_does_not_escape_block), or an injected mislabel attempting to hide the real action from the human (injected_mislabel_still_confirms_real_action) — plus wrap_untrusted_token_is_per_call (tools.rs) asserting the per-call token itself. Zero packages added — rand 0.8.5 already in-tree. 56/56 assistant:: tests pass in this task's own isolated state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
686616375c
commit
265ba5ab19
@@ -208,7 +208,12 @@ pub(crate) async fn execute_tool(call: &ToolCall, ctx: &ToolExecCtx) -> ToolResu
|
||||
Ok(v) => ToolResult {
|
||||
call_id: call.id.clone(),
|
||||
is_error: false,
|
||||
content: v.to_string(),
|
||||
// D-10: peer-authored content (filenames, log lines, mesh/peer
|
||||
// status) is wrapped in an untrusted-content boundary before it
|
||||
// becomes part of a ChatMessage — this IS the point where a
|
||||
// ToolResult is constructed. Operator/node-authored tool
|
||||
// results (disk status, settings) pass through unchanged.
|
||||
content: super::tools::wrap_tool_result_if_untrusted(&call.name, v.to_string()),
|
||||
},
|
||||
Err(msg) => ToolResult {
|
||||
call_id: call.id.clone(),
|
||||
|
||||
Reference in New Issue
Block a user