skilllite 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

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.
skilllite/mcp/server.py CHANGED
@@ -98,7 +98,27 @@ except ImportError:
98
98
 
99
99
  class SecurityScanResult:
100
100
  """Result of a security scan."""
101
-
101
+
102
+ # Issue types that are HARD BLOCKED in L3 sandbox (cannot execute even with confirmation)
103
+ # These operations are blocked at the sandbox runtime level, not just static analysis
104
+ HARD_BLOCKED_ISSUE_TYPES_L3 = {
105
+ "Process Execution", # os.system, subprocess, etc.
106
+ "ProcessExecution", # Alternative format
107
+ "process_execution", # Snake case format
108
+ }
109
+
110
+ # Rule IDs that are specifically hard blocked in L3 sandbox
111
+ HARD_BLOCKED_RULE_IDS_L3 = {
112
+ "py-subprocess", # subprocess.call/run/Popen
113
+ "py-os-system", # os.system/popen/spawn
114
+ "js-child-process", # child_process.exec/spawn
115
+ }
116
+
117
+ # Dangerous module imports that lead to hard blocks when combined with execution
118
+ HARD_BLOCKED_MODULES_L3 = {
119
+ "py-os-import", # import os/subprocess/shutil
120
+ }
121
+
102
122
  def __init__(
103
123
  self,
104
124
  is_safe: bool,
@@ -108,6 +128,7 @@ class SecurityScanResult:
108
128
  high_severity_count: int = 0,
109
129
  medium_severity_count: int = 0,
110
130
  low_severity_count: int = 0,
131
+ sandbox_level: int = 3,
111
132
  ):
112
133
  self.is_safe = is_safe
113
134
  self.issues = issues
@@ -116,8 +137,31 @@ class SecurityScanResult:
116
137
  self.high_severity_count = high_severity_count
117
138
  self.medium_severity_count = medium_severity_count
118
139
  self.low_severity_count = low_severity_count
140
+ self.sandbox_level = sandbox_level
119
141
  self.timestamp = time.time()
120
-
142
+
143
+ # Calculate hard blocked issues
144
+ self.hard_blocked_issues = self._find_hard_blocked_issues()
145
+ self.has_hard_blocked = len(self.hard_blocked_issues) > 0
146
+
147
+ def _find_hard_blocked_issues(self) -> List[Dict[str, Any]]:
148
+ """Find issues that are hard blocked in the current sandbox level."""
149
+ if self.sandbox_level < 3:
150
+ # Only L3 has hard blocks
151
+ return []
152
+
153
+ hard_blocked = []
154
+ for issue in self.issues:
155
+ issue_type = issue.get("issue_type", "")
156
+ rule_id = issue.get("rule_id", "")
157
+
158
+ # Check if this issue type or rule is hard blocked
159
+ if (issue_type in self.HARD_BLOCKED_ISSUE_TYPES_L3 or
160
+ rule_id in self.HARD_BLOCKED_RULE_IDS_L3):
161
+ hard_blocked.append(issue)
162
+
163
+ return hard_blocked
164
+
121
165
  def to_dict(self) -> Dict[str, Any]:
122
166
  return {
123
167
  "is_safe": self.is_safe,
@@ -127,42 +171,64 @@ class SecurityScanResult:
127
171
  "high_severity_count": self.high_severity_count,
128
172
  "medium_severity_count": self.medium_severity_count,
129
173
  "low_severity_count": self.low_severity_count,
130
- "requires_confirmation": self.high_severity_count > 0,
174
+ "requires_confirmation": self.high_severity_count > 0 and not self.has_hard_blocked,
175
+ "has_hard_blocked": self.has_hard_blocked,
176
+ "hard_blocked_count": len(self.hard_blocked_issues),
177
+ "sandbox_level": self.sandbox_level,
131
178
  }
132
-
179
+
133
180
  def format_report(self) -> str:
134
181
  """Format a human-readable security report."""
135
182
  if not self.issues:
136
183
  return "✅ Security scan passed. No issues found."
137
-
184
+
138
185
  lines = [
139
186
  f"📋 Security Scan Report (ID: {self.scan_id[:8]})",
187
+ f" Sandbox Level: L{self.sandbox_level}",
140
188
  f" Found {len(self.issues)} item(s) for review:",
141
189
  "",
142
190
  ]
143
-
191
+
144
192
  severity_icons = {
145
193
  "Critical": "🔴",
146
194
  "High": "🟠",
147
195
  "Medium": "🟡",
148
196
  "Low": "🟢",
149
197
  }
150
-
198
+
151
199
  for idx, issue in enumerate(self.issues, 1):
152
200
  severity = issue.get("severity", "Medium")
153
201
  icon = severity_icons.get(severity, "⚪")
154
- lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}")
202
+
203
+ # Mark hard blocked issues
204
+ is_hard_blocked = issue in self.hard_blocked_issues
205
+ block_marker = " 🚫 [HARD BLOCKED]" if is_hard_blocked else ""
206
+
207
+ lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}{block_marker}")
155
208
  lines.append(f" ├─ Rule: {issue.get('rule_id', 'N/A')}")
156
209
  lines.append(f" ├─ Line {issue.get('line_number', '?')}: {issue.get('description', '')}")
157
210
  lines.append(f" └─ Code: {issue.get('code_snippet', '')[:60]}...")
158
211
  lines.append("")
159
-
160
- if self.high_severity_count > 0:
212
+
213
+ # Different messages based on whether there are hard blocked issues
214
+ if self.has_hard_blocked:
215
+ lines.append("🚫 HARD BLOCKED: This code contains operations that CANNOT be executed")
216
+ lines.append(f" in the current L{self.sandbox_level} sandbox environment.")
217
+ lines.append("")
218
+ lines.append(" The following operations are permanently blocked at runtime:")
219
+ for issue in self.hard_blocked_issues:
220
+ lines.append(f" • {issue.get('issue_type', 'Unknown')}: {issue.get('description', '')}")
221
+ lines.append("")
222
+ lines.append(" ⚠️ Even with confirmation, this code will fail to execute.")
223
+ lines.append(" Options:")
224
+ lines.append(" 1. Modify the code to remove blocked operations")
225
+ lines.append(" 2. Use a lower sandbox level (L1 or L2) if permitted")
226
+ elif self.high_severity_count > 0:
161
227
  lines.append("⚠️ High severity issues found. Confirmation required to execute.")
162
228
  lines.append(f" To proceed, call execute_code with confirmed=true and scan_id=\"{self.scan_id}\"")
163
229
  else:
164
230
  lines.append("ℹ️ Only low/medium severity issues found. Safe to execute.")
165
-
231
+
166
232
  return "\n".join(lines)
167
233
 
168
234
 
@@ -243,26 +309,37 @@ This skill executes code from MCP.
243
309
 
244
310
  return skill_dir, code_file
245
311
 
246
- def scan_code(self, language: str, code: str) -> SecurityScanResult:
247
- """Scan code for security issues without executing it."""
312
+ def scan_code(self, language: str, code: str, sandbox_level: Optional[int] = None) -> SecurityScanResult:
313
+ """Scan code for security issues without executing it.
314
+
315
+ Args:
316
+ language: Programming language (python, javascript, bash)
317
+ code: Code to scan
318
+ sandbox_level: Sandbox level to check against (default: from env or 3)
319
+ """
320
+ # Use default sandbox level if not specified
321
+ if sandbox_level is None:
322
+ sandbox_level = self.default_sandbox_level
323
+
248
324
  if not self.runtime_available:
249
325
  return SecurityScanResult(
250
326
  is_safe=False,
251
- issues=[{"severity": "Critical", "issue_type": "RuntimeError",
327
+ issues=[{"severity": "Critical", "issue_type": "RuntimeError",
252
328
  "description": f"skillbox not found at {self.skillbox_path}",
253
329
  "rule_id": "system", "line_number": 0, "code_snippet": ""}],
254
330
  scan_id="error",
255
331
  code_hash="",
256
332
  high_severity_count=1,
333
+ sandbox_level=sandbox_level,
257
334
  )
258
-
335
+
259
336
  self._cleanup_expired_scans()
260
337
  code_hash = self._generate_code_hash(language, code)
261
338
  scan_id = self._generate_scan_id(code_hash)
262
-
339
+
263
340
  try:
264
341
  skill_dir, code_file = self._create_temp_skill(language, code)
265
-
342
+
266
343
  try:
267
344
  result = subprocess.run(
268
345
  [self.skillbox_path, "security-scan", code_file],
@@ -270,12 +347,12 @@ This skill executes code from MCP.
270
347
  text=True,
271
348
  timeout=30
272
349
  )
273
-
350
+
274
351
  issues = self._parse_scan_output(result.stdout + result.stderr)
275
352
  high_count = sum(1 for i in issues if i.get("severity") in ["Critical", "High"])
276
353
  medium_count = sum(1 for i in issues if i.get("severity") == "Medium")
277
354
  low_count = sum(1 for i in issues if i.get("severity") == "Low")
278
-
355
+
279
356
  scan_result = SecurityScanResult(
280
357
  is_safe=high_count == 0,
281
358
  issues=issues,
@@ -284,14 +361,15 @@ This skill executes code from MCP.
284
361
  high_severity_count=high_count,
285
362
  medium_severity_count=medium_count,
286
363
  low_severity_count=low_count,
364
+ sandbox_level=sandbox_level,
287
365
  )
288
-
366
+
289
367
  self._scan_cache[scan_id] = scan_result
290
368
  return scan_result
291
-
369
+
292
370
  finally:
293
371
  shutil.rmtree(skill_dir, ignore_errors=True)
294
-
372
+
295
373
  except subprocess.TimeoutExpired:
296
374
  return SecurityScanResult(
297
375
  is_safe=False,
@@ -301,6 +379,7 @@ This skill executes code from MCP.
301
379
  scan_id=scan_id,
302
380
  code_hash=code_hash,
303
381
  high_severity_count=1,
382
+ sandbox_level=sandbox_level,
304
383
  )
305
384
  except Exception as e:
306
385
  return SecurityScanResult(
@@ -311,6 +390,7 @@ This skill executes code from MCP.
311
390
  scan_id=scan_id,
312
391
  code_hash=code_hash,
313
392
  high_severity_count=1,
393
+ sandbox_level=sandbox_level,
314
394
  )
315
395
 
316
396
  def _parse_scan_output(self, output: str) -> List[Dict[str, Any]]:
@@ -412,9 +492,31 @@ This skill executes code from MCP.
412
492
  }
413
493
 
414
494
  code_hash = self._generate_code_hash(language, code)
415
-
495
+
416
496
  if sandbox_level >= 3 and not confirmed:
417
- scan_result = self.scan_code(language, code)
497
+ scan_result = self.scan_code(language, code, sandbox_level=sandbox_level)
498
+
499
+ # Check for hard blocked issues first
500
+ if scan_result.has_hard_blocked:
501
+ return {
502
+ "success": False,
503
+ "stdout": "",
504
+ "stderr": (
505
+ f"🚫 Execution Blocked\n\n"
506
+ f"{scan_result.format_report()}\n\n"
507
+ f"❌ This code contains operations that are PERMANENTLY BLOCKED\n"
508
+ f" in the L{sandbox_level} sandbox environment.\n\n"
509
+ f" Even with confirmation, this code CANNOT be executed.\n\n"
510
+ f"Options:\n"
511
+ f" 1. Modify the code to remove blocked operations\n"
512
+ f" 2. Use sandbox_level=1 or sandbox_level=2 (if permitted)\n"
513
+ ),
514
+ "exit_code": 4,
515
+ "hard_blocked": True,
516
+ "security_issues": scan_result.to_dict(),
517
+ }
518
+
519
+ # Soft risk: can be confirmed
418
520
  if scan_result.high_severity_count > 0:
419
521
  return {
420
522
  "success": False,
@@ -422,7 +524,9 @@ This skill executes code from MCP.
422
524
  "stderr": (
423
525
  f"🔐 Security Review Required\n\n"
424
526
  f"{scan_result.format_report()}\n\n"
425
- f"To execute this code, call execute_code again with:\n"
527
+ f"⚠️ IMPORTANT: You MUST ask the user for confirmation before proceeding.\n"
528
+ f"Show this security report to the user and wait for their explicit approval.\n\n"
529
+ f"If the user approves, call execute_code again with:\n"
426
530
  f" - confirmed: true\n"
427
531
  f" - scan_id: \"{scan_result.scan_id}\"\n"
428
532
  ),
@@ -431,7 +535,7 @@ This skill executes code from MCP.
431
535
  "scan_id": scan_result.scan_id,
432
536
  "security_issues": scan_result.to_dict(),
433
537
  }
434
-
538
+
435
539
  if confirmed and scan_id:
436
540
  cached_result = self.verify_scan(scan_id, code_hash)
437
541
  if not cached_result:
@@ -444,6 +548,28 @@ This skill executes code from MCP.
444
548
  ),
445
549
  "exit_code": 3,
446
550
  }
551
+
552
+ # Even with confirmation, check for hard blocked issues
553
+ if cached_result.has_hard_blocked:
554
+ return {
555
+ "success": False,
556
+ "stdout": "",
557
+ "stderr": (
558
+ f"🚫 Execution Blocked (Even After Confirmation)\n\n"
559
+ f"The code contains operations that are PERMANENTLY BLOCKED\n"
560
+ f"in the L{sandbox_level} sandbox environment:\n\n"
561
+ + "\n".join(f" • {issue.get('issue_type', 'Unknown')}: {issue.get('description', '')}"
562
+ for issue in cached_result.hard_blocked_issues) +
563
+ f"\n\n"
564
+ f"❌ Confirmation cannot override sandbox runtime restrictions.\n\n"
565
+ f"Options:\n"
566
+ f" 1. Modify the code to remove blocked operations\n"
567
+ f" 2. Use sandbox_level=1 or sandbox_level=2 (if permitted)\n"
568
+ ),
569
+ "exit_code": 4,
570
+ "hard_blocked": True,
571
+ "security_issues": cached_result.to_dict(),
572
+ }
447
573
 
448
574
  try:
449
575
  skill_dir, _ = self._create_temp_skill(language, code)
@@ -594,7 +720,10 @@ class MCPServer:
594
720
  description=(
595
721
  "Execute a skill with the given input parameters. "
596
722
  "Use list_skills to see available skills and "
597
- "get_skill_info to understand required parameters."
723
+ "get_skill_info to understand required parameters. "
724
+ "IMPORTANT: If the skill has high-severity security issues, "
725
+ "you MUST show the security report to the user and ASK for their explicit confirmation "
726
+ "before setting confirmed=true. Do NOT auto-confirm without user approval."
598
727
  ),
599
728
  inputSchema={
600
729
  "type": "object",
@@ -606,6 +735,17 @@ class MCPServer:
606
735
  "input": {
607
736
  "type": "object",
608
737
  "description": "Input parameters for the skill"
738
+ },
739
+ "confirmed": {
740
+ "type": "boolean",
741
+ "description": (
742
+ "Set to true ONLY after the user has explicitly approved execution. "
743
+ "You must ask the user for confirmation first."
744
+ )
745
+ },
746
+ "scan_id": {
747
+ "type": "string",
748
+ "description": "Scan ID from security review (required when confirmed=true)"
609
749
  }
610
750
  },
611
751
  "required": ["skill_name"]
@@ -639,8 +779,9 @@ class MCPServer:
639
779
  name="execute_code",
640
780
  description=(
641
781
  "Execute code in a secure sandbox environment. "
642
- "If security issues are found, you must set confirmed=true "
643
- "and provide the scan_id from a previous scan_code call."
782
+ "IMPORTANT: If security issues are found, you MUST show the security report "
783
+ "to the user and ASK for their explicit confirmation before setting confirmed=true. "
784
+ "Do NOT auto-confirm without user approval."
644
785
  ),
645
786
  inputSchema={
646
787
  "type": "object",
@@ -657,8 +798,8 @@ class MCPServer:
657
798
  "confirmed": {
658
799
  "type": "boolean",
659
800
  "description": (
660
- "Set to true to confirm execution despite security warnings. "
661
- "Required when high-severity issues are found."
801
+ "Set to true ONLY after the user has explicitly approved execution. "
802
+ "You must ask the user for confirmation first."
662
803
  ),
663
804
  "default": False
664
805
  },
@@ -772,6 +913,18 @@ class MCPServer:
772
913
  sandbox_level=sandbox_level,
773
914
  )
774
915
 
916
+ # Handle hard blocked case - this is a definitive block, not a confirmation request
917
+ if result.get("hard_blocked"):
918
+ return CallToolResult(
919
+ isError=True,
920
+ content=[
921
+ TextContent(
922
+ type="text",
923
+ text=result["stderr"]
924
+ )
925
+ ]
926
+ )
927
+
775
928
  if result.get("requires_confirmation"):
776
929
  return CallToolResult(
777
930
  content=[
@@ -781,15 +934,15 @@ class MCPServer:
781
934
  )
782
935
  ]
783
936
  )
784
-
937
+
785
938
  output_lines = []
786
939
  if result["stdout"]:
787
940
  output_lines.append(f"Output:\n{result['stdout']}")
788
941
  if result["stderr"]:
789
942
  output_lines.append(f"Errors:\n{result['stderr']}")
790
-
943
+
791
944
  output_text = "\n".join(output_lines) if output_lines else "Execution completed successfully (no output)"
792
-
945
+
793
946
  if result["success"]:
794
947
  return CallToolResult(
795
948
  content=[
@@ -921,9 +1074,18 @@ class MCPServer:
921
1074
  )
922
1075
 
923
1076
  async def _handle_run_skill(self, arguments: Dict[str, Any]) -> "CallToolResult":
924
- """Handle run_skill tool call."""
1077
+ """
1078
+ Handle run_skill tool call using UnifiedExecutionService.
1079
+
1080
+ This method uses the unified execution layer which:
1081
+ 1. Reads sandbox level at runtime
1082
+ 2. Handles security scanning
1083
+ 3. Properly downgrades sandbox level after confirmation
1084
+ """
925
1085
  skill_name = arguments.get("skill_name")
926
1086
  input_data = arguments.get("input", {})
1087
+ confirmed = arguments.get("confirmed", False)
1088
+ scan_id = arguments.get("scan_id")
927
1089
 
928
1090
  if not skill_name:
929
1091
  return CallToolResult(
@@ -959,36 +1121,82 @@ class MCPServer:
959
1121
  ]
960
1122
  )
961
1123
 
962
- # Execute the skill
963
- try:
964
- result = self.skill_manager.execute(skill_name, input_data)
1124
+ # Get skill info
1125
+ skill = self.skill_manager.get_skill(skill_name)
1126
+ if not skill:
1127
+ return CallToolResult(
1128
+ isError=True,
1129
+ content=[
1130
+ TextContent(
1131
+ type="text",
1132
+ text=f"Could not load skill: {skill_name}"
1133
+ )
1134
+ ]
1135
+ )
965
1136
 
966
- if result.success:
967
- return CallToolResult(
968
- content=[
969
- TextContent(
970
- type="text",
971
- text=f"Skill '{skill_name}' executed successfully.\n\nOutput:\n{result.output}"
972
- )
973
- ]
974
- )
975
- else:
976
- return CallToolResult(
977
- isError=True,
978
- content=[
979
- TextContent(
980
- type="text",
981
- text=f"Skill '{skill_name}' execution failed.\n\nError:\n{result.error}"
1137
+ # Use UnifiedExecutionService
1138
+ from ..sandbox.execution_service import UnifiedExecutionService
1139
+ from ..sandbox.context import ExecutionContext
1140
+
1141
+ service = UnifiedExecutionService.get_instance()
1142
+
1143
+ # MCP uses async confirmation pattern (return report -> client calls back with confirmed=True)
1144
+ # Create a "callback" that captures the scan result for MCP's async flow
1145
+ scan_result_holder = {"result": None}
1146
+
1147
+ def mcp_confirmation_callback(report: str, scan_id_from_scan: str) -> bool:
1148
+ # If client already confirmed, return True
1149
+ if confirmed:
1150
+ return True
1151
+ # Otherwise, store the result and return False to abort execution
1152
+ # MCP will then return the report to the client
1153
+ scan_result_holder["result"] = {"report": report, "scan_id": scan_id_from_scan}
1154
+ return False
1155
+
1156
+ # Execute using unified service
1157
+ result = service.execute_skill(
1158
+ skill_info=skill,
1159
+ input_data=input_data,
1160
+ confirmation_callback=mcp_confirmation_callback if not confirmed else lambda r, s: True,
1161
+ )
1162
+
1163
+ # Check if we need to return a security report (MCP async confirmation pattern)
1164
+ if scan_result_holder["result"] is not None:
1165
+ report_data = scan_result_holder["result"]
1166
+ return CallToolResult(
1167
+ content=[
1168
+ TextContent(
1169
+ type="text",
1170
+ text=(
1171
+ f"🔐 Security Review Required for skill '{skill_name}'\n\n"
1172
+ f"{report_data['report']}\n\n"
1173
+ f"⚠️ IMPORTANT: You MUST ask the user for confirmation before proceeding.\n"
1174
+ f"Show this security report to the user and wait for their explicit approval.\n\n"
1175
+ f"If the user approves, call run_skill again with:\n"
1176
+ f" - confirmed: true\n"
1177
+ f" - scan_id: \"{report_data['scan_id']}\"\n"
982
1178
  )
983
- ]
984
- )
985
- except Exception as e:
1179
+ )
1180
+ ]
1181
+ )
1182
+
1183
+ # Return execution result
1184
+ if result.success:
1185
+ return CallToolResult(
1186
+ content=[
1187
+ TextContent(
1188
+ type="text",
1189
+ text=f"Skill '{skill_name}' executed successfully.\n\nOutput:\n{result.output}"
1190
+ )
1191
+ ]
1192
+ )
1193
+ else:
986
1194
  return CallToolResult(
987
1195
  isError=True,
988
1196
  content=[
989
1197
  TextContent(
990
1198
  type="text",
991
- text=f"Error executing skill '{skill_name}': {str(e)}"
1199
+ text=f"Skill '{skill_name}' execution failed.\n\nError:\n{result.error}"
992
1200
  )
993
1201
  ]
994
1202
  )
skilllite/quick.py CHANGED
@@ -92,7 +92,8 @@ class SkillRunner:
92
92
  allow_network: Optional[bool] = None,
93
93
  enable_sandbox: Optional[bool] = None,
94
94
  execution_timeout: Optional[int] = None,
95
- max_memory_mb: Optional[int] = None
95
+ max_memory_mb: Optional[int] = None,
96
+ confirmation_callback: Optional[Callable[[str, str], bool]] = None
96
97
  ):
97
98
  """
98
99
  Initialize SkillRunner.
@@ -122,6 +123,9 @@ class SkillRunner:
122
123
  enable_sandbox: Whether to enable sandbox protection (defaults from .env or True)
123
124
  execution_timeout: Skill execution timeout in seconds (defaults from .env or 120)
124
125
  max_memory_mb: Maximum memory limit in MB (defaults from .env or 512)
126
+ confirmation_callback: Callback for security confirmation when sandbox_level=3.
127
+ Signature: (security_report: str, scan_id: str) -> bool
128
+ If None and sandbox_level=3, will use interactive terminal confirmation.
125
129
  """
126
130
  # Load .env
127
131
  load_env(env_file)
@@ -161,7 +165,11 @@ class SkillRunner:
161
165
 
162
166
  self.custom_tool_executor = custom_tool_executor
163
167
  self.use_enhanced_loop = use_enhanced_loop
164
-
168
+
169
+ # Security confirmation callback
170
+ # If None and sandbox_level=3, will use interactive terminal confirmation
171
+ self.confirmation_callback = confirmation_callback
172
+
165
173
  # Lazy initialization
166
174
  self._client = None
167
175
  self._manager = None
@@ -282,7 +290,8 @@ Example of CORRECT approach:
282
290
  model=self.model,
283
291
  max_iterations=self.max_iterations,
284
292
  custom_tools=self.custom_tools if self.custom_tools else None,
285
- custom_tool_executor=tool_executor
293
+ custom_tool_executor=tool_executor,
294
+ confirmation_callback=self.confirmation_callback
286
295
  )
287
296
  else:
288
297
  # Use basic AgenticLoop (backward compatible)
@@ -290,7 +299,8 @@ Example of CORRECT approach:
290
299
  client=self.client,
291
300
  model=self.model,
292
301
  system_prompt=self.system_context,
293
- max_iterations=self.max_iterations
302
+ max_iterations=self.max_iterations,
303
+ confirmation_callback=self.confirmation_callback
294
304
  )
295
305
 
296
306
  response = loop.run(user_message)