@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.
- package/CHANGELOG.md +23 -0
- package/LICENSE +202 -0
- package/NOTICE +32 -0
- package/README.md +116 -3
- package/atomic_io.py +94 -0
- package/autostart.py +199 -0
- package/bin/smart-tool.mjs +82 -0
- package/browser_service_client.py +232 -0
- package/capabilities.py +63 -0
- package/client_hooks.py +192 -0
- package/code_graph.py +403 -0
- package/code_graph_java.mjs +49 -0
- package/code_graph_js.cjs +135 -0
- package/config.py +241 -0
- package/daemon_launcher.py +165 -0
- package/directory_watch.py +131 -0
- package/document_text.py +69 -0
- package/duplicates.py +313 -0
- package/embedding_cache.py +99 -0
- package/endpoint_sync.py +179 -0
- package/folder_picker.py +73 -0
- package/freshness.py +278 -0
- package/gateway.py +220 -0
- package/hook_decision.py +96 -0
- package/hooks/pretool_router.py +45 -0
- package/index_inventory.py +75 -0
- package/index_profile.py +167 -0
- package/index_scope.py +458 -0
- package/index_views.py +121 -0
- package/indexer.py +878 -0
- package/install.py +251 -0
- package/install_runtime.py +249 -0
- package/integration_metrics.py +82 -0
- package/local_embedder.py +172 -0
- package/model_adapters/__init__.py +0 -0
- package/model_adapters/openai_compatible.py +57 -0
- package/model_catalog.py +74 -0
- package/model_client.py +38 -0
- package/model_defaults.py +70 -0
- package/package.json +35 -6
- package/paths.py +6 -0
- package/project_identity.py +42 -0
- package/project_insights_ui.py +38 -0
- package/project_maps_ui.py +96 -0
- package/project_monitor.py +204 -0
- package/project_scheduler.py +61 -0
- package/project_storage.py +165 -0
- package/project_store.py +220 -0
- package/projects_ui.py +146 -0
- package/requirements.txt +8 -0
- package/research_cache.py +171 -0
- package/research_metrics.py +110 -0
- package/router.py +582 -0
- package/secret_store.py +121 -0
- package/setup_ui.py +986 -0
- package/site_memory.py +53 -0
- package/smart_tool_daemon.py +3604 -0
- package/tray.py +245 -0
- package/update_check.py +22 -0
- package/usage_meter.py +124 -0
- package/version.py +18 -0
- package/view_compare.py +68 -0
- package/web_adapters/browser/browser_service.py +302 -0
- package/web_adapters/browser/crawl4ai_search.py +236 -0
- package/web_adapters/browser/requirements.txt +2 -0
- package/web_adapters/node/adapter-common.mjs +40 -0
- package/web_adapters/node/ddg-search.mjs +89 -0
- package/web_adapters/node/open-websearch-adapter.mjs +77 -0
- package/web_adapters/node/package-lock.json +2720 -0
- package/web_adapters/node/package.json +12 -0
- package/web_adapters/node/wikipedia-search.mjs +44 -0
- package/web_document_graph.py +71 -0
- package/web_fetch.py +242 -0
- package/web_search_adapters.py +193 -0
- package/web_search_ddgs.py +42 -0
- package/web_search_external.py +288 -0
- package/web_search_health.py +88 -0
package/secret_store.py
ADDED
|
@@ -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")
|