返回 CodeWhale
codemode.rs
根目录 / crates / tui / src / tools / codemode.rs
1 //! `execute_tools` — Code Mode Phase 1: run a model-provided JavaScript program
2 //! that composes read-only tool calls through `tools.call(name, args)`.
3 //!
4 //! The program handles loops, branching, filtering, and data movement;
5 //! intermediate results stay in the VM and only the bounded return value plus
6 //! a host-owned receipt reach the model. The engine-side precedent is the
7 //! synthetic interpreter dispatch (`js_execution` / `code_execution`): this
8 //! tool is engine-injected, never registered, and dispatched in
9 //! `core::engine::tool_execution`.
10 //!
11 //! Authority stays entirely in Rust. Every nested call traverses the same
12 //! gates a direct call would — registry resolution, the parent turn's
13 //! deny-lists and authority envelope — plus the Phase-1 profile gates
14 //! (read-only, auto-approved). Approving the program never approves anything
15 //! the program might do: a nested call that needs approval aborts the program
16 //! with a receipt naming it. Nested calls do not take per-tool locks (the
17 //! program runs under its own exclusive lock instead); see the limitations.
18 //!
19 //! KV-cache effect: under the default Direct tool mode this tool is deferred,
20 //! not eager, so the session-pinned prefix is unchanged until the model
21 //! activates it via `tool_search` — activation is a declared
22 //! `change:tool_surface` transition, same as any other deferred tool. Under
23 //! CodeMode (`[features] code_mode`) it is eager instead; the flag is session
24 //! config, so the prefix stays stable within a session either way. Program
25 //! text and nested results live in append-only turn history, never in the
26 //! prefix.
27 //!
28 //! Known limitations (Phase 1):
29 //! - Read-only composition. Nested calls must satisfy `is_read_only_for` and
30 //! resolve `ApprovalRequirement::Auto` (posture-independent: Auto tools run
31 //! under every posture, including Never). Anything else aborts the program.
32 //! - No nested `agent`, `workflow`, `tool_search`, interpreter, or MCP calls,
33 //! and no recursive `execute_tools`. Fan-out stays with `workflow`/`task()`.
34 //! - No approval suspension: a gated call aborts with a receipt instead of
35 //! prompting. Per-call approval previews are Phase 2.
36 //! - Nested reads do not serialize against concurrent sibling top-level
37 //! writes. Prefer running `execute_tools` alone in its block when a
38 //! consistent snapshot matters.
39 //! - Rich content blocks (images) from nested results are dropped; text and
40 //! JSON payloads pass through bounded. Each nested payload is a
41 //! `{content, metadata}` envelope: content parsed as JSON when possible,
42 //! metadata verbatim (continuation notices included) or null.
43 //! - Hidden from Plan mode and refused under a worker authority envelope,
44 //! like the other execution surfaces.
45
46 use std::sync::{Arc, Mutex};
47 use std::time::{Duration, Instant};
48
49 use async_trait::async_trait;
50 use serde_json::{Value, json};
51 use tokio::sync::Semaphore;
52 use tokio::time::timeout;
53
54 use codewhale_models::Tool;
55 use codewhale_workflow_js::{
56 BudgetSnapshot, DriverError, ProgressEvent, SpawnedTask, TaskRequest, ToolCallRequest,
57 ToolCallResponse, ToolInvoker, WorkflowDriver, WorkflowRunCancel, WorkflowVm,
58 };
59
60 use crate::tools::registry::{ToolRegistry, enforce_tool_authority};
61 use crate::tools::spec::{
62 ApprovalRequirement, ToolContext, ToolError, ToolResult, ToolSpec, required_str,
63 };
64
65 /// Tool name surfaced to the model. Dispatched alongside the synthetic
66 /// interpreter tools; see `core::engine::tool_execution`.
67 pub const EXECUTE_TOOLS_TOOL_NAME: &str = "execute_tools";
68
69 const EXECUTE_TOOLS_TOOL_TYPE: &str = "execute_tools_20260918";
70
71 /// Maximum program source accepted, in bytes.
72 const MAX_CODE_BYTES: usize = 64 * 1024;
73 /// Whole-run wall deadline. The watchdog drops the run future; the VM
74 /// thread then unwinds through the standard cancel cascade.
75 const RUN_DEADLINE_SECS: u64 = 30;
76 /// Maximum nested tool calls in flight at once, enforced host-side.
77 const MAX_CONCURRENT_CALLS: usize = 4;
78 /// Per nested-call result cap, in serialized bytes.
79 const PER_CALL_RESULT_CAP_BYTES: usize = 32 * 1024;
80 /// Model-visible return cap, in serialized bytes.
81 const RETURN_CAP_BYTES: usize = 16 * 1024;
82
83 /// Names refused before any other check, with a message that names the
84 /// supported alternative. Checked against the requested name; the read-only
85 /// and auto-approve gates below would refuse most of these anyway, but the
86 /// explicit list keeps the receipt diagnostic instead of puzzling.
87 const PROHIBITED_NESTED: &[&str] = &[
88 EXECUTE_TOOLS_TOOL_NAME,
89 "code_execution",
90 "js_execution",
91 "agent",
92 "workflow",
93 "tool_search",
94 ];
95
96 /// Model-facing definition. `defer_loading` is decided by the catalog (this
97 /// name is not in the eager set, so it stays deferred); `allowed_callers`
98 /// mirrors the interpreter tools.
99 pub fn execute_tools_tool_definition() -> Tool {
100 Tool {
101 tool_type: Some(EXECUTE_TOOLS_TOOL_TYPE.to_string()),
102 name: EXECUTE_TOOLS_TOOL_NAME.to_string(),
103 description: "Execute a JavaScript program that composes read-only tool calls via \
104 tools.call(name, args) and returns a bounded JSON result. Discover tool \
105 names and schemas with tool_search BEFORE writing the program. Phase-1 \
106 limits: nested calls must be read-only and auto-approved; writes, \
107 shell, subagents, workflows, MCP tools, and nested execute_tools abort \
108 the program with a receipt. At most 50 nested calls, 4 concurrent, \
109 30s per run, 16 KiB returned. Intermediate results stay in the \
110 program; return only what the next decision needs."
111 .to_string(),
112 input_schema: json!({
113 "type": "object",
114 "properties": {
115 "code": {
116 "type": "string",
117 "description": "JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls."
118 }
119 },
120 "required": ["code"]
121 }),
122 allowed_callers: Some(vec!["direct".to_string()]),
123 defer_loading: Some(false),
124 input_examples: None,
125 strict: None,
126 cache_control: None,
127 }
128 }
129
130 /// One nested call, as recorded by the host — not the script. The receipt is
131 /// what makes "no failures found" distinguishable from "nothing ran".
132 #[derive(Debug, Clone, serde::Serialize)]
133 struct CallReceipt {
134 tool: String,
135 ok: bool,
136 elapsed_ms: u64,
137 bytes: usize,
138 truncated: bool,
139 note: Option<String>,
140 }
141
142 /// [`ToolInvoker`] over a snapshot of the parent turn's registry.
143 ///
144 /// The snapshot (spec Arcs plus a cloned [`ToolContext`]) is taken at
145 /// dispatch so the invoker is `'static` for the VM thread. Deny-lists and
146 /// the authority envelope are re-enforced per call from the cloned context,
147 /// so a program never outranks the turn that launched it.
148 pub(crate) struct CodemodeInvoker {
149 specs: Vec<Arc<dyn ToolSpec>>,
150 context: ToolContext,
151 semaphore: Arc<Semaphore>,
152 receipts: Mutex<Vec<CallReceipt>>,
153 }
154
155 impl CodemodeInvoker {
156 fn new(specs: Vec<Arc<dyn ToolSpec>>, context: ToolContext) -> Self {
157 Self {
158 specs,
159 context,
160 semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_CALLS)),
161 receipts: Mutex::new(Vec::new()),
162 }
163 }
164
165 fn record(&self, receipt: CallReceipt) {
166 if let Ok(mut receipts) = self.receipts.lock() {
167 receipts.push(receipt);
168 }
169 }
170
171 fn drain(&self) -> Vec<CallReceipt> {
172 self.receipts
173 .lock()
174 .map(|receipts| receipts.clone())
175 .unwrap_or_default()
176 }
177
178 fn refused(&self, tool: &str, started: Instant, note: String) -> DriverError {
179 self.record(CallReceipt {
180 tool: tool.to_string(),
181 ok: false,
182 elapsed_ms: started.elapsed().as_millis() as u64,
183 bytes: 0,
184 truncated: false,
185 note: Some(note.clone()),
186 });
187 DriverError::Rejected(note)
188 }
189 }
190
191 #[async_trait]
192 impl ToolInvoker for CodemodeInvoker {
193 async fn invoke(&self, request: ToolCallRequest) -> Result<ToolCallResponse, DriverError> {
194 let _permit = self
195 .semaphore
196 .clone()
197 .acquire_owned()
198 .await
199 .map_err(|_| DriverError::Unavailable("code-mode run shut down".to_string()))?;
200 let started = Instant::now();
201 let name = request.tool.as_str();
202
203 if PROHIBITED_NESTED.contains(&name) {
204 return Err(self.refused(
205 name,
206 started,
207 format!(
208 "`{name}` is not available inside execute_tools programs; use workflow/task() for fan-out and tool_search before writing the program"
209 ),
210 ));
211 }
212 if crate::mcp::McpPool::is_mcp_tool(name) {
213 return Err(self.refused(
214 name,
215 started,
216 "MCP tools are excluded from Phase-1 code mode".to_string(),
217 ));
218 }
219 let Some(spec) = self.specs.iter().find(|spec| spec.name() == name) else {
220 return Err(self.refused(
221 name,
222 started,
223 format!("unknown tool `{name}`; discover names with tool_search before writing the program"),
224 ));
225 };
226 if !spec.is_read_only_for(&request.input) {
227 return Err(self.refused(
228 name,
229 started,
230 format!("`{name}` can mutate; Phase-1 code mode executes read-only calls only"),
231 ));
232 }
233 if spec.approval_requirement_for(&request.input) != ApprovalRequirement::Auto {
234 return Err(self.refused(
235 name,
236 started,
237 format!(
238 "`{name}` needs approval; Phase-1 code mode executes only auto-approved calls"
239 ),
240 ));
241 }
242 if let Err(err) = enforce_tool_authority(name, &request.input, spec.as_ref(), &self.context)
243 {
244 return Err(self.refused(name, started, err.to_string()));
245 }
246
247 match spec
248 .execute_rich(request.input.clone(), &self.context)
249 .await
250 {
251 Ok(rich) => {
252 let result = rich.into_result();
253 // Stable envelope: the tool's text content (parsed as JSON
254 // when it is JSON) plus its structured metadata, so
255 // continuation and truncation notices survive the bridge.
256 let content = serde_json::from_str(&result.content)
257 .unwrap_or_else(|_| Value::String(result.content.clone()));
258 let payload = json!({
259 "content": content,
260 "metadata": result.metadata.clone().unwrap_or(Value::Null),
261 });
262 let raw_len = payload.to_string().len();
263 let (bounded, truncated) = bound_json(payload, PER_CALL_RESULT_CAP_BYTES);
264 self.record(CallReceipt {
265 tool: name.to_string(),
266 ok: result.success,
267 elapsed_ms: started.elapsed().as_millis() as u64,
268 bytes: raw_len,
269 truncated,
270 note: None,
271 });
272 Ok(ToolCallResponse {
273 ok: result.success,
274 result: if result.success {
275 bounded
276 } else {
277 Value::String(result.content)
278 },
279 })
280 }
281 Err(err) => {
282 let message = err.to_string();
283 // Validation-shaped failures mean nothing ran (admission);
284 // execution failures ran and failed (agent kind via ok:false);
285 // seam breaks are unavailable.
286 match err {
287 ToolError::InvalidInput { .. }
288 | ToolError::MissingField { .. }
289 | ToolError::PathEscape { .. }
290 | ToolError::PermissionDenied { .. } => {
291 Err(self.refused(name, started, message))
292 }
293 ToolError::Timeout { .. }
294 | ToolError::Cancelled { .. }
295 | ToolError::NotAvailable { .. } => Err(DriverError::Unavailable(message)),
296 ToolError::ExecutionFailed { .. } => {
297 self.record(CallReceipt {
298 tool: name.to_string(),
299 ok: false,
300 elapsed_ms: started.elapsed().as_millis() as u64,
301 bytes: message.len(),
302 truncated: false,
303 note: None,
304 });
305 Ok(ToolCallResponse {
306 ok: false,
307 result: Value::String(message),
308 })
309 }
310 }
311 }
312 }
313 }
314 }
315
316 /// [`WorkflowDriver`] for code-mode runs: `task()` is refused (fan-out stays
317 /// with `workflow`), the token budget is unconstrained, and progress events
318 /// feed the run receipt.
319 pub(crate) struct CodemodeDriver {
320 events: Mutex<Vec<ProgressEvent>>,
321 }
322
323 impl Default for CodemodeDriver {
324 fn default() -> Self {
325 Self {
326 events: Mutex::new(Vec::new()),
327 }
328 }
329 }
330
331 impl CodemodeDriver {
332 fn log_lines(&self) -> Vec<String> {
333 self.events
334 .lock()
335 .map(|events| {
336 events
337 .iter()
338 .filter_map(|event| match event {
339 ProgressEvent::Log { message } => Some(message.clone()),
340 _ => None,
341 })
342 .collect()
343 })
344 .unwrap_or_default()
345 }
346 }
347
348 #[async_trait]
349 impl WorkflowDriver for CodemodeDriver {
350 async fn spawn_task(&self, _request: TaskRequest) -> Result<SpawnedTask, DriverError> {
351 Err(DriverError::Rejected(
352 "task() is unavailable in execute_tools programs; tools.call() composes direct tool calls"
353 .to_string(),
354 ))
355 }
356
357 fn budget(&self) -> BudgetSnapshot {
358 BudgetSnapshot {
359 total: None,
360 spent: 0,
361 }
362 }
363
364 fn progress(&self, event: ProgressEvent) {
365 if let Ok(mut events) = self.events.lock() {
366 events.push(event);
367 }
368 }
369
370 fn cancel_all(&self) {}
371 }
372
373 /// Bound a JSON value to `cap` serialized bytes, replacing oversize payloads
374 /// with a preview that stays valid JSON.
375 fn bound_json(value: Value, cap: usize) -> (Value, bool) {
376 let raw = value.to_string();
377 if raw.len() <= cap {
378 return (value, false);
379 }
380 let preview: String = raw.chars().take(cap / 2).collect();
381 (
382 json!({
383 "_truncated": true,
384 "bytes": raw.len(),
385 "preview": preview,
386 }),
387 true,
388 )
389 }
390
391 fn receipt_payload(
392 success: bool,
393 body: Value,
394 invoker: &CodemodeInvoker,
395 driver: &CodemodeDriver,
396 ) -> ToolResult {
397 let calls = invoker.drain();
398 let content = json!({
399 "success": success,
400 "body": body,
401 "nested_calls": calls.len(),
402 "calls": calls,
403 "log": driver.log_lines(),
404 })
405 .to_string();
406 ToolResult {
407 content,
408 success,
409 metadata: None,
410 }
411 }
412
413 /// Execute one `execute_tools` call: validate, run the program under the run
414 /// deadline, and return the bounded program value plus the host-owned
415 /// receipt. A script failure is a `success: false` payload, not a host
416 /// error — only VM and deadline failures are `Err`.
417 pub async fn execute_tools_tool(
418 input: &Value,
419 registry: &ToolRegistry,
420 context: &ToolContext,
421 ) -> Result<ToolResult, ToolError> {
422 let code = required_str(input, "code")?;
423 if code.trim().is_empty() {
424 return Err(ToolError::missing_field("code"));
425 }
426 if code.len() > MAX_CODE_BYTES {
427 return Err(ToolError::invalid_input(format!(
428 "code exceeds {MAX_CODE_BYTES} bytes"
429 )));
430 }
431 let invoker = Arc::new(CodemodeInvoker::new(registry.all(), context.clone()));
432 let driver = Arc::new(CodemodeDriver::default());
433 let outcome = timeout(
434 Duration::from_secs(RUN_DEADLINE_SECS),
435 WorkflowVm::new().run_tools_script(
436 code,
437 Value::Null,
438 driver.clone(),
439 invoker.clone(),
440 WorkflowRunCancel::new(),
441 ),
442 )
443 .await;
444 let program_result = match outcome {
445 Err(_) => {
446 return Err(ToolError::Timeout {
447 seconds: RUN_DEADLINE_SECS,
448 });
449 }
450 Ok(Err(err)) => {
451 return Ok(receipt_payload(
452 false,
453 json!({ "error": err.to_string() }),
454 &invoker,
455 &driver,
456 ));
457 }
458 Ok(Ok(value)) => value,
459 };
460 let (bounded, truncated) = bound_json(program_result, RETURN_CAP_BYTES);
461 Ok(receipt_payload(
462 true,
463 json!({ "return": bounded, "return_truncated": truncated }),
464 &invoker,
465 &driver,
466 ))
467 }
468
469 #[cfg(test)]
470 mod tests {
471 use super::*;
472
473 #[test]
474 fn catalog_advertises_execute_tools_deferred_outside_plan() {
475 use codewhale_config::AppMode;
476 use std::collections::HashSet;
477 let empty = HashSet::new();
478 for mode in [AppMode::Agent, AppMode::Operate] {
479 let mut catalog = Vec::new();
480 crate::core::engine::tool_catalog::ensure_advanced_tooling(
481 &mut catalog,
482 mode,
483 &empty,
484 ToolMode::Direct,
485 );
486 let tool = catalog
487 .iter()
488 .find(|tool| tool.name == EXECUTE_TOOLS_TOOL_NAME)
489 .unwrap_or_else(|| panic!("{mode:?} catalog must advertise execute_tools"));
490 assert_eq!(tool.defer_loading, Some(true), "{mode:?} must defer it");
491 }
492 let mut catalog = Vec::new();
493 crate::core::engine::tool_catalog::ensure_advanced_tooling(
494 &mut catalog,
495 AppMode::Plan,
496 &empty,
497 ToolMode::Direct,
498 );
499 assert!(
500 catalog
501 .iter()
502 .all(|tool| tool.name != EXECUTE_TOOLS_TOOL_NAME)
503 );
504 }
505
506 #[test]
507 fn definition_is_deferred_direct_only() {
508 let tool = execute_tools_tool_definition();
509 assert_eq!(tool.name, EXECUTE_TOOLS_TOOL_NAME);
510 assert_eq!(tool.tool_type.as_deref(), Some(EXECUTE_TOOLS_TOOL_TYPE));
511 assert!(tool.description.contains("tools.call"));
512 // Catalog decides deferral; the name is absent from the eager set,
513 // so injection marks it deferred like the interpreter tools.
514 assert!(
515 !crate::core::engine::tool_catalog::DEFAULT_ACTIVE_NATIVE_TOOLS
516 .contains(&tool.name.as_str())
517 );
518 assert_eq!(tool.allowed_callers, Some(vec!["direct".to_string()]));
519 }
520
521 #[test]
522 fn bound_json_keeps_small_values_verbatim() {
523 let (value, truncated) = bound_json(json!({"a": 1}), 1024);
524 assert!(!truncated);
525 assert_eq!(value, json!({"a": 1}));
526 }
527
528 #[test]
529 fn bound_json_truncates_to_valid_json_with_preview() {
530 let big = "x".repeat(100);
531 let (value, truncated) = bound_json(json!({ "blob": big }), 64);
532 assert!(truncated);
533 assert_eq!(value["bytes"], json!(111));
534 assert!(value["preview"].as_str().is_some());
535 }
536
537 use crate::core::engine::tool_catalog::ToolMode;
538 use crate::tools::file_tool::{ReadTool, WriteTool};
539 use crate::tools::registry::ToolRegistryBuilder;
540
541 fn workspace_with_note() -> (tempfile::TempDir, std::path::PathBuf) {
542 let dir = tempfile::tempdir().unwrap();
543 let note = dir.path().join("note.txt");
544 std::fs::write(&note, "alpha\nbeta\n").unwrap();
545 (dir, note)
546 }
547
548 #[tokio::test]
549 async fn nested_read_passes_gates_and_records_receipt() {
550 let (_dir, note) = workspace_with_note();
551 let context = ToolContext::new(note.parent().unwrap());
552 let invoker = CodemodeInvoker::new(vec![Arc::new(ReadTool)], context);
553 let response = invoker
554 .invoke(ToolCallRequest {
555 tool: "read".to_string(),
556 input: json!({ "path": note.to_string_lossy() }),
557 })
558 .await
559 .unwrap();
560 assert!(response.ok);
561 assert!(response.result.to_string().contains("alpha"));
562 let receipts = invoker.drain();
563 assert_eq!(receipts.len(), 1);
564 assert_eq!(receipts[0].tool, "read");
565 assert!(receipts[0].ok);
566 }
567
568 #[tokio::test]
569 async fn nested_write_is_refused_and_writes_nothing() {
570 let (_dir, note) = workspace_with_note();
571 let target = note.parent().unwrap().join("evil.txt");
572 let context = ToolContext::new(note.parent().unwrap());
573 let invoker = CodemodeInvoker::new(vec![Arc::new(WriteTool)], context);
574 let err = invoker
575 .invoke(ToolCallRequest {
576 tool: "write".to_string(),
577 input: json!({ "path": target.to_string_lossy(), "content": "x" }),
578 })
579 .await
580 .unwrap_err();
581 assert!(
582 matches!(&err, DriverError::Rejected(message) if message.contains("read-only")),
583 "unexpected: {err:?}"
584 );
585 assert!(!target.exists());
586 assert_eq!(invoker.drain().len(), 1);
587 }
588
589 #[tokio::test]
590 async fn prohibited_nested_names_are_refused() {
591 let dir = tempfile::tempdir().unwrap();
592 let context = ToolContext::new(dir.path());
593 let invoker = CodemodeInvoker::new(vec![], context);
594 for name in ["agent", "workflow", "execute_tools", "tool_search", "nope"] {
595 let err = invoker
596 .invoke(ToolCallRequest {
597 tool: name.to_string(),
598 input: json!({}),
599 })
600 .await
601 .unwrap_err();
602 assert!(matches!(err, DriverError::Rejected(_)), "{name}: {err:?}");
603 }
604 }
605
606 #[tokio::test]
607 async fn program_composes_nested_read_and_returns_receipt() {
608 let (_dir, note) = workspace_with_note();
609 let workspace = note.parent().unwrap().to_path_buf();
610 let context = ToolContext::new(workspace.clone());
611 let registry = ToolRegistryBuilder::new()
612 .with_tool(Arc::new(ReadTool))
613 .build(context.clone());
614 let path = note.to_string_lossy().replace('\\', "\\\\");
615 let code = format!(
616 "const r = await tools.call('read', {{ path: '{path}' }}); return {{ hasAlpha: JSON.stringify(r).includes('alpha') }};"
617 );
618 let result = execute_tools_tool(&json!({ "code": code }), &registry, &context)
619 .await
620 .unwrap();
621 assert!(result.success, "{}", result.content);
622 let body: Value = serde_json::from_str(&result.content).unwrap();
623 assert_eq!(body["nested_calls"], 1);
624 assert_eq!(body["body"]["return"]["hasAlpha"], true);
625 }
626
627 #[tokio::test]
628 async fn program_loads_skills_at_runtime_through_load_skill() {
629 // Skills-as-tools composes with code mode: `load_skill` is
630 // read-only and auto-approved, so a program can list and load
631 // skills at runtime without widening its authority.
632 let dir = tempfile::tempdir().unwrap();
633 let skill_dir = dir.path().join(".agents/skills/greet");
634 std::fs::create_dir_all(&skill_dir).unwrap();
635 std::fs::write(
636 skill_dir.join("SKILL.md"),
637 "---\nname: greet\ndescription: Say hello\n---\n# Greet\nSay hello warmly.\n",
638 )
639 .unwrap();
640 let context = ToolContext::new(dir.path());
641 let registry = ToolRegistryBuilder::new()
642 .with_tool(Arc::new(crate::tools::skill::LoadSkillTool))
643 .build(context.clone());
644 let code = "const list = await tools.call('load_skill', { name: 'list' }); \
645 const body = await tools.call('load_skill', { name: 'greet' }); \
646 return { listed: JSON.stringify(list).includes('greet'), \
647 loaded: JSON.stringify(body).includes('warmly') };";
648 let result = execute_tools_tool(&json!({ "code": code }), &registry, &context)
649 .await
650 .unwrap();
651 assert!(result.success, "{}", result.content);
652 let body: Value = serde_json::from_str(&result.content).unwrap();
653 assert_eq!(body["nested_calls"], 2);
654 assert_eq!(body["body"]["return"]["listed"], true);
655 assert_eq!(body["body"]["return"]["loaded"], true);
656 }
657 }
658
658 lines RUST