@allansantos-dev/smart-tool 0.0.0-stage → 0.9.0

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 (77) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +32 -0
  4. package/README.md +116 -3
  5. package/atomic_io.py +94 -0
  6. package/autostart.py +199 -0
  7. package/bin/smart-tool.mjs +82 -0
  8. package/browser_service_client.py +232 -0
  9. package/capabilities.py +63 -0
  10. package/client_hooks.py +192 -0
  11. package/code_graph.py +403 -0
  12. package/code_graph_java.mjs +49 -0
  13. package/code_graph_js.cjs +135 -0
  14. package/config.py +241 -0
  15. package/daemon_launcher.py +165 -0
  16. package/directory_watch.py +131 -0
  17. package/document_text.py +69 -0
  18. package/duplicates.py +313 -0
  19. package/embedding_cache.py +99 -0
  20. package/endpoint_sync.py +179 -0
  21. package/folder_picker.py +73 -0
  22. package/freshness.py +278 -0
  23. package/gateway.py +220 -0
  24. package/hook_decision.py +96 -0
  25. package/hooks/pretool_router.py +45 -0
  26. package/index_inventory.py +75 -0
  27. package/index_profile.py +167 -0
  28. package/index_scope.py +458 -0
  29. package/index_views.py +121 -0
  30. package/indexer.py +878 -0
  31. package/install.py +251 -0
  32. package/install_runtime.py +249 -0
  33. package/integration_metrics.py +82 -0
  34. package/local_embedder.py +172 -0
  35. package/model_adapters/__init__.py +0 -0
  36. package/model_adapters/openai_compatible.py +57 -0
  37. package/model_catalog.py +74 -0
  38. package/model_client.py +38 -0
  39. package/model_defaults.py +70 -0
  40. package/package.json +35 -6
  41. package/paths.py +6 -0
  42. package/project_identity.py +42 -0
  43. package/project_insights_ui.py +38 -0
  44. package/project_maps_ui.py +96 -0
  45. package/project_monitor.py +204 -0
  46. package/project_scheduler.py +61 -0
  47. package/project_storage.py +165 -0
  48. package/project_store.py +220 -0
  49. package/projects_ui.py +146 -0
  50. package/requirements.txt +8 -0
  51. package/research_cache.py +171 -0
  52. package/research_metrics.py +110 -0
  53. package/router.py +582 -0
  54. package/secret_store.py +121 -0
  55. package/setup_ui.py +986 -0
  56. package/site_memory.py +53 -0
  57. package/smart_tool_daemon.py +3604 -0
  58. package/tray.py +245 -0
  59. package/update_check.py +22 -0
  60. package/usage_meter.py +124 -0
  61. package/version.py +18 -0
  62. package/view_compare.py +68 -0
  63. package/web_adapters/browser/browser_service.py +302 -0
  64. package/web_adapters/browser/crawl4ai_search.py +236 -0
  65. package/web_adapters/browser/requirements.txt +2 -0
  66. package/web_adapters/node/adapter-common.mjs +40 -0
  67. package/web_adapters/node/ddg-search.mjs +89 -0
  68. package/web_adapters/node/open-websearch-adapter.mjs +77 -0
  69. package/web_adapters/node/package-lock.json +2720 -0
  70. package/web_adapters/node/package.json +12 -0
  71. package/web_adapters/node/wikipedia-search.mjs +44 -0
  72. package/web_document_graph.py +71 -0
  73. package/web_fetch.py +242 -0
  74. package/web_search_adapters.py +193 -0
  75. package/web_search_ddgs.py +42 -0
  76. package/web_search_external.py +288 -0
  77. package/web_search_health.py +88 -0
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ """Proteção em repouso de segredos gravados em disco via DPAPI (`CryptProtectData`/
3
+ `CryptUnprotectData`, escopo do usuário atual) — a mesma mecânica que o launcher
4
+ PowerShell usa com `ConvertFrom-SecureString`, aqui em `ctypes` para manter a regra
5
+ stdlib-only.
6
+
7
+ O que isto protege e o que não protege, para ninguém confiar mais do que deve:
8
+ o blob só é decifrável pela conta Windows que o gerou, na máquina em que foi gerado
9
+ (a chave-mestra DPAPI vive no perfil e é derivada da credencial de logon). Então uma
10
+ cópia *deste arquivo* — backup, sincronização de perfil, coleta de diagnóstico, pacote
11
+ de suporte, upload de EDR, pendrive — deixa de carregar credencial utilizável. Não é
12
+ barreira contra código rodando *como o próprio usuário*: esse código chama
13
+ `CryptUnprotectData` igual a este módulo. Também não protege o valor em memória.
14
+
15
+ A garantia vale por arquivo, não pelo segredo: enquanto o mesmo valor existir em outro
16
+ arquivo desprotegido, quem copia a pasta pega a cópia mais fácil.
17
+ """
18
+ import base64
19
+ import os
20
+
21
+ PREFIX = "dpapi:v1:"
22
+ _PREFIX_BYTES = PREFIX.encode("ascii")
23
+
24
+ _CRYPTPROTECT_UI_FORBIDDEN = 0x01
25
+
26
+
27
+ class SecretProtectionError(Exception):
28
+ """DPAPI indisponível ou recusou o blob (perfil diferente, máquina diferente,
29
+ chave-mestra recriada). Mensagem já pronta para exibir."""
30
+
31
+
32
+ def _load_api():
33
+ if os.name != "nt":
34
+ return None
35
+ try:
36
+ import ctypes
37
+ from ctypes import wintypes
38
+
39
+ class DataBlob(ctypes.Structure):
40
+ _fields_ = [("cbData", wintypes.DWORD), ("pbData", ctypes.POINTER(ctypes.c_char))]
41
+
42
+ blob_p = ctypes.POINTER(DataBlob)
43
+ crypt32 = ctypes.WinDLL("crypt32.dll", use_last_error=True)
44
+ kernel32 = ctypes.WinDLL("kernel32.dll", use_last_error=True)
45
+ for name in ("CryptProtectData", "CryptUnprotectData"):
46
+ fn = getattr(crypt32, name)
47
+ fn.argtypes = [blob_p, ctypes.c_void_p, blob_p, ctypes.c_void_p,
48
+ ctypes.c_void_p, wintypes.DWORD, blob_p]
49
+ fn.restype = wintypes.BOOL
50
+ kernel32.LocalFree.argtypes = [wintypes.HLOCAL]
51
+ kernel32.LocalFree.restype = wintypes.HLOCAL
52
+ return ctypes, DataBlob, crypt32, kernel32
53
+ except (ImportError, OSError, AttributeError):
54
+ return None
55
+
56
+
57
+ _API = _load_api()
58
+
59
+
60
+ def available():
61
+ return _API is not None
62
+
63
+
64
+ def _crypt(func_name, data):
65
+ ctypes, DataBlob, crypt32, kernel32 = _API
66
+ # `buf_in` precisa continuar referenciado até a chamada retornar: o blob aponta pra
67
+ # dentro dele.
68
+ buf_in = ctypes.create_string_buffer(data, len(data))
69
+ blob_in = DataBlob(len(data), ctypes.cast(buf_in, ctypes.POINTER(ctypes.c_char)))
70
+ blob_out = DataBlob()
71
+ ok = getattr(crypt32, func_name)(
72
+ ctypes.byref(blob_in), None, None, None, None,
73
+ _CRYPTPROTECT_UI_FORBIDDEN, ctypes.byref(blob_out),
74
+ )
75
+ if not ok:
76
+ raise SecretProtectionError(
77
+ f"{func_name} failed (error {ctypes.get_last_error()}): the protected secret "
78
+ "is only readable by the same Windows account and machine that saved it."
79
+ )
80
+ try:
81
+ return ctypes.string_at(blob_out.pbData, blob_out.cbData)
82
+ finally:
83
+ kernel32.LocalFree(ctypes.cast(blob_out.pbData, ctypes.c_void_p))
84
+
85
+
86
+ def is_protected(stored):
87
+ return isinstance(stored, str) and stored.startswith(PREFIX)
88
+
89
+
90
+ def protect(plaintext):
91
+ """Forma de armazenamento do segredo. Idempotente: um valor já protegido volta como
92
+ está, pra regravar a config não cifrar duas vezes."""
93
+ if not plaintext:
94
+ return ""
95
+ if is_protected(plaintext):
96
+ return plaintext
97
+ if not available():
98
+ raise SecretProtectionError("DPAPI unavailable on this platform.")
99
+ blob = _crypt("CryptProtectData", plaintext.encode("utf-8"))
100
+ stored = PREFIX + base64.b64encode(blob).decode("ascii")
101
+ # Invariante: nunca devolver uma forma de armazenamento que não volte a ser o segredo
102
+ # original — quem chama grava isto em lugar do texto puro.
103
+ if unprotect(stored) != plaintext:
104
+ raise SecretProtectionError("DPAPI did not return the same value when decrypting what it just encrypted.")
105
+ return stored
106
+
107
+
108
+ def unprotect(stored):
109
+ """Aceita tanto a forma protegida quanto texto puro: um valor legado em claro é
110
+ formato válido de entrada, e migrá-lo é responsabilidade de quem grava."""
111
+ if not stored:
112
+ return ""
113
+ if not is_protected(stored):
114
+ return stored
115
+ if not available():
116
+ raise SecretProtectionError("Secret is protected by DPAPI and DPAPI is unavailable on this platform.")
117
+ try:
118
+ blob = base64.b64decode(stored[len(PREFIX):], validate=True)
119
+ except (ValueError, TypeError) as exc:
120
+ raise SecretProtectionError(f"Protected secret has invalid base64 ({exc}).")
121
+ return _crypt("CryptUnprotectData", blob).decode("utf-8", "replace")