computer-use-ootb-internal 0.0.136__py3-none-any.whl → 0.0.138__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.
@@ -190,6 +190,11 @@ async def update_parameters(request: Request):
190
190
  status_code=400
191
191
  )
192
192
 
193
+ # Clear message histories before updating parameters
194
+ shared_state.message_queue.clear()
195
+ shared_state.chatbot_messages.clear()
196
+ logging.info("Cleared message queue and chatbot messages.")
197
+
193
198
  shared_state.args = argparse.Namespace(**data)
194
199
  shared_state.task_updated = True
195
200
 
@@ -301,13 +306,26 @@ async def stop_processing(request: Request):
301
306
  shared_state.should_stop = True
302
307
  shared_state.stop_event.set()
303
308
 
309
+ # Clear message histories
310
+ shared_state.message_queue.clear()
311
+ shared_state.chatbot_messages.clear()
312
+ logging.info("Cleared message queue and chatbot messages during stop.")
313
+
304
314
  # Send an immediate message to the queue to inform the user
305
315
  stop_initiated_msg = {"role": "assistant", "content": f"Stopping task '{shared_state.task}'...", "type": "text", "action_type": ""}
316
+ # Append the stop message AFTER clearing, so it's the only one left
317
+ shared_state.message_queue.append(stop_initiated_msg)
318
+ shared_state.chatbot_messages.append(stop_initiated_msg)
319
+
306
320
  return JSONResponse(
307
321
  content={"status": "success", "message": "Task is being stopped, server will remain available for new tasks"},
308
322
  status_code=200
309
323
  )
310
324
  else:
325
+ # Clear message histories even if not processing, to ensure clean state
326
+ shared_state.message_queue.clear()
327
+ shared_state.chatbot_messages.clear()
328
+ logging.info("Cleared message queue and chatbot messages (no active process to stop).")
311
329
  return JSONResponse(
312
330
  content={"status": "error", "message": "No active processing to stop"},
313
331
  status_code=400
@@ -12,7 +12,6 @@ def is_image_path(text):
12
12
  else:
13
13
  return False
14
14
 
15
-
16
15
  def encode_image(image_path):
17
16
  """Encode image file to base64."""
18
17
  with open(image_path, "rb") as image_file:
@@ -4,19 +4,7 @@ import logging
4
4
  import base64
5
5
  import requests
6
6
 
7
-
8
- def is_image_path(text):
9
- # Checking if the input text ends with typical image file extensions
10
- image_extensions = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif")
11
- if text.endswith(image_extensions):
12
- return True
13
- else:
14
- return False
15
-
16
- def encode_image(image_path):
17
- """Encode image file to base64."""
18
- with open(image_path, "rb") as image_file:
19
- return base64.b64encode(image_file.read()).decode("utf-8")
7
+ from .llm_utils import is_image_path, encode_image
20
8
 
21
9
 
22
10
 
@@ -4,19 +4,8 @@ import logging
4
4
  import base64
5
5
  from litellm import completion
6
6
 
7
+ from .llm_utils import is_image_path, encode_image
7
8
 
8
- def is_image_path(text):
9
- # Checking if the input text ends with typical image file extensions
10
- image_extensions = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif")
11
- if text.endswith(image_extensions):
12
- return True
13
- else:
14
- return False
15
-
16
- def encode_image(image_path):
17
- """Encode image file to base64."""
18
- with open(image_path, "rb") as image_file:
19
- return base64.b64encode(image_file.read()).decode("utf-8")
20
9
 
21
10
  def run_litellm(messages: list, system: str, llm: str, max_tokens=256, temperature=0):
22
11
  """
@@ -86,6 +86,39 @@ def receive_command():
86
86
 
87
87
  return jsonify({"message": f"Command {command_id} received and queued"}), 202 # Accepted
88
88
 
89
+ @flask_app.route('/internal/user_connected', methods=['POST'])
90
+ def user_connected_notification():
91
+ """Internal endpoint triggered by the scheduled task helper script."""
92
+ global _service_instance
93
+ if not _service_instance:
94
+ logging.error("Received user_connected signal but service instance is not set.")
95
+ return jsonify({"error": "Service not ready"}), 503
96
+
97
+ if not request.is_json:
98
+ logging.warning("Received non-JSON user_connected signal.")
99
+ return jsonify({"error": "Request must be JSON"}), 400
100
+
101
+ data = request.get_json()
102
+ username = data.get("username")
103
+ if not username:
104
+ logging.error("Received user_connected signal with no username.")
105
+ return jsonify({"error": "Missing 'username' in data"}), 400
106
+
107
+ logging.info(f"Received user_connected signal for user: {username}")
108
+
109
+ # Call the internal start logic directly (non-blocking? or blocking?)
110
+ # Let's make it non-blocking by putting it on the main command queue
111
+ # This avoids holding up the signal script and uses the existing processor.
112
+ internal_command = {
113
+ "action": "_internal_start_ootb",
114
+ "target_user": username
115
+ }
116
+ internal_command_id = f"internal_{username}_{time.time():.0f}"
117
+ _service_instance.command_queue.put((internal_command_id, internal_command))
118
+ logging.info(f"Queued internal start command {internal_command_id} for user {username}.")
119
+
120
+ return jsonify({"message": "Signal received and queued"}), 202
121
+
89
122
  # --- Helper Functions --- Only logging helpers needed adjustments
90
123
  # Move these inside the class later
91
124
  # def get_python_executable(): ...
@@ -193,7 +226,9 @@ class GuardService(win32serviceutil.ServiceFramework):
193
226
  self.log_info(f"Using target executable: {self.target_executable_path}")
194
227
 
195
228
  _service_instance = self
196
- self.log_info(f"Service initialized. Target executable set to: {self.target_executable_path}")
229
+ # Determine path to the signal script
230
+ self.signal_script_path = self._find_signal_script()
231
+ self.log_info(f"Service initialized. Target executable: {self.target_executable_path}. Signal script: {self.signal_script_path}")
197
232
 
198
233
  def SvcStop(self):
199
234
  self.log_info(f"Service stop requested.")
@@ -263,9 +298,10 @@ class GuardService(win32serviceutil.ServiceFramework):
263
298
  command_id, command = item
264
299
  action = command.get("action")
265
300
  target = command.get("target_user", "all_active")
266
- status = "failed_unknown" # Default
301
+ status = "failed_unknown" # Default status for external commands
302
+ is_internal = action == "_internal_start_ootb"
267
303
 
268
- self.log_info(f"Dequeued Command ID {command_id}: action='{action}', target='{target}'")
304
+ self.log_info(f"Dequeued {'Internal' if is_internal else 'External'} Command ID {command_id}: action='{action}', target='{target}'")
269
305
 
270
306
  try:
271
307
  if action == "update":
@@ -274,6 +310,11 @@ class GuardService(win32serviceutil.ServiceFramework):
274
310
  status = self.handle_stop(target)
275
311
  elif action == "start_ootb":
276
312
  status = self.handle_start(target)
313
+ elif action == "_internal_start_ootb":
314
+ # Call the core start logic but don't report status back externally
315
+ internal_status = self._trigger_start_for_user(target)
316
+ self.log_info(f"Internal start for {target} finished with status: {internal_status}")
317
+ # No external status reporting for internal commands
277
318
  else:
278
319
  self.log_error(f"Unknown action in queue: {action}")
279
320
  status = "failed_unknown_action"
@@ -281,7 +322,9 @@ class GuardService(win32serviceutil.ServiceFramework):
281
322
  self.log_error(f"Exception processing Command ID {command_id} ({action}): {handler_ex}", exc_info=True)
282
323
  status = "failed_exception"
283
324
  finally:
284
- self.report_command_status(command_id, status)
325
+ # Only report status for non-internal commands
326
+ if not is_internal:
327
+ self.report_command_status(command_id, status)
285
328
  self.command_queue.task_done()
286
329
 
287
330
  except queue.Empty:
@@ -320,25 +363,50 @@ class GuardService(win32serviceutil.ServiceFramework):
320
363
  # --- Command Handlers --- Now call self. for helpers
321
364
 
322
365
  def handle_update(self):
366
+ """Handles the update command by running the target executable with the 'update' argument."""
323
367
  self.log_info("Executing OOTB update...")
324
368
  if not self.target_executable_path:
325
- self.log_error("Cannot update: Target executable not found.")
369
+ self.log_error("Cannot update: Target executable path is not set.")
326
370
  return "failed_executable_not_found"
327
371
 
328
- update_command = f"{self.target_executable_path} update"
372
+ # Construct the command: "C:\path\to\exe" update
373
+ # self.target_executable_path should already be quoted if necessary
374
+ update_command = f'{self.target_executable_path} update'
375
+ # Determine the executable path without quotes for subprocess call if needed
376
+ executable_for_run = self.target_executable_path.strip('"')
377
+ args_for_run = ["update"]
378
+
329
379
  self.log_info(f"Running update command: {update_command}")
330
380
  try:
331
- result = subprocess.run(update_command, shell=True, capture_output=True, text=True, check=True, timeout=300, encoding='utf-8')
332
- self.log_info(f"Update successful: \nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
333
- return "success"
334
- except subprocess.CalledProcessError as e:
335
- self.log_error(f"Update failed (Exit Code {e.returncode}):\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}")
336
- return f"failed_exit_{e.returncode}"
337
- except subprocess.TimeoutExpired:
338
- self.log_error(f"Update command timed out.")
339
- return "failed_timeout"
381
+ # Execute the command directly. Running as LocalSystem should have rights to ProgramData.
382
+ # Capture output for logging.
383
+ result = subprocess.run(
384
+ [executable_for_run] + args_for_run,
385
+ capture_output=True,
386
+ text=True,
387
+ check=False, # Don't throw on non-zero exit, we check manually
388
+ encoding='utf-8',
389
+ errors='ignore'
390
+ )
391
+
392
+ # Log stdout/stderr regardless of exit code
393
+ if result.stdout:
394
+ self.log_info(f"Update process STDOUT:\n{result.stdout.strip()}")
395
+ if result.stderr:
396
+ self.log_warning(f"Update process STDERR:\n{result.stderr.strip()}") # Use warning for stderr
397
+
398
+ if result.returncode == 0:
399
+ self.log_info("Update process completed successfully (Exit Code 0).")
400
+ return "success"
401
+ else:
402
+ self.log_error(f"Update process failed (Exit Code {result.returncode}).")
403
+ return f"failed_update_exit_code_{result.returncode}"
404
+
405
+ except FileNotFoundError:
406
+ self.log_error(f"Update failed: Executable not found at '{executable_for_run}'.")
407
+ return "failed_executable_not_found"
340
408
  except Exception as e:
341
- self.log_error(f"Unexpected error during update: {e}", exc_info=True)
409
+ self.log_error(f"Update failed with exception: {e}", exc_info=True)
342
410
  return "failed_exception"
343
411
 
344
412
  def _get_ootb_processes(self, target_user="all_active"):
@@ -511,218 +579,210 @@ class GuardService(win32serviceutil.ServiceFramework):
511
579
 
512
580
 
513
581
  def handle_start(self, target_user="all_active"):
514
- self.log_info(f"Executing start OOTB for target '{target_user}'...")
515
- start_results = {} # Track results per user {username: (task_status, immediate_status)}
516
- failed_users = set()
582
+ """Handles external start command request (finds users, calls internal trigger)."""
583
+ self.log_info(f"External start requested for target '{target_user}'...")
584
+ # This function now primarily identifies target users and calls the internal trigger method.
585
+ # The status returned here reflects the process of identifying and triggering,
586
+ # not necessarily the final success/failure of the actual start (which happens async).
587
+
588
+ active_sessions = {} # user_lower: session_id
589
+ all_system_users = set() # user_lower
590
+ try:
591
+ # Use psutil for system user list, WTS for active sessions/IDs
592
+ for user_session in psutil.users():
593
+ username_lower = user_session.name.split('\\')[-1].lower()
594
+ all_system_users.add(username_lower)
595
+
596
+ sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
597
+ for session in sessions:
598
+ if session['State'] == win32ts.WTSActive:
599
+ try:
600
+ user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
601
+ if user:
602
+ active_sessions[user.lower()] = session['SessionId']
603
+ except Exception as query_err:
604
+ self.log_error(f"Could not query session {session['SessionId']}: {query_err}")
605
+ except Exception as user_enum_err:
606
+ self.log_error(f"Error enumerating users/sessions: {user_enum_err}", exc_info=True)
607
+ return "failed_user_enumeration"
608
+
609
+ target_users_normalized = set()
610
+ if target_user == "all_active":
611
+ target_users_normalized = set(active_sessions.keys())
612
+ self.log_info(f"Targeting all active users for start: {target_users_normalized}")
613
+ else:
614
+ normalized_target = target_user.lower()
615
+ target_users_normalized.add(normalized_target)
616
+ self.log_info(f"Targeting specific user for start: {normalized_target}")
617
+
618
+ if not target_users_normalized:
619
+ self.log_info("No target users identified for start.")
620
+ return "failed_no_target_users"
621
+
622
+ trigger_results = {}
623
+ for user in target_users_normalized:
624
+ self.log_info(f"Calling internal start trigger for user: {user}")
625
+ # Call the core logic directly (this is now synchronous within the handler)
626
+ # Or queue it? Queuing might be better to avoid blocking the handler if many users.
627
+ # Let's stick to the queue approach from the internal endpoint:
628
+ internal_command = {
629
+ "action": "_internal_start_ootb",
630
+ "target_user": user
631
+ }
632
+ internal_command_id = f"external_{user}_{time.time():.0f}"
633
+ self.command_queue.put((internal_command_id, internal_command))
634
+ trigger_results[user] = "queued"
635
+
636
+ self.log_info(f"Finished queuing start triggers. Results: {trigger_results}")
637
+ # The status here just means we successfully queued the actions
638
+ # Actual success/failure happens in the command processor later.
639
+ # We might need a different way to report overall status if needed.
640
+ return f"success_queued::{json.dumps(trigger_results)}"
641
+
642
+
643
+ def _trigger_start_for_user(self, username):
644
+ """Core logic to start OOTB for a single user. Called internally."""
645
+ user = username.lower() # Ensure lowercase
646
+ self.log_info(f"Internal trigger: Starting OOTB check for user '{user}'...")
647
+ task_created_status = "task_unknown"
648
+ immediate_start_status = "start_not_attempted"
649
+ final_status = "failed_unknown"
517
650
 
518
651
  try:
519
- # --- Get target users and active sessions ---
520
- active_sessions = {} # user_lower: session_id
521
- all_system_users = set() # user_lower
652
+ # 1. Ensure scheduled task exists (still useful fallback/persistence)
653
+ try:
654
+ task_created = self.create_or_update_logon_task(user)
655
+ task_created_status = "task_success" if task_created else "task_failed"
656
+ except Exception as task_err:
657
+ self.log_error(f"Internal trigger: Exception creating/updating task for {user}: {task_err}", exc_info=True)
658
+ task_created_status = "task_exception"
659
+ # Don't necessarily fail the whole operation yet
660
+
661
+ # 2. Check if user is active
662
+ active_sessions = {} # Re-check active sessions specifically for this user
663
+ session_id = None
664
+ token = None
665
+ is_active = False
522
666
  try:
523
- # Use psutil for system user list, WTS for active sessions/IDs
524
- for user_session in psutil.users():
525
- username_lower = user_session.name.split('\\')[-1].lower()
526
- all_system_users.add(username_lower)
527
-
528
667
  sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
529
668
  for session in sessions:
530
669
  if session['State'] == win32ts.WTSActive:
531
670
  try:
532
- user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
533
- if user:
534
- active_sessions[user.lower()] = session['SessionId']
535
- except Exception as query_err:
536
- self.log_error(f"Could not query session {session['SessionId']}: {query_err}")
537
- except Exception as user_enum_err:
538
- self.log_error(f"Error enumerating users/sessions: {user_enum_err}", exc_info=True)
539
- return "failed_user_enumeration"
540
-
541
- self.log_info(f"Found active user sessions: {active_sessions}")
542
-
543
- target_users_normalized = set()
544
- if target_user == "all_active":
545
- # If targeting all_active, only target those CURRENTLY active
546
- target_users_normalized = set(active_sessions.keys())
547
- self.log_info(f"Targeting all active users: {target_users_normalized}")
548
- else:
549
- normalized_target = target_user.lower()
550
- # Check if the target user actually exists on the system, even if inactive
551
- # This check might be complex/unreliable. Rely on task scheduler potentially failing?
552
- # Let's assume admin provides a valid username for specific targeting.
553
- # if normalized_target in all_system_users: # Removing this check, assume valid user input
554
- target_users_normalized.add(normalized_target)
555
- self.log_info(f"Targeting specific user: {normalized_target}")
556
- # else:
557
- # log_error(f"Target user '{target_user}' does not appear to exist on this system based on psutil.")
558
- # return "failed_user_does_not_exist"
559
-
560
- if not target_users_normalized:
561
- self.log_info("No target users identified (or none active for 'all_active').")
562
- # If target was specific user but they weren't found active, still try task?
563
- # Let's proceed to task creation anyway for specific user case.
564
- if target_user != "all_active": target_users_normalized.add(target_user.lower())
565
- if not target_users_normalized:
566
- return "failed_no_target_users"
671
+ current_user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
672
+ if current_user and current_user.lower() == user:
673
+ session_id = session['SessionId']
674
+ is_active = True
675
+ self.log_info(f"Internal trigger: User '{user}' is active in session {session_id}.")
676
+ break
677
+ except Exception: pass # Ignore errors querying other sessions
678
+ except Exception as e:
679
+ self.log_error(f"Internal trigger: Error checking active sessions for {user}: {e}")
680
+ # Continue, assume inactive if check failed?
567
681
 
568
- # --- Check existing processes ---
569
- # This check is only relevant for immediate start attempt
570
- running_procs_by_user = {} # user_lower: count
682
+ if not is_active:
683
+ self.log_info(f"Internal trigger: User '{user}' is not active. Skipping immediate start.")
684
+ immediate_start_status = "start_skipped_inactive"
685
+ final_status = task_created_status # Status depends only on task creation
686
+ return final_status # Exit early if inactive
687
+
688
+ # 3. Check if already running for this active user
689
+ is_running = False
571
690
  try:
572
- current_running = self._get_ootb_processes("all_active") # Check all
573
- for proc in current_running:
574
- try:
575
- proc_username = proc.info.get('username')
576
- if proc_username:
577
- user_lower = proc_username.split('\\')[-1].lower()
578
- running_procs_by_user[user_lower] = running_procs_by_user.get(user_lower, 0) + 1
579
- except Exception: pass
691
+ running_procs = self._get_ootb_processes(user)
692
+ if running_procs:
693
+ is_running = True
694
+ self.log_info(f"Internal trigger: OOTB already running for active user '{user}'. Skipping immediate start.")
695
+ immediate_start_status = "start_skipped_already_running"
696
+ final_status = "success_already_running" # Considered success
697
+ return final_status # Exit early if already running
580
698
  except Exception as e:
581
- self.log_error(f"Error checking existing processes: {e}")
582
- self.log_info(f"Users currently running OOTB: {running_procs_by_user}")
583
-
584
- # --- Process each target user ---
585
- for user in target_users_normalized:
586
- task_created_status = "task_unknown"
587
- immediate_start_status = "start_not_attempted"
588
- token = None # Ensure token is reset/defined
589
-
590
- self.log_info(f"Processing start for user '{user}'...")
591
-
592
- # 1. Always try to create/update the scheduled task
593
- try:
594
- task_created = self.create_or_update_logon_task(user)
595
- task_created_status = "task_success" if task_created else "task_failed"
596
- except Exception as task_err:
597
- self.log_error(f"Exception creating/updating scheduled task for {user}: {task_err}", exc_info=True)
598
- task_created_status = "task_exception"
599
- failed_users.add(user)
600
- # Continue to potentially try immediate start IF user is active?
601
- # Or maybe skip if task creation failed badly?
602
- # Let's skip immediate start if task creation had exception.
603
- start_results[user] = (task_created_status, immediate_start_status)
604
- continue
605
-
606
- # 2. If user is active AND not already running, try immediate start
607
- is_active = user in active_sessions
608
- is_running = running_procs_by_user.get(user, 0) > 0
609
-
610
- if is_active:
611
- if not is_running:
612
- immediate_start_status = "start_attempted"
613
- self.log_info(f"User '{user}' is active and not running OOTB. Attempting immediate start...")
614
- try:
615
- session_id = active_sessions[user]
616
- token = win32ts.WTSQueryUserToken(session_id)
617
- env = win32profile.CreateEnvironmentBlock(token, False) # <-- Restore creating env block
618
- startup = win32process.STARTUPINFO()
619
- creation_flags = 0x00000010 # CREATE_NEW_CONSOLE
620
-
621
- # --- Launch via cmd /K ---
622
- lpApplicationName = None
623
- lpCommandLine = f'cmd.exe /K "{self.target_executable_path}"'
624
- cwd = os.path.dirname(self.target_executable_path.strip('"')) if os.path.dirname(self.target_executable_path.strip('"')) != '' else None
625
- # --- End Launch via cmd /K ---
626
-
627
- self.log_info(f"Calling CreateProcessAsUser to launch via cmd /K (Restored Environment Block):") # Updated log note
628
- self.log_info(f" lpApplicationName: {lpApplicationName}")
629
- self.log_info(f" lpCommandLine: {lpCommandLine}")
630
- self.log_info(f" lpCurrentDirectory: {cwd if cwd else 'Default'}")
631
- self.log_info(f" dwCreationFlags: {creation_flags} (CREATE_NEW_CONSOLE)")
632
-
633
- # CreateProcessAsUser call with restored env
634
- hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
635
- token, # User token
636
- lpApplicationName, # Application name (None)
637
- lpCommandLine, # Command line (cmd.exe /K "...")
638
- None, # Process attributes
639
- None, # Thread attributes
640
- False, # Inherit handles
641
- creation_flags, # Creation flags (CREATE_NEW_CONSOLE)
642
- env, # Environment block (Restored)
643
- cwd, # Current directory for cmd.exe
644
- startup # Startup info
645
- )
646
-
647
- self.log_info(f"CreateProcessAsUser call succeeded for user '{user}' (PID: {dwPid}). Checking existence...")
648
- win32api.CloseHandle(hProcess)
649
- win32api.CloseHandle(hThread)
650
-
651
- time.sleep(1)
652
- if psutil.pid_exists(dwPid):
653
- self.log_info(f"Immediate start succeeded for user '{user}' (PID {dwPid}).")
654
- immediate_start_status = "start_success"
655
- else:
656
- self.log_error(f"Immediate start failed for user '{user}': Process {dwPid} exited immediately.")
657
- immediate_start_status = "start_failed_exited"
658
- failed_users.add(user)
659
-
660
- except Exception as proc_err:
661
- self.log_error(f"Exception during immediate start for user '{user}': {proc_err}", exc_info=True)
662
- immediate_start_status = "start_failed_exception"
663
- failed_users.add(user)
664
- finally:
665
- if token:
666
- try: win32api.CloseHandle(token)
667
- except: pass
668
- else: # User is active but already running
669
- self.log_info(f"User '{user}' is active but OOTB is already running. Skipping immediate start.")
670
- immediate_start_status = "start_skipped_already_running"
671
- else: # User is not active
672
- self.log_info(f"User '{user}' is not active. Skipping immediate start (task created/updated).")
673
- immediate_start_status = "start_skipped_inactive"
674
-
675
- # Record final results for this user
676
- start_results[user] = (task_created_status, immediate_start_status)
699
+ self.log_error(f"Internal trigger: Error checking existing processes for {user}: {e}")
700
+ # Continue and attempt start despite error?
677
701
 
678
-
679
- # --- Consolidate status ---
680
- total_processed = len(target_users_normalized)
681
- final_status = "partial_success" if failed_users else "success"
682
- if not start_results: final_status = "no_targets_processed"
683
- # If all processed users failed in some way (either task or start)
684
- if len(failed_users) == total_processed and total_processed > 0: final_status = "failed"
685
- # Special case: target was specific user who wasn't found active
686
- elif total_processed == 1 and target_user != "all_active" and target_user.lower() not in active_sessions:
687
- user_key = target_user.lower()
688
- if user_key in start_results and start_results[user_key][0] == "task_success":
689
- final_status = "success_task_only_user_inactive"
702
+ # 4. Attempt immediate start (User is active and not running)
703
+ immediate_start_status = "start_attempted"
704
+ self.log_info(f"Internal trigger: User '{user}' is active and not running. Attempting immediate start via CreateProcessAsUser...")
705
+ try:
706
+ token = win32ts.WTSQueryUserToken(session_id)
707
+ env = win32profile.CreateEnvironmentBlock(token, False)
708
+ startup = win32process.STARTUPINFO()
709
+ creation_flags = 0x00000010 # CREATE_NEW_CONSOLE
710
+ lpApplicationName = None
711
+ lpCommandLine = f'cmd.exe /K "{self.target_executable_path}"'
712
+ cwd = os.path.dirname(self.target_executable_path.strip('"')) if os.path.dirname(self.target_executable_path.strip('"')) != '' else None
713
+
714
+ # Log details before call
715
+ self.log_info(f"Internal trigger: Calling CreateProcessAsUser:")
716
+ self.log_info(f" lpCommandLine: {lpCommandLine}")
717
+ self.log_info(f" lpCurrentDirectory: {cwd if cwd else 'Default'}")
718
+
719
+ hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
720
+ token, lpApplicationName, lpCommandLine, None, None, False,
721
+ creation_flags, env, cwd, startup
722
+ )
723
+ self.log_info(f"Internal trigger: CreateProcessAsUser call succeeded for user '{user}' (PID: {dwPid}). Checking existence...")
724
+ win32api.CloseHandle(hProcess)
725
+ win32api.CloseHandle(hThread)
726
+
727
+ time.sleep(1)
728
+ if psutil.pid_exists(dwPid):
729
+ self.log_info(f"Internal trigger: Immediate start succeeded for user '{user}' (PID {dwPid}).")
730
+ immediate_start_status = "start_success"
731
+ final_status = "success" # Overall success
690
732
  else:
691
- final_status = "failed_task_user_inactive"
692
-
693
- self.log_info(f"Finished starting OOTB. Overall Status: {final_status}. Results: {start_results}")
694
- # Return detailed results as a JSON string for easier parsing/logging server-side
695
- try:
696
- details = json.dumps(start_results)
697
- except Exception:
698
- details = str(start_results) # Fallback
699
- return f"{final_status}::{details}"
733
+ self.log_error(f"Internal trigger: Immediate start failed for user '{user}': Process {dwPid} exited immediately.")
734
+ immediate_start_status = "start_failed_exited"
735
+ final_status = "failed_start_exited"
736
+
737
+ except Exception as proc_err:
738
+ self.log_error(f"Internal trigger: Exception during CreateProcessAsUser for user '{user}': {proc_err}", exc_info=True)
739
+ immediate_start_status = "start_failed_exception"
740
+ final_status = "failed_start_exception"
741
+ finally:
742
+ if token: win32api.CloseHandle(token)
743
+
744
+ # Combine results (though mostly determined by start attempt now)
745
+ # Example: final_status = f"{task_created_status}_{immediate_start_status}"
746
+ return final_status
700
747
 
701
748
  except Exception as e:
702
- self.log_error(f"Error during combined start OOTB process: {e}", exc_info=True)
703
- return "failed_exception"
749
+ self.log_error(f"Internal trigger: Unexpected error processing start for {username}: {e}", exc_info=True)
750
+ return "failed_trigger_exception"
751
+
704
752
 
705
753
  def create_or_update_logon_task(self, username):
706
- """Creates/updates task to run OOTB app via cmd /K on session connect/reconnect."""
707
- if not self.target_executable_path:
708
- self.log_error(f"Cannot create task for {username}: Target executable path is not set.")
754
+ """Creates/updates task to trigger the internal signal script on session connect."""
755
+ if not self.signal_script_path:
756
+ self.log_error(f"Cannot create task for {username}: Signal script path is not set.")
709
757
  return False
758
+ if not sys.executable:
759
+ self.log_error(f"Cannot create task for {username}: sys.executable is not found.")
760
+ return False
761
+
762
+ # Use the python executable that the service itself is running under
763
+ python_exe = sys.executable
764
+ if ' ' in python_exe and not python_exe.startswith('"'):
765
+ python_exe = f'"{python_exe}"'
710
766
 
711
767
  task_name = f"OOTB_UserConnect_{username}"
712
- # Action: Revert to running the actual executable via cmd /K
713
- action_executable = 'cmd.exe'
714
- action_arguments = f'/K "{self.target_executable_path}"'
715
- safe_action_executable = action_executable.replace("'", "''")
716
- safe_action_arguments = action_arguments.replace("'", "''")
768
+ # Action: Run python.exe with the signal script and username argument
769
+ action_executable = python_exe
770
+ # Ensure script path is quoted if needed
771
+ script_arg = self.signal_script_path # Should be quoted already by _find_signal_script
772
+ # Username might need quoting if it contains spaces, though unlikely
773
+ user_arg = username # Keep simple for now
774
+ action_arguments = f'{script_arg} "{user_arg}"' # Pass username as quoted arg
775
+ safe_action_executable = action_executable.replace("'", "''") # Escape for PS
776
+ safe_action_arguments = action_arguments.replace("'", "''") # Escape for PS
717
777
 
718
- # Explicitly set the working directory to the executable's location
778
+ # Working directory for the script (likely its own directory)
719
779
  try:
720
- executable_dir = os.path.dirname(self.target_executable_path.strip('"'))
721
- if not executable_dir: executable_dir = "."
722
- safe_working_directory = executable_dir.replace("'", "''")
780
+ script_dir = os.path.dirname(self.signal_script_path.strip('"'))
781
+ if not script_dir: script_dir = "."
782
+ safe_working_directory = script_dir.replace("'", "''")
723
783
  working_directory_setting = f"$action.WorkingDirectory = '{safe_working_directory}'"
724
784
  except Exception as e:
725
- self.log_error(f"Error determining working directory for task: {e}. WD will not be set.")
785
+ self.log_error(f"Error determining working directory for signal script task: {e}. WD will not be set.")
726
786
  working_directory_setting = "# Could not set WorkingDirectory"
727
787
 
728
788
  # PowerShell command construction
@@ -730,33 +790,29 @@ class GuardService(win32serviceutil.ServiceFramework):
730
790
  $taskName = "{task_name}"
731
791
  $principal = New-ScheduledTaskPrincipal -UserId "{username}" -LogonType Interactive
732
792
 
733
- # Action: Run the OOTB executable via cmd /K
793
+ # Action: Run python signal script
734
794
  $action = New-ScheduledTaskAction -Execute '{safe_action_executable}' -Argument '{safe_action_arguments}'
735
- {working_directory_setting} # Set the working directory
795
+ {working_directory_setting}
736
796
 
737
- # Triggers: On session connect (21) AND reconnect (25)
797
+ # Trigger: On session connect (Event ID 21)
738
798
  $logName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
739
799
  $source = 'Microsoft-Windows-TerminalServices-LocalSessionManager'
740
- # Define multiple triggers
741
- $trigger1 = New-ScheduledTaskTrigger -Event -LogName $logName -Source $source -EventId 21
742
- $trigger2 = New-ScheduledTaskTrigger -Event -LogName $logName -Source $source -EventId 25
743
- # Optional Delay - Apply to both triggers if desired?
744
- # $trigger1.Delay = 'PT15S'
745
- # $trigger2.Delay = 'PT15S'
800
+ $eventIDs = @(21, 25)
801
+ $trigger = New-ScheduledTaskTrigger -Event -LogName $logName -Source $source -EventId $eventIDs[0]
802
+ # Optional Delay: -Delay 'PT15S'
803
+ # $trigger.Delay = 'PT15S'
746
804
 
747
805
  $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Days 9999) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
748
- $description = "Runs OOTB Application (via cmd) for user {username} upon session connect/reconnect." # Updated description
806
+ $description = "Triggers OOTB Guard Service for user {username} upon session connect via internal signal." # Updated description
749
807
 
750
- # Unregister existing task first (force) - Use the NEW task name
808
+ # Unregister existing task first (force)
751
809
  Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
752
810
 
753
- # Register the new task with MULTIPLE triggers
754
- Register-ScheduledTask -TaskName $taskName -Principal $principal -Action $action -Trigger $trigger1, $trigger2 -Settings $settings -Description $description -Force
755
-
811
+ # Register the new task
812
+ Register-ScheduledTask -TaskName $taskName -Principal $principal -Action $action -Trigger $trigger -Settings $settings -Description $description -Force
756
813
  """
757
- self.log_info(f"Attempting to create/update task '{task_name}' for user '{username}' to run OOTB on session connect/reconnect.")
814
+ self.log_info(f"Attempting to create/update task '{task_name}' for user '{username}' to run signal script.")
758
815
  try:
759
- # Need to actually run the powershell command here!
760
816
  success = self.run_powershell_command(ps_command)
761
817
  if success:
762
818
  self.log_info(f"Successfully ran PowerShell command to create/update task '{task_name}'.")
@@ -805,6 +861,24 @@ class GuardService(win32serviceutil.ServiceFramework):
805
861
  self.log_info(f"Attempted removal of scheduled task '{task_name}' for user '{username}'.")
806
862
  return True
807
863
 
864
+ def _find_signal_script(self):
865
+ """Finds the signal_connection.py script relative to this service file."""
866
+ try:
867
+ base_dir = os.path.dirname(os.path.abspath(__file__))
868
+ script_path = os.path.join(base_dir, "signal_connection.py")
869
+ if os.path.exists(script_path):
870
+ self.log_info(f"Found signal script at: {script_path}")
871
+ # Quote if needed?
872
+ if " " in script_path and not script_path.startswith('"'):
873
+ return f'"{script_path}"'
874
+ return script_path
875
+ else:
876
+ self.log_error(f"Signal script signal_connection.py not found near {base_dir}")
877
+ return None
878
+ except Exception as e:
879
+ self.log_error(f"Error finding signal script: {e}")
880
+ return None
881
+
808
882
  # --- Main Execution Block ---
809
883
  if __name__ == '__main__':
810
884
  if len(sys.argv) > 1 and sys.argv[1] == 'debug':
@@ -1,7 +1,6 @@
1
1
  # src/computer_use_ootb_internal/preparation/star_rail_prepare.py
2
2
  import time
3
3
  import platform
4
- import subprocess # Added for taskkill
5
4
  import pyautogui
6
5
  import webbrowser
7
6
  import logging # Use logging instead of print for better practice
@@ -12,8 +11,7 @@ log = logging.getLogger(__name__)
12
11
  def run_preparation(state):
13
12
  """
14
13
  Performs environment preparation specific to Star Rail on Windows.
15
- Closes existing Edge browsers, opens the specified URL in a new Edge instance,
16
- and performs initial clicks.
14
+ Opens the specified URL in Edge and performs initial clicks.
17
15
  """
18
16
  if platform.system() != "Windows":
19
17
  log.info("Star Rail preparation skipped: Not running on Windows.")
@@ -23,31 +21,11 @@ def run_preparation(state):
23
21
  url = "https://sr.mihoyo.com/cloud/#/" # Consider making this configurable later
24
22
  browser_opened = False
25
23
  try:
26
- # Attempt to close existing Microsoft Edge processes
27
- log.info("Attempting to close existing Microsoft Edge processes...")
28
- try:
29
- # /F forces termination, /IM specifies image name
30
- result = subprocess.run(['taskkill', '/F', '/IM', 'msedge.exe'],
31
- capture_output=True, text=True, check=False)
32
- if result.returncode == 0:
33
- log.info("Successfully sent termination signal to msedge.exe processes.")
34
- elif "not found" in result.stderr.lower() or "not found" in result.stdout.lower():
35
- log.info("No running msedge.exe processes found to close.")
36
- else:
37
- log.warning(f"taskkill command finished with return code {result.returncode}. Output: {result.stdout} Stderr: {result.stderr}")
38
- time.sleep(2) # Give processes time to close
39
- except FileNotFoundError:
40
- log.error("Error: 'taskkill' command not found. Make sure it's in the system PATH.")
41
- except Exception as e:
42
- log.error(f"Error occurred while trying to close Edge: {e}", exc_info=True)
43
-
44
24
  # Use only webbrowser.open
45
25
  log.info(f"Attempting to open {url} using webbrowser.open()...")
46
26
  if webbrowser.open(url):
47
27
  log.info(f"Successfully requested browser to open {url} via webbrowser.open().")
48
28
  browser_opened = True
49
- # Keep original sleep time for browser load before clicks
50
- time.sleep(5)
51
29
  else:
52
30
  log.warning("webbrowser.open() returned False, indicating potential failure.")
53
31
 
@@ -3,6 +3,7 @@ import time
3
3
  import json
4
4
  import platform
5
5
  import uuid
6
+ import base64
6
7
  import datetime
7
8
  from datetime import datetime, timedelta, timezone
8
9
 
@@ -74,7 +75,10 @@ def simple_teachmode_sampling_loop(
74
75
  # yield {"role": "assistant", "content": "screenshot", "type": "action", "action_type": "screenshot"}
75
76
 
76
77
  if is_image_path(sc_path):
77
- yield {"role": "assistant", "content": sc_path, "type": "image", "action_type": "screenshot"}
78
+ # yield {"role": "assistant", "content": sc_path, "type": "image", "action_type": "screenshot"}
79
+ with open(sc_path, "rb") as image_file:
80
+ sc_base64 = base64.b64encode(image_file.read()).decode('utf-8')
81
+ yield {"role": "assistant", "content": sc_base64, "type": "image_base64", "action_type": "screenshot"}
78
82
 
79
83
  payload = {
80
84
  "task_id": unique_task_id,
@@ -0,0 +1,48 @@
1
+ # Helper script to signal guard service about user connection
2
+ # Save as e.g., ootb_lite_pypi/ootb_lite_pypi/src/computer_use_ootb_internal/signal_connection.py
3
+ import sys
4
+ import requests
5
+ import logging
6
+ import os
7
+ import pathlib
8
+
9
+ # Basic logging for the script itself
10
+ LOG_DIR = pathlib.Path(os.environ.get('PROGRAMDATA', 'C:/ProgramData')) / "OOTBGuardService" / "SignalLogs"
11
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
12
+ LOG_FILE = LOG_DIR / "signal_connection.log"
13
+
14
+ logging.basicConfig(
15
+ filename=LOG_FILE,
16
+ level=logging.INFO,
17
+ format='%(asctime)s %(levelname)s: %(message)s'
18
+ )
19
+
20
+ GUARD_SERVICE_URL = "http://localhost:14000/internal/user_connected"
21
+
22
+ if __name__ == "__main__":
23
+ if len(sys.argv) < 2:
24
+ logging.error("Username argument missing.")
25
+ sys.exit(1)
26
+
27
+ username = sys.argv[1]
28
+ logging.info(f"Signaling connection for user: {username}")
29
+
30
+ payload = {"username": username}
31
+
32
+ try:
33
+ response = requests.post(GUARD_SERVICE_URL, json=payload, timeout=10)
34
+ response.raise_for_status()
35
+ logging.info(f"Successfully signaled connection for user {username}. Status: {response.status_code}")
36
+ sys.exit(0)
37
+ except requests.exceptions.ConnectionError:
38
+ logging.error(f"Connection refused when trying to signal for user {username}. Guard service might not be running or accessible.")
39
+ sys.exit(2)
40
+ except requests.exceptions.Timeout:
41
+ logging.error(f"Timeout when trying to signal for user {username}.")
42
+ sys.exit(3)
43
+ except requests.exceptions.RequestException as e:
44
+ logging.error(f"Error signaling connection for user {username}: {e}")
45
+ sys.exit(4)
46
+ except Exception as e:
47
+ logging.error(f"Unexpected error for user {username}: {e}")
48
+ sys.exit(5)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computer-use-ootb-internal
3
- Version: 0.0.136
3
+ Version: 0.0.138
4
4
  Summary: Computer Use OOTB
5
5
  Author-email: Siyuan Hu <siyuan.hu.sg@gmail.com>
6
6
  Requires-Python: >=3.11
@@ -1,11 +1,12 @@
1
1
  computer_use_ootb_internal/README.md,sha256=FxpW95lyub2iX73ZDfK6ML7SdEKg060H5I6Grub7li4,31
2
2
  computer_use_ootb_internal/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
3
- computer_use_ootb_internal/app_teachmode.py,sha256=ZZW98870QPNK76LN4f4k9RjBELYcF_3hl0w0fvxj3dY,22268
3
+ computer_use_ootb_internal/app_teachmode.py,sha256=76JbhRhhB-PohVJ-PJSpqMPMqvLGH8QwKTZnPRXYO3w,23176
4
4
  computer_use_ootb_internal/dependency_check.py,sha256=y8RMEP6RXQzTgU1MS_1piBLtz4J-Hfn9RjUZg59dyvo,1333
5
- computer_use_ootb_internal/guard_service.py,sha256=T0j18PIbeHMTrV4myQX8DRobrL5ysi3HGraCAGrUykw,44652
5
+ computer_use_ootb_internal/guard_service.py,sha256=RjnfBIsLU0P4T1o2lMWFY1lSmAOILhJmhUiYzrTxg4A,47329
6
6
  computer_use_ootb_internal/requirements-lite.txt,sha256=5DAHomz4A_P2BmTIXNkNqkHbnIF0AyZ4_1XAlb1LaYs,290
7
- computer_use_ootb_internal/run_teachmode_ootb_args.py,sha256=HB7L9vuIPUXVqeTicyNJKArzTNAfgdpsyO3JEzmByIo,7315
7
+ computer_use_ootb_internal/run_teachmode_ootb_args.py,sha256=3ud1hvvOkODD3woc0Zwe5UG5p8x2KEHLks7JfdFRzCg,7579
8
8
  computer_use_ootb_internal/service_manager.py,sha256=SD8jzfn0VVXBOr_nP6zmBWSC2TzrU_sp2e5JJkSlQFU,9734
9
+ computer_use_ootb_internal/signal_connection.py,sha256=8jPLOj0WSBXI_NElm5A-F146DTAE5LdL07lov7YXTNQ,1774
9
10
  computer_use_ootb_internal/computer_use_demo/animation/click_animation.py,sha256=tP1gsayFy-CKk10UlrE9RlexwlHWiHQUe5Ogg4vQvSg,3234
10
11
  computer_use_ootb_internal/computer_use_demo/animation/icons8-select-cursor-transparent-96.gif,sha256=4LfwsfFQnREXrNRs32aJU2jO65JXianJoL_8q7-8elg,30966
11
12
  computer_use_ootb_internal/computer_use_demo/animation/test_animation.py,sha256=2R1u98OLKYalSZ5nt5vvyZ71FL5R5vLv-n8zM8jVdV8,1183
@@ -16,9 +17,9 @@ computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/simple_parser/
16
17
  computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/uia_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
18
  computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/uia_tools/screenshot_cli.py,sha256=hrUBH3aeUe9t4EDsMhaUZLPpNLKeEaG4ZvikrFy-bOU,1313
18
19
  computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/uia_tools/screenshot_service.py,sha256=fJ8G4_-ZLGsHLdLgHCNgjVM1osbfz21L2w8QrcvnuhA,2738
19
- computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/llm_utils.py,sha256=TtsB1sf8xrolt-fHyaJvRW7vY1gstlo9qqwOIG5wWww,3985
20
- computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/oai.py,sha256=zWAHa37fhIWCYpIEvQsIe5prfBJQ9yZFQwAi_dm8baE,4158
21
- computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/run_litellm.py,sha256=SBnWMhwZoCFji9-ii7VSfPYAhDKUWvlJgJzw9-Ei3-g,7750
20
+ computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/llm_utils.py,sha256=ySvoB-ZndjKF-189ZUnRIOK5_hWYQDfc4dGgCjyuFOI,3984
21
+ computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/oai.py,sha256=ezsUlGiNGKh1qjcEw3sf8wxvlJJopKbcqFuMfb5aTGo,3753
22
+ computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/run_litellm.py,sha256=lKvobmbZ5mLsAxwUDdQ-RKQ1J9yiWu581tMbrO5QFOA,7346
22
23
  computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/run_llm.py,sha256=fxC-7lg8TLAi9f69zs5y5Pwga8Y5mY7Yfc5NNZNRJgM,1558
23
24
  computer_use_ootb_internal/computer_use_demo/tools/__init__.py,sha256=Pj8_5L4_PPQK298X4NV3KMbP-84t-bM0pbjEeb5_SJQ,343
24
25
  computer_use_ootb_internal/computer_use_demo/tools/aws_request.py,sha256=12UVzeA2PmpZhpy2Pt5Vh48-_q1e8ZjVQux6r3pbAcw,2629
@@ -32,8 +33,8 @@ computer_use_ootb_internal/computer_use_demo/tools/edit.py,sha256=b0PwUitxckHCQq
32
33
  computer_use_ootb_internal/computer_use_demo/tools/run.py,sha256=xhXdnBK1di9muaO44CEirL9hpGy3NmKbjfMpyeVmn8Y,1595
33
34
  computer_use_ootb_internal/computer_use_demo/tools/screen_capture.py,sha256=L8qfvtUkPPQGt92N-2Zfw5ZTDBzLsDps39uMnX3_uSA,6857
34
35
  computer_use_ootb_internal/preparation/__init__.py,sha256=AgtGHcBpiTkxJjF0xwcs3yyQ6SyUvhL3G0vD2XO-zJw,63
35
- computer_use_ootb_internal/preparation/star_rail_prepare.py,sha256=Q6P9WuhtK6gNfYcHQqRGBY0XfD6vKr0f7oGknZ4Ac6I,3979
36
- computer_use_ootb_internal-0.0.136.dist-info/METADATA,sha256=BcPCfVBrZVzGvuhk6-v8JWlgI2ojMApI8OXWUDkBN44,1048
37
- computer_use_ootb_internal-0.0.136.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
- computer_use_ootb_internal-0.0.136.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
39
- computer_use_ootb_internal-0.0.136.dist-info/RECORD,,
36
+ computer_use_ootb_internal/preparation/star_rail_prepare.py,sha256=s1VWszcTnJAKxqCHFlaOEwPkqVSrkiFx_yKpWSnSbHs,2649
37
+ computer_use_ootb_internal-0.0.138.dist-info/METADATA,sha256=ssU5RYeRr30uWirpkatg1lBgY0ANtqnIQ3KL_dtM0t4,1048
38
+ computer_use_ootb_internal-0.0.138.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ computer_use_ootb_internal-0.0.138.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
40
+ computer_use_ootb_internal-0.0.138.dist-info/RECORD,,