@tencent-ai/agent-sdk 0.3.206 → 0.3.209

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.
Files changed (38) hide show
  1. package/cli/CHANGELOG.md +53 -0
  2. package/cli/builtin/workflows/deep-research.workflow.js +77 -20
  3. package/cli/dist/codebuddy-headless.js +293 -245
  4. package/cli/dist/web-ui/assets/index-CH6e4vvI.css +32 -0
  5. package/cli/dist/web-ui/assets/index-GcU4pZms.js +1102 -0
  6. package/cli/dist/web-ui/index.html +2 -2
  7. package/cli/dist/web-ui/sw.js +1 -1
  8. package/cli/package.json +4 -4
  9. package/cli/product.cloudhosted.json +2 -2
  10. package/cli/product.internal.json +2 -2
  11. package/cli/product.ioa.json +2 -2
  12. package/cli/product.json +3 -3
  13. package/cli/product.selfhosted.json +2 -2
  14. package/cli/vendor/native-builds.json +18 -0
  15. package/cli/vendor/shim/broker-ipc-client.cjs +158 -0
  16. package/cli/vendor/shim/brokered-bin/codebuddy-toybox-dispatch +381 -0
  17. package/cli/vendor/shim/brokered-sandbox-bash-env.sh +10 -0
  18. package/cli/vendor/shim/genie-safe-delete.cjs +4 -781
  19. package/cli/vendor/shim/native/binding.gyp +13 -0
  20. package/cli/vendor/shim/native/brokered_sandbox_native.c +293 -0
  21. package/cli/vendor/shim/native/darwin-arm64/brokered-sandbox-native.node +0 -0
  22. package/cli/vendor/shim/native/darwin-x64/brokered-sandbox-native.node +0 -0
  23. package/cli/vendor/shim/node-brokered-fs-shim.cjs +932 -0
  24. package/cli/vendor/shim/node-language-shim.cjs +35 -0
  25. package/cli/vendor/shim/node-safe-delete-shim.cjs +810 -0
  26. package/cli/vendor/shim/safe-bin/safe-delete-bash-env.sh +1 -1
  27. package/cli/vendor/shim/safe-bin/safe-delete-common.sh +95 -34
  28. package/cli/vendor/shim/safe-delete-broker-delete.cjs +166 -0
  29. package/cli/vendor/shim/shell-runtime-bash-env.sh +8 -0
  30. package/cli/vendor/shim/sitecustomize.py +551 -22
  31. package/cli/vendor/toybox-macos/LICENSE +12 -0
  32. package/cli/vendor/toybox-macos/toybox +0 -0
  33. package/cli/vendor/toybox-macos/toybox.sb +19 -0
  34. package/cli/vendor/zsh-macos/LICENSE +37 -0
  35. package/cli/vendor/zsh-macos/bin/zsh +0 -0
  36. package/package.json +1 -1
  37. package/cli/dist/web-ui/assets/index-BZjhFKbY.js +0 -1051
  38. package/cli/dist/web-ui/assets/index-C5EhSt3C.css +0 -32
@@ -13,8 +13,11 @@ genie-safe-delete — Python 运行时安全删除 shim
13
13
  """
14
14
 
15
15
  import errno
16
+ import builtins
17
+ import io
16
18
  import json
17
19
  import os
20
+ import socket
18
21
  import stat
19
22
  import sys
20
23
  import tempfile
@@ -29,6 +32,11 @@ _SESSION_ID = (
29
32
  or os.environ.get("CLAUDE_SESSION_ID")
30
33
  )
31
34
  _IN_SANDBOX = os.environ.get("CODEBUDDY_SAFE_DELETE_SANDBOX") == "1"
35
+ _SAFE_DELETE_ENABLED = os.environ.get("CODEBUDDY_SAFE_DELETE_ENABLED") != "0"
36
+ _BROKERED_FS_HOOK_ENABLED = (
37
+ os.environ.get("CODEBUDDY_BROKERED_FS_HOOK_ENABLED") == "1"
38
+ or _IN_SANDBOX
39
+ )
32
40
 
33
41
  if _SESSION_ID:
34
42
  # __file__ 在 sitecustomize 加载时指向本文件路径,dirname 即 vendor/shim/
@@ -268,14 +276,484 @@ if _SESSION_ID:
268
276
  import pathlib
269
277
  import platform
270
278
 
279
+ _orig_builtin_open = builtins.open
280
+ _orig_io_open = io.open
281
+ _orig_os_open = os.open
271
282
  _orig_remove = os.remove
272
283
  _orig_unlink = os.unlink
273
284
  _orig_rmdir = os.rmdir
285
+ _orig_mkdir = os.mkdir
286
+ _orig_makedirs = os.makedirs
287
+ _orig_rename = os.rename
288
+ _orig_replace = os.replace
289
+ _orig_chmod = os.chmod
290
+ _orig_link = os.link
291
+ _orig_symlink = os.symlink
274
292
  _orig_shutil_rmtree = shutil.rmtree
293
+ _orig_shutil_copyfile = shutil.copyfile
294
+ _orig_shutil_copy2 = shutil.copy2
295
+ _orig_shutil_copytree = shutil.copytree
275
296
  _orig_path_unlink = pathlib.Path.unlink
276
297
  _orig_path_rmdir = pathlib.Path.rmdir
298
+ _orig_path_mkdir = pathlib.Path.mkdir
299
+ _orig_path_touch = pathlib.Path.touch
277
300
  _report_path = os.environ.get("CODEBUDDY_SAFE_DELETE_REPORT_PATH")
278
301
 
302
+ # -----------------------------------------------------------------------
303
+ # Brokered sandbox file-token support.
304
+ #
305
+ # Mirrors the shell/toybox broker model, but the interception point is
306
+ # Python's runtime file APIs. The host still owns policy and token issuing.
307
+ # -----------------------------------------------------------------------
308
+
309
+ _broker_socket_path = os.environ.get("CODEBUDDY_SANDBOX_BROKER_IPC_ADDRESS")
310
+ _broker_session_id = os.environ.get("CODEBUDDY_SANDBOX_BROKER_SESSION_ID") or _SESSION_ID
311
+ _broker_tool_call_id = (
312
+ os.environ.get("CODEBUDDY_SANDBOX_BROKER_TOOL_CALL_ID")
313
+ or os.environ.get("CODEBUDDY_TOOL_CALL_ID")
314
+ )
315
+ _broker_trace_id = os.environ.get("CODEBUDDY_SANDBOX_BROKER_TRACE_ID")
316
+ _broker_file_token_command = (
317
+ os.environ.get("CODEBUDDY_SANDBOX_FILE_TOKEN_OPERATION_COMMAND")
318
+ or "FileTokenRequest"
319
+ )
320
+ _broker_host_operation_command = os.environ.get(
321
+ "CODEBUDDY_SANDBOX_HOST_FILE_OPERATION_COMMAND"
322
+ )
323
+ _broker_request_seq = 0
324
+ _broker_host_operation_depth = 0
325
+ _COPYTREE_DEFAULT = object()
326
+
327
+ def _broker_enabled():
328
+ return bool(sys.platform == "darwin" and _BROKERED_FS_HOOK_ENABLED and _broker_socket_path and _broker_session_id and _broker_file_token_command)
329
+
330
+ def _broker_write_completed_enabled():
331
+ return bool(sys.platform == "darwin" and _BROKERED_FS_HOOK_ENABLED and _broker_socket_path and _broker_session_id)
332
+
333
+ def _broker_host_ops_enabled():
334
+ return bool(
335
+ sys.platform == "darwin"
336
+ and _BROKERED_FS_HOOK_ENABLED
337
+ and _broker_socket_path
338
+ and _broker_session_id
339
+ and _broker_host_operation_command
340
+ and _broker_host_operation_depth == 0
341
+ )
342
+
343
+ def _broker_abs_path(file_path, dir_fd=None):
344
+ if isinstance(file_path, int):
345
+ return None
346
+ try:
347
+ raw_path = os.fspath(file_path)
348
+ except TypeError:
349
+ return None
350
+ if isinstance(raw_path, bytes):
351
+ try:
352
+ raw_path = os.fsdecode(raw_path)
353
+ except UnicodeError:
354
+ return None
355
+ if not raw_path:
356
+ return None
357
+ if dir_fd is not None and not os.path.isabs(raw_path):
358
+ return None
359
+ return os.path.abspath(raw_path)
360
+
361
+ def _broker_readline(sock):
362
+ chunks = []
363
+ while True:
364
+ chunk = sock.recv(1)
365
+ if not chunk:
366
+ break
367
+ if chunk == b"\n":
368
+ break
369
+ chunks.append(chunk)
370
+ return b"".join(chunks).decode("utf-8", errors="replace")
371
+
372
+ def _broker_consume_token(token):
373
+ if sys.platform != "darwin":
374
+ return
375
+ import ctypes
376
+
377
+ lib = ctypes.CDLL(None, use_errno=True)
378
+ fn = lib.sandbox_extension_consume
379
+ fn.argtypes = [ctypes.c_char_p]
380
+ fn.restype = ctypes.c_int64
381
+ handle = fn(token.encode("utf-8"))
382
+ if handle <= 0:
383
+ err = ctypes.get_errno() or errno.EINVAL
384
+ raise OSError(err, os.strerror(err))
385
+
386
+ def _broker_send_request(request):
387
+ try:
388
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
389
+ sock.settimeout(5)
390
+ sock.connect(_broker_socket_path)
391
+ sock.sendall((json.dumps(request, separators=(",", ":")) + "\n").encode("utf-8"))
392
+ line = _broker_readline(sock)
393
+ except Exception:
394
+ return None
395
+
396
+ try:
397
+ response = json.loads(line)
398
+ except Exception:
399
+ return None
400
+ return response if isinstance(response, dict) else None
401
+
402
+ def _broker_request_file_token(abs_path, operation):
403
+ global _broker_request_seq
404
+ if not _broker_enabled():
405
+ return
406
+
407
+ _broker_request_seq += 1
408
+ request = {
409
+ "id": "python-%d" % _broker_request_seq,
410
+ "command": _broker_file_token_command,
411
+ "runtime": "python",
412
+ "operation": operation,
413
+ "path": abs_path,
414
+ "sessionId": _broker_session_id,
415
+ }
416
+ if _broker_tool_call_id:
417
+ request["toolCallId"] = _broker_tool_call_id
418
+ if _broker_trace_id:
419
+ request["brokerTraceId"] = _broker_trace_id
420
+
421
+ response = _broker_send_request(request)
422
+ if response is None:
423
+ # Match toybox's fail-to-sandbox behavior: if IPC is unavailable,
424
+ # do not grant anything here; let the native sandbox decide.
425
+ return
426
+
427
+ token = response.get("token") if response.get("ok") is True else None
428
+ if isinstance(token, str) and token:
429
+ _broker_consume_token(token)
430
+ return
431
+ if response.get("decision") == "sandbox":
432
+ return
433
+
434
+ raise PermissionError(
435
+ response.get("error")
436
+ or "Brokered sandbox denied %s access to %s" % (operation, abs_path)
437
+ )
438
+
439
+ def _broker_notify_file_write_completed(abs_path, existed_before):
440
+ global _broker_request_seq
441
+ if existed_before:
442
+ return
443
+ if not _broker_write_completed_enabled():
444
+ return
445
+
446
+ _broker_request_seq += 1
447
+ request = {
448
+ "id": "python-%d" % _broker_request_seq,
449
+ "command": "FileWriteCompleted",
450
+ "runtime": "python",
451
+ "operation": "write",
452
+ "path": abs_path,
453
+ "sessionId": _broker_session_id,
454
+ }
455
+ if _broker_tool_call_id:
456
+ request["toolCallId"] = _broker_tool_call_id
457
+ if _broker_trace_id:
458
+ request["brokerTraceId"] = _broker_trace_id
459
+ _broker_send_request(request)
460
+
461
+ def _broker_request_host_operation(operation, primary_path, **fields):
462
+ global _broker_request_seq
463
+ if not _broker_host_ops_enabled():
464
+ return False
465
+
466
+ _broker_request_seq += 1
467
+ request = {
468
+ "id": "python-%d" % _broker_request_seq,
469
+ "command": _broker_host_operation_command,
470
+ "runtime": "python",
471
+ "operation": operation,
472
+ "sessionId": _broker_session_id,
473
+ }
474
+ request.update(fields)
475
+ if _broker_tool_call_id:
476
+ request["toolCallId"] = _broker_tool_call_id
477
+ if _broker_trace_id:
478
+ request["brokerTraceId"] = _broker_trace_id
479
+
480
+ response = _broker_send_request(request)
481
+ if response is None:
482
+ return False
483
+ if response.get("ok") is True:
484
+ return True
485
+ if response.get("decision") == "sandbox":
486
+ return False
487
+ raise PermissionError(
488
+ response.get("error")
489
+ or "Brokered sandbox denied host %s for %s" % (operation, primary_path)
490
+ )
491
+
492
+ def _broker_call_native_host_operation(fn, *args, **kwargs):
493
+ global _broker_host_operation_depth
494
+ _broker_host_operation_depth += 1
495
+ try:
496
+ return fn(*args, **kwargs)
497
+ finally:
498
+ _broker_host_operation_depth -= 1
499
+
500
+ def _broker_operation_for_mode(mode):
501
+ mode = mode or "r"
502
+ return "write" if any(ch in mode for ch in "wax+") else "read"
503
+
504
+ def _broker_operation_for_flags(flags):
505
+ write_flags = (
506
+ os.O_WRONLY
507
+ | os.O_RDWR
508
+ | os.O_CREAT
509
+ | os.O_TRUNC
510
+ | os.O_APPEND
511
+ | getattr(os, "O_TMPFILE", 0)
512
+ )
513
+ return "write" if flags & write_flags else "read"
514
+
515
+ def _broker_gate(file_path, operation, dir_fd=None):
516
+ abs_path = _broker_abs_path(file_path, dir_fd=dir_fd)
517
+ if abs_path:
518
+ _broker_request_file_token(abs_path, operation)
519
+
520
+ def _brokered_open(
521
+ open_fn,
522
+ file,
523
+ mode="r",
524
+ buffering=-1,
525
+ encoding=None,
526
+ errors=None,
527
+ newline=None,
528
+ closefd=True,
529
+ opener=None,
530
+ ):
531
+ operation = _broker_operation_for_mode(mode)
532
+ abs_path = _broker_abs_path(file)
533
+ existed_before = bool(abs_path and os.path.exists(abs_path))
534
+ if abs_path:
535
+ _broker_request_file_token(abs_path, operation)
536
+ file_obj = open_fn(file, mode, buffering, encoding, errors, newline, closefd, opener)
537
+ if operation == "write" and abs_path and os.path.exists(abs_path):
538
+ _broker_notify_file_write_completed(abs_path, existed_before)
539
+ return file_obj
540
+
541
+ def _brokered_builtin_open(
542
+ file,
543
+ mode="r",
544
+ buffering=-1,
545
+ encoding=None,
546
+ errors=None,
547
+ newline=None,
548
+ closefd=True,
549
+ opener=None,
550
+ ):
551
+ return _brokered_open(
552
+ _orig_builtin_open,
553
+ file, mode, buffering, encoding, errors, newline, closefd, opener,
554
+ )
555
+
556
+ def _brokered_io_open(
557
+ file,
558
+ mode="r",
559
+ buffering=-1,
560
+ encoding=None,
561
+ errors=None,
562
+ newline=None,
563
+ closefd=True,
564
+ opener=None,
565
+ ):
566
+ return _brokered_open(
567
+ _orig_io_open,
568
+ file, mode, buffering, encoding, errors, newline, closefd, opener,
569
+ )
570
+
571
+ def _brokered_os_open(path, flags, mode=0o777, *, dir_fd=None):
572
+ _broker_gate(path, _broker_operation_for_flags(flags), dir_fd=dir_fd)
573
+ if dir_fd is not None:
574
+ return _orig_os_open(path, flags, mode, dir_fd=dir_fd)
575
+ return _orig_os_open(path, flags, mode)
576
+
577
+ def _brokered_os_mkdir(path, mode=0o777, *, dir_fd=None):
578
+ abs_path = _broker_abs_path(path, dir_fd=dir_fd)
579
+ if abs_path and isinstance(mode, int) and _broker_request_host_operation(
580
+ "mkdir", abs_path, path=abs_path, mode=mode
581
+ ):
582
+ return None
583
+ if dir_fd is not None:
584
+ return _broker_call_native_host_operation(_orig_mkdir, path, mode, dir_fd=dir_fd)
585
+ return _broker_call_native_host_operation(_orig_mkdir, path, mode)
586
+
587
+ def _brokered_os_makedirs(name, mode=0o777, exist_ok=False):
588
+ abs_path = _broker_abs_path(name)
589
+ if abs_path and isinstance(mode, int):
590
+ if not exist_ok and os.path.exists(abs_path):
591
+ return _broker_call_native_host_operation(
592
+ _orig_makedirs, name, mode=mode, exist_ok=exist_ok
593
+ )
594
+ if _broker_request_host_operation(
595
+ "mkdir", abs_path, path=abs_path, recursive=True, mode=mode
596
+ ):
597
+ return None
598
+ return _broker_call_native_host_operation(
599
+ _orig_makedirs, name, mode=mode, exist_ok=exist_ok
600
+ )
601
+
602
+ def _brokered_os_rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None):
603
+ from_path = _broker_abs_path(src, dir_fd=src_dir_fd)
604
+ to_path = _broker_abs_path(dst, dir_fd=dst_dir_fd)
605
+ if from_path and to_path and _broker_request_host_operation(
606
+ "rename", from_path, **{"from": from_path, "to": to_path}
607
+ ):
608
+ return None
609
+ if src_dir_fd is not None or dst_dir_fd is not None:
610
+ return _broker_call_native_host_operation(
611
+ _orig_rename, src, dst, src_dir_fd=src_dir_fd, dst_dir_fd=dst_dir_fd
612
+ )
613
+ return _broker_call_native_host_operation(_orig_rename, src, dst)
614
+
615
+ def _brokered_os_replace(src, dst, *, src_dir_fd=None, dst_dir_fd=None):
616
+ from_path = _broker_abs_path(src, dir_fd=src_dir_fd)
617
+ to_path = _broker_abs_path(dst, dir_fd=dst_dir_fd)
618
+ if from_path and to_path and _broker_request_host_operation(
619
+ "rename", from_path, **{"from": from_path, "to": to_path}
620
+ ):
621
+ return None
622
+ if src_dir_fd is not None or dst_dir_fd is not None:
623
+ return _broker_call_native_host_operation(
624
+ _orig_replace, src, dst, src_dir_fd=src_dir_fd, dst_dir_fd=dst_dir_fd
625
+ )
626
+ return _broker_call_native_host_operation(_orig_replace, src, dst)
627
+
628
+ def _brokered_os_chmod(path, mode, *, dir_fd=None, follow_symlinks=True):
629
+ abs_path = _broker_abs_path(path, dir_fd=dir_fd)
630
+ if abs_path and isinstance(mode, int) and follow_symlinks and _broker_request_host_operation(
631
+ "chmod", abs_path, path=abs_path, mode=mode
632
+ ):
633
+ return None
634
+ if dir_fd is not None:
635
+ return _broker_call_native_host_operation(
636
+ _orig_chmod, path, mode, dir_fd=dir_fd, follow_symlinks=follow_symlinks
637
+ )
638
+ return _broker_call_native_host_operation(
639
+ _orig_chmod, path, mode, follow_symlinks=follow_symlinks
640
+ )
641
+
642
+ def _brokered_os_link(src, dst, *, src_dir_fd=None, dst_dir_fd=None, follow_symlinks=True):
643
+ from_path = _broker_abs_path(src, dir_fd=src_dir_fd)
644
+ to_path = _broker_abs_path(dst, dir_fd=dst_dir_fd)
645
+ should_broker = follow_symlinks and from_path and to_path
646
+ try:
647
+ should_broker = should_broker and not os.path.islink(from_path)
648
+ except Exception:
649
+ should_broker = False
650
+ if should_broker and _broker_request_host_operation(
651
+ "link", from_path, **{"from": from_path, "to": to_path, "symbolic": False}
652
+ ):
653
+ return None
654
+ if src_dir_fd is not None or dst_dir_fd is not None:
655
+ return _broker_call_native_host_operation(
656
+ _orig_link, src, dst, src_dir_fd=src_dir_fd, dst_dir_fd=dst_dir_fd,
657
+ follow_symlinks=follow_symlinks
658
+ )
659
+ return _broker_call_native_host_operation(
660
+ _orig_link, src, dst, follow_symlinks=follow_symlinks
661
+ )
662
+
663
+ def _brokered_os_symlink(src, dst, target_is_directory=False, *, dir_fd=None):
664
+ source = None
665
+ try:
666
+ source = os.fspath(src)
667
+ if isinstance(source, bytes):
668
+ source = os.fsdecode(source)
669
+ except (TypeError, UnicodeError):
670
+ source = None
671
+ to_path = _broker_abs_path(dst, dir_fd=dir_fd)
672
+ if source and to_path and _broker_request_host_operation(
673
+ "link", to_path, **{"from": source, "to": to_path, "symbolic": True}
674
+ ):
675
+ return None
676
+ if dir_fd is not None:
677
+ return _broker_call_native_host_operation(
678
+ _orig_symlink, src, dst, target_is_directory, dir_fd=dir_fd
679
+ )
680
+ return _broker_call_native_host_operation(
681
+ _orig_symlink, src, dst, target_is_directory
682
+ )
683
+
684
+ def _brokered_shutil_copyfile(src, dst, *, follow_symlinks=True):
685
+ from_path = _broker_abs_path(src)
686
+ to_path = _broker_abs_path(dst)
687
+ if follow_symlinks and from_path and to_path and _broker_request_host_operation(
688
+ "copy", from_path, **{"from": from_path, "to": to_path, "recursive": False}
689
+ ):
690
+ return dst
691
+ return _broker_call_native_host_operation(
692
+ _orig_shutil_copyfile, src, dst, follow_symlinks=follow_symlinks
693
+ )
694
+
695
+ def _brokered_shutil_copytree(
696
+ src,
697
+ dst,
698
+ symlinks=False,
699
+ ignore=None,
700
+ copy_function=_COPYTREE_DEFAULT,
701
+ ignore_dangling_symlinks=False,
702
+ dirs_exist_ok=False,
703
+ ):
704
+ native_copy_function = (
705
+ _orig_shutil_copy2
706
+ if copy_function is _COPYTREE_DEFAULT
707
+ else copy_function
708
+ )
709
+ can_broker = (
710
+ symlinks is False
711
+ and ignore is None
712
+ and native_copy_function is _orig_shutil_copy2
713
+ and ignore_dangling_symlinks is False
714
+ and dirs_exist_ok is False
715
+ )
716
+ from_path = _broker_abs_path(src) if can_broker else None
717
+ to_path = _broker_abs_path(dst) if can_broker else None
718
+ if from_path and to_path and _broker_request_host_operation(
719
+ "copy", from_path, **{"from": from_path, "to": to_path, "recursive": True}
720
+ ):
721
+ return dst
722
+ return _broker_call_native_host_operation(
723
+ _orig_shutil_copytree,
724
+ src,
725
+ dst,
726
+ symlinks=symlinks,
727
+ ignore=ignore,
728
+ copy_function=native_copy_function,
729
+ ignore_dangling_symlinks=ignore_dangling_symlinks,
730
+ dirs_exist_ok=dirs_exist_ok,
731
+ )
732
+
733
+ def _brokered_path_mkdir(self, mode=0o777, parents=False, exist_ok=False):
734
+ abs_path = _broker_abs_path(self)
735
+ if abs_path and isinstance(mode, int):
736
+ if not exist_ok and os.path.exists(abs_path):
737
+ return _broker_call_native_host_operation(
738
+ _orig_path_mkdir, self, mode=mode, parents=parents, exist_ok=exist_ok
739
+ )
740
+ if _broker_request_host_operation(
741
+ "mkdir", abs_path, path=abs_path, recursive=parents, mode=mode
742
+ ):
743
+ return None
744
+ return _broker_call_native_host_operation(
745
+ _orig_path_mkdir, self, mode=mode, parents=parents, exist_ok=exist_ok
746
+ )
747
+
748
+ def _brokered_path_touch(self, mode=0o666, exist_ok=True):
749
+ abs_path = _broker_abs_path(self)
750
+ can_broker = abs_path and mode == 0o666 and (exist_ok or not os.path.exists(abs_path))
751
+ if can_broker and _broker_request_host_operation("touch", abs_path, path=abs_path):
752
+ return None
753
+ return _broker_call_native_host_operation(
754
+ _orig_path_touch, self, mode=mode, exist_ok=exist_ok
755
+ )
756
+
279
757
  def _path_for_compare(path):
280
758
  return os.path.normcase(os.path.realpath(os.path.abspath(path)))
281
759
 
@@ -314,7 +792,7 @@ if _SESSION_ID:
314
792
  if not _report_path:
315
793
  return
316
794
  try:
317
- with open(_report_path, "a", encoding="utf-8") as f:
795
+ with _orig_builtin_open(_report_path, "a", encoding="utf-8") as f:
318
796
  f.write(json.dumps({
319
797
  "operation": "trash",
320
798
  "runtime": "python",
@@ -419,11 +897,30 @@ if _SESSION_ID:
419
897
  return False
420
898
 
421
899
  # -----------------------------------------------------------------------
422
- # 核心:尝试移入回收站。优先 binary,降级到平台内联实现。
900
+ # 核心:移入回收站。
423
901
  # 返回 True=已移入回收站;抛错=fail-closed,调用方不得真删。
424
902
  # -----------------------------------------------------------------------
425
903
 
426
- def _try_trash(abs_path):
904
+ def _try_broker_delete(abs_path, recursive=False, force=False):
905
+ if not _broker_host_ops_enabled():
906
+ return "unavailable"
907
+ try:
908
+ if _broker_request_host_operation(
909
+ "delete",
910
+ abs_path,
911
+ path=abs_path,
912
+ deleteMode="trash",
913
+ recursive=bool(recursive),
914
+ force=bool(force),
915
+ ):
916
+ return "ok"
917
+ return "unavailable"
918
+ except PermissionError:
919
+ return "denied"
920
+ except Exception:
921
+ return "unavailable"
922
+
923
+ def _try_trash(abs_path, recursive=False, force=False):
427
924
  if sys.platform == "win32" and _IN_SANDBOX:
428
925
  message = (
429
926
  "[safe-delete][SAFE_DELETE_FAIL_CLOSED] "
@@ -439,6 +936,15 @@ if _SESSION_ID:
439
936
  pass
440
937
  raise OSError(message)
441
938
  _check_bulk_delete_guard(abs_path)
939
+
940
+ broker_result = _try_broker_delete(abs_path, recursive=recursive, force=force)
941
+ if broker_result == "ok":
942
+ return True
943
+ if broker_result == "denied":
944
+ raise PermissionError(
945
+ "[safe-delete] broker denied delete: path=%s" % abs_path
946
+ )
947
+
442
948
  if _try_trash_via_binary(abs_path):
443
949
  _record_trash(abs_path)
444
950
  return True
@@ -461,17 +967,18 @@ if _SESSION_ID:
461
967
  except OSError:
462
968
  return False
463
969
 
464
- def _is_under_os_tmp_dir(abs_path):
970
+ def _is_under_root(root, abs_path):
465
971
  try:
466
972
  target = _path_for_compare(os.fsdecode(abs_path))
467
- except (TypeError, UnicodeError):
973
+ root_path = _path_for_compare(root)
974
+ rel = os.path.relpath(target, root_path)
975
+ except (TypeError, UnicodeError, ValueError):
468
976
  return False
977
+ return rel not in ("", ".") and rel != ".." and not rel.startswith(".." + os.sep) and not os.path.isabs(rel)
978
+
979
+ def _is_under_os_tmp_dir(abs_path):
469
980
  for tmp_dir in _os_tmp_dirs:
470
- try:
471
- rel = os.path.relpath(target, tmp_dir)
472
- except ValueError:
473
- continue
474
- if rel not in ("", ".") and rel != ".." and not rel.startswith(".." + os.sep) and not os.path.isabs(rel):
981
+ if _is_under_root(tmp_dir, abs_path):
475
982
  return True
476
983
  return False
477
984
 
@@ -489,6 +996,9 @@ if _SESSION_ID:
489
996
  return True
490
997
  return False
491
998
 
999
+ def _should_bypass_safe_delete(abs_path):
1000
+ return _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_temp_dir(abs_path)
1001
+
492
1002
  # -----------------------------------------------------------------------
493
1003
  # 包装:os.remove / os.unlink (文件,不能是目录)
494
1004
  # -----------------------------------------------------------------------
@@ -498,7 +1008,7 @@ if _SESSION_ID:
498
1008
  if dir_fd is not None:
499
1009
  return _orig_remove(path, dir_fd=dir_fd)
500
1010
  abs_path = os.path.abspath(os.fspath(path))
501
- if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_temp_dir(abs_path):
1011
+ if _should_bypass_safe_delete(abs_path):
502
1012
  return _orig_remove(path)
503
1013
  st = _safe_lstat(abs_path)
504
1014
  if not st:
@@ -515,7 +1025,7 @@ if _SESSION_ID:
515
1025
  if dir_fd is not None:
516
1026
  return _orig_rmdir(path, dir_fd=dir_fd)
517
1027
  abs_path = os.path.abspath(os.fspath(path))
518
- if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_temp_dir(abs_path):
1028
+ if _should_bypass_safe_delete(abs_path):
519
1029
  return _orig_rmdir(path)
520
1030
  st = _safe_lstat(abs_path)
521
1031
  if not st:
@@ -539,7 +1049,7 @@ if _SESSION_ID:
539
1049
  path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
540
1050
  )
541
1051
 
542
- if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_temp_dir(abs_path):
1052
+ if _should_bypass_safe_delete(abs_path):
543
1053
  return _orig_shutil_rmtree(
544
1054
  path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
545
1055
  )
@@ -556,7 +1066,7 @@ if _SESSION_ID:
556
1066
  )
557
1067
 
558
1068
  try:
559
- _try_trash(abs_path)
1069
+ _try_trash(abs_path, recursive=True)
560
1070
  except Exception as e:
561
1071
  if ignore_errors:
562
1072
  return
@@ -571,7 +1081,7 @@ if _SESSION_ID:
571
1081
 
572
1082
  def _safe_path_unlink(self, missing_ok=False):
573
1083
  abs_path = os.path.abspath(str(self))
574
- if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_temp_dir(abs_path):
1084
+ if _should_bypass_safe_delete(abs_path):
575
1085
  return _orig_path_unlink(self, missing_ok=missing_ok)
576
1086
  st = _safe_lstat(abs_path)
577
1087
  if not st:
@@ -584,7 +1094,7 @@ if _SESSION_ID:
584
1094
 
585
1095
  def _safe_path_rmdir(self):
586
1096
  abs_path = os.path.abspath(str(self))
587
- if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_temp_dir(abs_path):
1097
+ if _should_bypass_safe_delete(abs_path):
588
1098
  return _orig_path_rmdir(self)
589
1099
  st = _safe_lstat(abs_path)
590
1100
  if not st:
@@ -599,9 +1109,28 @@ if _SESSION_ID:
599
1109
  # 安装 patch
600
1110
  # -----------------------------------------------------------------------
601
1111
 
602
- os.remove = _safe_remove
603
- os.unlink = _safe_remove # CPython 里 os.unlink is os.remove,显式 patch 兼容其它实现
604
- os.rmdir = _safe_rmdir
605
- shutil.rmtree = _safe_shutil_rmtree
606
- pathlib.Path.unlink = _safe_path_unlink
607
- pathlib.Path.rmdir = _safe_path_rmdir
1112
+ if _broker_enabled():
1113
+ builtins.open = _brokered_builtin_open
1114
+ io.open = _brokered_io_open
1115
+ os.open = _brokered_os_open
1116
+
1117
+ if _broker_host_ops_enabled():
1118
+ os.mkdir = _brokered_os_mkdir
1119
+ os.makedirs = _brokered_os_makedirs
1120
+ os.rename = _brokered_os_rename
1121
+ os.replace = _brokered_os_replace
1122
+ os.chmod = _brokered_os_chmod
1123
+ os.link = _brokered_os_link
1124
+ os.symlink = _brokered_os_symlink
1125
+ shutil.copyfile = _brokered_shutil_copyfile
1126
+ shutil.copytree = _brokered_shutil_copytree
1127
+ pathlib.Path.mkdir = _brokered_path_mkdir
1128
+ pathlib.Path.touch = _brokered_path_touch
1129
+
1130
+ if _SAFE_DELETE_ENABLED:
1131
+ os.remove = _safe_remove
1132
+ os.unlink = _safe_remove # CPython 里 os.unlink is os.remove,显式 patch 兼容其它实现
1133
+ os.rmdir = _safe_rmdir
1134
+ shutil.rmtree = _safe_shutil_rmtree
1135
+ pathlib.Path.unlink = _safe_path_unlink
1136
+ pathlib.Path.rmdir = _safe_path_rmdir
@@ -0,0 +1,12 @@
1
+ Copyright (C) 2006, 2019 by Rob Landley <rob@landley.net>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
7
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
8
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
9
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
10
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
11
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
12
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
Binary file