zerochat 7.3.0__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.
- zerochat-7.3.0.dist-info/METADATA +70 -0
- zerochat-7.3.0.dist-info/RECORD +130 -0
- zerochat-7.3.0.dist-info/WHEEL +5 -0
- zerochat-7.3.0.dist-info/entry_points.txt +2 -0
- zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
- zerochat-7.3.0.dist-info/top_level.txt +2 -0
- zerochat.py +2004 -0
- zerochat_runtime/__init__.py +1 -0
- zerochat_runtime/assets/css/base.css +199 -0
- zerochat_runtime/assets/css/components/composer.css +1258 -0
- zerochat_runtime/assets/css/components/debug.css +777 -0
- zerochat_runtime/assets/css/components/header.css +97 -0
- zerochat_runtime/assets/css/components/icons.css +39 -0
- zerochat_runtime/assets/css/components/markdown.css +685 -0
- zerochat_runtime/assets/css/components/messages.css +473 -0
- zerochat_runtime/assets/css/components/modals.css +1319 -0
- zerochat_runtime/assets/css/components/sidebar.css +622 -0
- zerochat_runtime/assets/css/components/tools.css +1223 -0
- zerochat_runtime/assets/css/layout.css +171 -0
- zerochat_runtime/assets/css/print.css +120 -0
- zerochat_runtime/assets/css/styles.css +18 -0
- zerochat_runtime/assets/css/theme-overrides.css +555 -0
- zerochat_runtime/assets/css/tokens.css +165 -0
- zerochat_runtime/assets/help/architecture.html +247 -0
- zerochat_runtime/assets/help/composer.html +134 -0
- zerochat_runtime/assets/help/conversations.html +105 -0
- zerochat_runtime/assets/help/debug.html +108 -0
- zerochat_runtime/assets/help/en/architecture.html +247 -0
- zerochat_runtime/assets/help/en/composer.html +134 -0
- zerochat_runtime/assets/help/en/conversations.html +104 -0
- zerochat_runtime/assets/help/en/debug.html +105 -0
- zerochat_runtime/assets/help/en/gemini-free.html +39 -0
- zerochat_runtime/assets/help/en/index.html +191 -0
- zerochat_runtime/assets/help/en/learning.html +71 -0
- zerochat_runtime/assets/help/en/mcp.html +143 -0
- zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/en/profiles.html +173 -0
- zerochat_runtime/assets/help/en/rag.html +130 -0
- zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
- zerochat_runtime/assets/help/en/tools-agent.html +133 -0
- zerochat_runtime/assets/help/en/webllm.html +355 -0
- zerochat_runtime/assets/help/gemini-free.html +39 -0
- zerochat_runtime/assets/help/help.css +604 -0
- zerochat_runtime/assets/help/help.js +51 -0
- zerochat_runtime/assets/help/index.html +194 -0
- zerochat_runtime/assets/help/learning.html +71 -0
- zerochat_runtime/assets/help/mcp.html +143 -0
- zerochat_runtime/assets/help/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/profiles.html +174 -0
- zerochat_runtime/assets/help/rag.html +130 -0
- zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
- zerochat_runtime/assets/help/tools-agent.html +133 -0
- zerochat_runtime/assets/help/webllm.html +359 -0
- zerochat_runtime/assets/js/agent-core.js +1553 -0
- zerochat_runtime/assets/js/api.js +844 -0
- zerochat_runtime/assets/js/app.js +2558 -0
- zerochat_runtime/assets/js/attachments.js +220 -0
- zerochat_runtime/assets/js/charts.js +338 -0
- zerochat_runtime/assets/js/chat-engine.js +566 -0
- zerochat_runtime/assets/js/config-store.js +207 -0
- zerochat_runtime/assets/js/context-manager.js +584 -0
- zerochat_runtime/assets/js/conversation-service.js +484 -0
- zerochat_runtime/assets/js/cookies.js +688 -0
- zerochat_runtime/assets/js/data-reset-service.js +136 -0
- zerochat_runtime/assets/js/debug.js +508 -0
- zerochat_runtime/assets/js/defaults.js +16 -0
- zerochat_runtime/assets/js/export.js +179 -0
- zerochat_runtime/assets/js/file-parser.js +2088 -0
- zerochat_runtime/assets/js/generation-controller.js +417 -0
- zerochat_runtime/assets/js/i18n.js +1483 -0
- zerochat_runtime/assets/js/icons.js +156 -0
- zerochat_runtime/assets/js/ingestionEngine.js +436 -0
- zerochat_runtime/assets/js/markdown.js +588 -0
- zerochat_runtime/assets/js/mcp.js +1271 -0
- zerochat_runtime/assets/js/message-turns.js +62 -0
- zerochat_runtime/assets/js/profile-backup.js +118 -0
- zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
- zerochat_runtime/assets/js/profile-repository.js +213 -0
- zerochat_runtime/assets/js/providers-webllm.js +486 -0
- zerochat_runtime/assets/js/providers.js +1560 -0
- zerochat_runtime/assets/js/rag-index.js +295 -0
- zerochat_runtime/assets/js/rag-service.js +530 -0
- zerochat_runtime/assets/js/rag-ui.js +997 -0
- zerochat_runtime/assets/js/ragStorage.js +676 -0
- zerochat_runtime/assets/js/sandbox.js +449 -0
- zerochat_runtime/assets/js/state.js +704 -0
- zerochat_runtime/assets/js/storage-db.js +144 -0
- zerochat_runtime/assets/js/tool-cards.js +330 -0
- zerochat_runtime/assets/js/tool-security.js +653 -0
- zerochat_runtime/assets/js/tools/README.md +50 -0
- zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
- zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
- zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
- zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
- zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
- zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
- zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
- zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
- zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
- zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
- zerochat_runtime/assets/js/ui-composer.js +319 -0
- zerochat_runtime/assets/js/ui-conversation.js +655 -0
- zerochat_runtime/assets/js/ui-dialogs.js +132 -0
- zerochat_runtime/assets/js/ui-generation-status.js +119 -0
- zerochat_runtime/assets/js/ui-inspector.js +804 -0
- zerochat_runtime/assets/js/ui-mcp.js +604 -0
- zerochat_runtime/assets/js/ui-profiles.js +824 -0
- zerochat_runtime/assets/js/ui-reasoning.js +146 -0
- zerochat_runtime/assets/js/ui-settings.js +825 -0
- zerochat_runtime/assets/js/ui-shell.js +215 -0
- zerochat_runtime/assets/js/ui-sidebar.js +429 -0
- zerochat_runtime/assets/js/ui-telemetry.js +404 -0
- zerochat_runtime/assets/js/ui-transfer.js +262 -0
- zerochat_runtime/assets/js/utils.js +216 -0
- zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
- zerochat_runtime/assets/js/web-browser.js +569 -0
- zerochat_runtime/assets/js/web-search.js +519 -0
- zerochat_runtime/assets/manifest.webmanifest +20 -0
- zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
- zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
- zerochat_runtime/assets/services/lsp/installer.json +9 -0
- zerochat_runtime/assets/services/lsp/service.json +22 -0
- zerochat_runtime/assets/services/memory/installer.json +9 -0
- zerochat_runtime/assets/services/memory/service.json +24 -0
- zerochat_runtime/assets/services/playwright/installer.json +10 -0
- zerochat_runtime/assets/services/playwright/service.json +39 -0
- zerochat_runtime/assets/sw.js +164 -0
- zerochat_runtime/assets/zerochat.html +671 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Módulo de Seguridad y Autorización de Herramientas (ChatToolSecurity) para ZeroChat.
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidades:
|
|
5
|
+
* - Evaluación de políticas de ejecución para herramientas agénticas y servidores MCP.
|
|
6
|
+
* - Registro persistente de autorizaciones de grano fino por toolId.
|
|
7
|
+
* - Control global de autorización MCP (restringido a la configuración global).
|
|
8
|
+
* - Desacoplamiento e isomorfismo (Node.js y navegador).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
(function (root, factory) {
|
|
12
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
13
|
+
module.exports = factory();
|
|
14
|
+
} else {
|
|
15
|
+
root.ChatToolSecurity = factory();
|
|
16
|
+
}
|
|
17
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const STORAGE_KEY = 'chat_tool_security';
|
|
21
|
+
|
|
22
|
+
const GLOBAL_POLICIES = Object.freeze({
|
|
23
|
+
ASK: 'ask',
|
|
24
|
+
ALLOW_ALL: 'allow_all'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const TOOL_POLICIES = Object.freeze({
|
|
28
|
+
ALLOW: 'allow',
|
|
29
|
+
DENY: 'deny',
|
|
30
|
+
ASK: 'ask'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function resolveDep(name, path) {
|
|
34
|
+
if (typeof window !== 'undefined' && window.ChatUtils?.resolveDep) {
|
|
35
|
+
return window.ChatUtils.resolveDep(name, path);
|
|
36
|
+
}
|
|
37
|
+
if (typeof window !== 'undefined' && window[name]) return window[name];
|
|
38
|
+
if (typeof require !== 'undefined') {
|
|
39
|
+
try { return require(path); } catch (e) { return null; }
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getStorage() {
|
|
45
|
+
return resolveDep('ChatStorage', './cookies.js');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getState() {
|
|
49
|
+
return resolveDep('ChatState', './state.js');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getAgentCore() {
|
|
53
|
+
return resolveDep('ChatAgentCore', './agent-core.js');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizePath(p) {
|
|
57
|
+
if (typeof p !== 'string') return '';
|
|
58
|
+
let norm = p.replace(/\\/g, '/');
|
|
59
|
+
norm = norm.replace(/\/+/g, '/');
|
|
60
|
+
return norm;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isPathTraversal(norm) {
|
|
64
|
+
const segments = norm.split('/');
|
|
65
|
+
let depth = 0;
|
|
66
|
+
for (const seg of segments) {
|
|
67
|
+
if (seg === '..') {
|
|
68
|
+
depth--;
|
|
69
|
+
if (depth < 0) return true;
|
|
70
|
+
} else if (seg && seg !== '.') {
|
|
71
|
+
depth++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function evaluatePathConstraint(pathVal, pathConstraints) {
|
|
78
|
+
if (!pathVal || typeof pathVal !== 'string') return { allowed: true };
|
|
79
|
+
if (!pathConstraints || typeof pathConstraints !== 'object') return { allowed: true };
|
|
80
|
+
|
|
81
|
+
const norm = normalizePath(pathVal);
|
|
82
|
+
|
|
83
|
+
if (pathConstraints.preventTraversal) {
|
|
84
|
+
if (norm.includes('../') || norm.startsWith('..') || norm.endsWith('/..')) {
|
|
85
|
+
if (isPathTraversal(norm)) {
|
|
86
|
+
return {
|
|
87
|
+
allowed: false,
|
|
88
|
+
denied: true,
|
|
89
|
+
reason: 'path_traversal_detected',
|
|
90
|
+
details: `Navegación de directorios no permitida: ${pathVal}`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(pathConstraints.deniedDirectories)) {
|
|
97
|
+
for (const denied of pathConstraints.deniedDirectories) {
|
|
98
|
+
const normDenied = normalizePath(denied);
|
|
99
|
+
if (norm.startsWith(normDenied) || norm === normDenied || norm.includes(normDenied)) {
|
|
100
|
+
return {
|
|
101
|
+
allowed: false,
|
|
102
|
+
denied: true,
|
|
103
|
+
reason: 'path_in_denied_directory',
|
|
104
|
+
details: `Ruta denegada por lista negra: ${pathVal}`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(pathConstraints.allowedDirectories) && pathConstraints.allowedDirectories.length > 0) {
|
|
111
|
+
let matched = false;
|
|
112
|
+
for (const allowed of pathConstraints.allowedDirectories) {
|
|
113
|
+
const normAllowed = normalizePath(allowed);
|
|
114
|
+
if (normAllowed === './' || normAllowed === '.') {
|
|
115
|
+
if (!norm.startsWith('/') && !norm.startsWith('~')) {
|
|
116
|
+
matched = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
} else if (norm.startsWith(normAllowed)) {
|
|
120
|
+
matched = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!matched) {
|
|
125
|
+
return {
|
|
126
|
+
allowed: false,
|
|
127
|
+
denied: false,
|
|
128
|
+
reason: 'path_outside_allowed_directories',
|
|
129
|
+
details: `Ruta fuera de las carpetas autorizadas: ${pathVal}`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { allowed: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function evaluateCommandConstraint(cmdVal, commandConstraints) {
|
|
138
|
+
if (!cmdVal || typeof cmdVal !== 'string') return { allowed: true };
|
|
139
|
+
if (!commandConstraints || typeof commandConstraints !== 'object') return { allowed: true };
|
|
140
|
+
|
|
141
|
+
const trimmed = cmdVal.trim();
|
|
142
|
+
|
|
143
|
+
if (commandConstraints.allowChaining === false) {
|
|
144
|
+
// Encadenamiento peligroso: ejecución secuencial (;), condicional (&&, ||), o sustitución de subshell (` o $()`)
|
|
145
|
+
const isSequentialOrCond = /(?:;|&&|\|\||`|\$\()/.test(trimmed);
|
|
146
|
+
// Background execution: & que NO sea parte de una redirección de descriptor (como 2>&1, >&2, &>, &>>)
|
|
147
|
+
const isBackgroundAmp = /(?<!>|\d)&(?!\d|>)/.test(trimmed);
|
|
148
|
+
|
|
149
|
+
// Si allowPipes es explícitamente false, se prohíben tuberías |. Por defecto o si allowPipes es true, se permiten pipes.
|
|
150
|
+
const hasUnauthorizedPipe = commandConstraints.allowPipes === false && trimmed.includes('|');
|
|
151
|
+
|
|
152
|
+
if (isSequentialOrCond || isBackgroundAmp || hasUnauthorizedPipe) {
|
|
153
|
+
return {
|
|
154
|
+
allowed: false,
|
|
155
|
+
denied: false,
|
|
156
|
+
reason: 'command_chaining_requires_approval',
|
|
157
|
+
details: `Comando contiene encadenamiento o operadores de shell no autorizados: ${trimmed}`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(commandConstraints.deniedPatterns)) {
|
|
163
|
+
for (const pattern of commandConstraints.deniedPatterns) {
|
|
164
|
+
if (typeof pattern === 'string' && trimmed.includes(pattern)) {
|
|
165
|
+
return {
|
|
166
|
+
allowed: false,
|
|
167
|
+
denied: true,
|
|
168
|
+
reason: 'command_matches_denied_pattern',
|
|
169
|
+
details: `Comando contiene un patrón bloqueado (${pattern}): ${trimmed}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Array.isArray(commandConstraints.allowedPrefixes) && commandConstraints.allowedPrefixes.length > 0) {
|
|
176
|
+
let matched = false;
|
|
177
|
+
for (const prefix of commandConstraints.allowedPrefixes) {
|
|
178
|
+
const cleanPrefix = prefix.trim();
|
|
179
|
+
if (
|
|
180
|
+
trimmed === cleanPrefix ||
|
|
181
|
+
trimmed.startsWith(cleanPrefix + ' ') ||
|
|
182
|
+
trimmed.startsWith(cleanPrefix + '\t') ||
|
|
183
|
+
trimmed.startsWith('/usr/bin/' + cleanPrefix + ' ') ||
|
|
184
|
+
trimmed.startsWith('/bin/' + cleanPrefix + ' ') ||
|
|
185
|
+
trimmed.startsWith('/usr/local/bin/' + cleanPrefix + ' ')
|
|
186
|
+
) {
|
|
187
|
+
matched = true;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!matched) {
|
|
192
|
+
return {
|
|
193
|
+
allowed: false,
|
|
194
|
+
denied: false,
|
|
195
|
+
reason: 'command_outside_allowed_prefixes',
|
|
196
|
+
details: `Comando fuera de los prefijos autorizados: ${trimmed}`
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { allowed: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function evaluateConstraints(constraints, args = {}) {
|
|
205
|
+
if (!constraints || typeof constraints !== 'object') {
|
|
206
|
+
return { status: 'allow' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (constraints.command) {
|
|
210
|
+
const cmdVal = args.command || args.cmd || args.script || '';
|
|
211
|
+
if (cmdVal) {
|
|
212
|
+
const cmdRes = evaluateCommandConstraint(cmdVal, constraints.command);
|
|
213
|
+
if (cmdRes.denied) {
|
|
214
|
+
return { status: 'deny', reason: cmdRes.reason, details: cmdRes.details };
|
|
215
|
+
}
|
|
216
|
+
if (!cmdRes.allowed) {
|
|
217
|
+
return { status: 'ask', reason: cmdRes.reason, details: cmdRes.details };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (constraints.path) {
|
|
223
|
+
const pathVal = args.path || args.filepath || args.file || args.directory || args.dir || args.cwd || '';
|
|
224
|
+
if (pathVal) {
|
|
225
|
+
const pathRes = evaluatePathConstraint(pathVal, constraints.path);
|
|
226
|
+
if (pathRes.denied) {
|
|
227
|
+
return { status: 'deny', reason: pathRes.reason, details: pathRes.details };
|
|
228
|
+
}
|
|
229
|
+
if (!pathRes.allowed) {
|
|
230
|
+
return { status: 'ask', reason: pathRes.reason, details: pathRes.details };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { status: 'allow' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Administrador de Seguridad y Políticas de Ejecución.
|
|
240
|
+
*/
|
|
241
|
+
class ToolSecurityManager {
|
|
242
|
+
constructor(options = {}) {
|
|
243
|
+
this.storageKey = options.storageKey || STORAGE_KEY;
|
|
244
|
+
this.globalMcpPolicy = GLOBAL_POLICIES.ASK;
|
|
245
|
+
this.tools = new Map();
|
|
246
|
+
this.listeners = new Set();
|
|
247
|
+
this.load();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Carga el estado persistido desde almacenamiento local.
|
|
252
|
+
*/
|
|
253
|
+
load() {
|
|
254
|
+
try {
|
|
255
|
+
const Storage = getStorage();
|
|
256
|
+
let raw = null;
|
|
257
|
+
if (Storage && typeof Storage.getStorageItem === 'function') {
|
|
258
|
+
raw = Storage.getStorageItem(this.storageKey);
|
|
259
|
+
if (raw === null && typeof Storage.migrateLegacyStorageItem === 'function') {
|
|
260
|
+
raw = Storage.migrateLegacyStorageItem(this.storageKey);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (raw) {
|
|
265
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
266
|
+
if (parsed && typeof parsed === 'object') {
|
|
267
|
+
if (parsed.globalMcpPolicy === GLOBAL_POLICIES.ALLOW_ALL || parsed.globalMcpPolicy === GLOBAL_POLICIES.ASK) {
|
|
268
|
+
this.globalMcpPolicy = parsed.globalMcpPolicy;
|
|
269
|
+
}
|
|
270
|
+
if (parsed.tools && typeof parsed.tools === 'object') {
|
|
271
|
+
this.tools.clear();
|
|
272
|
+
Object.entries(parsed.tools).forEach(([id, item]) => {
|
|
273
|
+
if (item && typeof item === 'object') {
|
|
274
|
+
this.tools.set(id, {
|
|
275
|
+
policy: item.policy || TOOL_POLICIES.ASK,
|
|
276
|
+
grantedAt: item.grantedAt || Date.now(),
|
|
277
|
+
lastUsedAt: item.lastUsedAt || null,
|
|
278
|
+
serverName: item.serverName || '',
|
|
279
|
+
originalName: item.originalName || id,
|
|
280
|
+
constraints: item.constraints || null
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
// En caso de corrupción, mantener estado seguro por defecto
|
|
289
|
+
this.globalMcpPolicy = GLOBAL_POLICIES.ASK;
|
|
290
|
+
this.tools.clear();
|
|
291
|
+
}
|
|
292
|
+
this.syncWithState();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Guarda el estado en almacenamiento local y sincroniza el store reactivo.
|
|
297
|
+
*/
|
|
298
|
+
save() {
|
|
299
|
+
try {
|
|
300
|
+
const Storage = getStorage();
|
|
301
|
+
const toolsObj = {};
|
|
302
|
+
this.tools.forEach((val, key) => {
|
|
303
|
+
toolsObj[key] = val;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const payload = {
|
|
307
|
+
version: 1,
|
|
308
|
+
globalMcpPolicy: this.globalMcpPolicy,
|
|
309
|
+
tools: toolsObj,
|
|
310
|
+
updatedAt: Date.now()
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const serialized = JSON.stringify(payload);
|
|
314
|
+
if (Storage && typeof Storage.setStorageItem === 'function') {
|
|
315
|
+
Storage.setStorageItem(this.storageKey, serialized);
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.warn('[ToolSecurity] Error al persistir políticas de seguridad:', err?.message || err);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.syncWithState();
|
|
322
|
+
this.notifyListeners();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Sincroniza las políticas con ChatState si está disponible.
|
|
327
|
+
*/
|
|
328
|
+
syncWithState() {
|
|
329
|
+
const State = getState();
|
|
330
|
+
if (State && typeof State.set === 'function') {
|
|
331
|
+
const toolsObj = {};
|
|
332
|
+
this.tools.forEach((val, key) => { toolsObj[key] = val; });
|
|
333
|
+
State.set('toolSecurity', {
|
|
334
|
+
globalMcpPolicy: this.globalMcpPolicy,
|
|
335
|
+
authorizedCount: this.tools.size,
|
|
336
|
+
tools: toolsObj
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Registra un observador para cambios en políticas de seguridad.
|
|
343
|
+
*/
|
|
344
|
+
subscribe(listener) {
|
|
345
|
+
if (typeof listener === 'function') {
|
|
346
|
+
this.listeners.add(listener);
|
|
347
|
+
return () => this.listeners.delete(listener);
|
|
348
|
+
}
|
|
349
|
+
return () => {};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
notifyListeners() {
|
|
353
|
+
this.listeners.forEach(fn => {
|
|
354
|
+
try { fn(this); } catch (e) {}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Obtiene la política global para herramientas MCP.
|
|
360
|
+
* @returns {'ask'|'allow_all'}
|
|
361
|
+
*/
|
|
362
|
+
getGlobalMcpPolicy() {
|
|
363
|
+
return this.globalMcpPolicy;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Establece la política global para herramientas MCP.
|
|
368
|
+
* SOLO debe ser invocado desde la interfaz de configuración global de MCP.
|
|
369
|
+
* @param {'ask'|'allow_all'} policy
|
|
370
|
+
*/
|
|
371
|
+
setGlobalMcpPolicy(policy) {
|
|
372
|
+
if (policy !== GLOBAL_POLICIES.ASK && policy !== GLOBAL_POLICIES.ALLOW_ALL) {
|
|
373
|
+
throw new Error(`Política global no válida: ${policy}`);
|
|
374
|
+
}
|
|
375
|
+
this.globalMcpPolicy = policy;
|
|
376
|
+
this.save();
|
|
377
|
+
return this.globalMcpPolicy;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Resuelve la regla de autorización exclusivamente por ID canónico.
|
|
382
|
+
* @param {string} toolIdOrName
|
|
383
|
+
* @param {object|null} [tool=null]
|
|
384
|
+
* @returns {{ toolId: string, entry: object }|null}
|
|
385
|
+
*/
|
|
386
|
+
findToolEntry(toolIdOrName, tool = null) {
|
|
387
|
+
if (!toolIdOrName && !tool) return null;
|
|
388
|
+
|
|
389
|
+
const id = tool ? (tool.id || tool.name) : toolIdOrName;
|
|
390
|
+
if (typeof id === 'string' && this.tools.has(id)) {
|
|
391
|
+
return { toolId: id, entry: this.tools.get(id) };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Obtiene la política específica para una herramienta.
|
|
399
|
+
* @param {string} toolId
|
|
400
|
+
* @returns {'allow'|'deny'|'ask'|null}
|
|
401
|
+
*/
|
|
402
|
+
getToolPolicy(toolId) {
|
|
403
|
+
if (!toolId) return null;
|
|
404
|
+
const found = this.findToolEntry(toolId);
|
|
405
|
+
return found ? found.entry.policy : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Establece la política de grano fino para una herramienta individual.
|
|
410
|
+
* @param {string} toolId - Identificador único de la herramienta (ej: 'zmcp_read_file').
|
|
411
|
+
* @param {'allow'|'deny'|'ask'} policy - Decisión de autorización.
|
|
412
|
+
* @param {object} [meta={}] - Metadatos auxiliares (serverName, originalName, etc.).
|
|
413
|
+
*/
|
|
414
|
+
setToolPolicy(toolId, policy, meta = {}) {
|
|
415
|
+
if (!toolId) return;
|
|
416
|
+
const cleanPolicy = policy === TOOL_POLICIES.ALLOW || policy === TOOL_POLICIES.DENY
|
|
417
|
+
? policy
|
|
418
|
+
: TOOL_POLICIES.ASK;
|
|
419
|
+
|
|
420
|
+
const found = this.findToolEntry(toolId);
|
|
421
|
+
const targetId = found ? found.toolId : toolId;
|
|
422
|
+
const existing = found ? found.entry : (this.tools.get(targetId) || {});
|
|
423
|
+
|
|
424
|
+
this.tools.set(targetId, {
|
|
425
|
+
policy: cleanPolicy,
|
|
426
|
+
grantedAt: existing.grantedAt || Date.now(),
|
|
427
|
+
lastUsedAt: Date.now(),
|
|
428
|
+
serverName: meta.serverName || existing.serverName || '',
|
|
429
|
+
originalName: meta.originalName || existing.originalName || toolId,
|
|
430
|
+
constraints: meta.constraints !== undefined ? meta.constraints : (existing.constraints || null)
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
this.save();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Obtiene los metadatos completos y restricciones de una herramienta registrada.
|
|
438
|
+
* @param {string} toolId
|
|
439
|
+
*/
|
|
440
|
+
getToolEntry(toolId) {
|
|
441
|
+
const found = this.findToolEntry(toolId);
|
|
442
|
+
return found ? found.entry : null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Obtiene las restricciones configuradas para una herramienta.
|
|
447
|
+
* @param {string} toolId
|
|
448
|
+
* @returns {object|null}
|
|
449
|
+
*/
|
|
450
|
+
getToolConstraints(toolId) {
|
|
451
|
+
const found = this.findToolEntry(toolId);
|
|
452
|
+
return found ? (found.entry.constraints || null) : null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Establece o actualiza las restricciones de una herramienta.
|
|
457
|
+
* @param {string} toolId
|
|
458
|
+
* @param {object|null} constraints
|
|
459
|
+
*/
|
|
460
|
+
setToolConstraints(toolId, constraints) {
|
|
461
|
+
const found = this.findToolEntry(toolId);
|
|
462
|
+
if (found) {
|
|
463
|
+
found.entry.constraints = constraints || null;
|
|
464
|
+
this.save();
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Revoca la autorización guardada de una herramienta individual.
|
|
472
|
+
* @param {string} toolId
|
|
473
|
+
*/
|
|
474
|
+
revokeToolPolicy(toolId) {
|
|
475
|
+
if (!toolId) return false;
|
|
476
|
+
const found = this.findToolEntry(toolId);
|
|
477
|
+
if (found) {
|
|
478
|
+
this.tools.delete(found.toolId);
|
|
479
|
+
this.save();
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Restablece todas las autorizaciones guardadas.
|
|
487
|
+
*/
|
|
488
|
+
clearAllAuthorizations() {
|
|
489
|
+
this.tools.clear();
|
|
490
|
+
this.save();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Lista todas las herramientas con autorizaciones individuales registradas.
|
|
495
|
+
* @returns {Array<{ toolId: string, policy: string, grantedAt: number, lastUsedAt?: number, serverName?: string, originalName?: string, constraints?: object }>}
|
|
496
|
+
*/
|
|
497
|
+
listAuthorizedTools() {
|
|
498
|
+
const list = [];
|
|
499
|
+
this.tools.forEach((val, key) => {
|
|
500
|
+
list.push({
|
|
501
|
+
toolId: key,
|
|
502
|
+
...val
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
return list.sort((a, b) => (b.grantedAt || 0) - (a.grantedAt || 0));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Evalúa si una llamada a herramienta puede ejecutarse o requiere autorización del usuario.
|
|
510
|
+
*
|
|
511
|
+
* @param {object|string} toolOrName - Instancia de Tool o nombre de la herramienta.
|
|
512
|
+
* @param {object} [args={}] - Argumentos de la llamada.
|
|
513
|
+
* @param {object} [options={}] - Opciones de contexto.
|
|
514
|
+
* @returns {{ requiresApproval: boolean, status: 'allow'|'deny'|'ask', reason: string, toolId: string, serverName: string, originalName: string, details?: string }}
|
|
515
|
+
*/
|
|
516
|
+
evaluateAuthorization(toolOrName, args = {}, options = {}) {
|
|
517
|
+
let tool = null;
|
|
518
|
+
let toolName = '';
|
|
519
|
+
|
|
520
|
+
if (typeof toolOrName === 'string') {
|
|
521
|
+
toolName = toolOrName;
|
|
522
|
+
const AgentCore = getAgentCore();
|
|
523
|
+
if (AgentCore && AgentCore.registry && typeof AgentCore.registry.getTool === 'function') {
|
|
524
|
+
tool = AgentCore.registry.getTool(toolName);
|
|
525
|
+
}
|
|
526
|
+
} else if (toolOrName && typeof toolOrName === 'object') {
|
|
527
|
+
tool = toolOrName;
|
|
528
|
+
toolName = tool.name || tool.id || '';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const toolId = (tool && (tool.id || tool.name)) || toolName;
|
|
532
|
+
const category = tool?.category || (/^(?:zmcp|mcp)_/.test(toolName) ? 'mcp' : 'other');
|
|
533
|
+
const isMcp = category === 'mcp' || /^(?:zmcp|mcp)_/.test(toolName);
|
|
534
|
+
|
|
535
|
+
const serverName = tool?.metadata?.mcpServerName || '';
|
|
536
|
+
const originalName = tool?.metadata?.originalName || toolName;
|
|
537
|
+
|
|
538
|
+
// 1. Herramientas integradas (no MCP): permitidas por defecto
|
|
539
|
+
if (!isMcp) {
|
|
540
|
+
return {
|
|
541
|
+
requiresApproval: false,
|
|
542
|
+
status: TOOL_POLICIES.ALLOW,
|
|
543
|
+
reason: 'builtin_tool',
|
|
544
|
+
toolId,
|
|
545
|
+
serverName: '',
|
|
546
|
+
originalName: toolName
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 2. Herramientas MCP con modo global 'allow_all'
|
|
551
|
+
if (this.globalMcpPolicy === GLOBAL_POLICIES.ALLOW_ALL) {
|
|
552
|
+
return {
|
|
553
|
+
requiresApproval: false,
|
|
554
|
+
status: TOOL_POLICIES.ALLOW,
|
|
555
|
+
reason: 'mcp_global_allow_all',
|
|
556
|
+
toolId,
|
|
557
|
+
serverName,
|
|
558
|
+
originalName
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 3. Herramientas MCP con regla granular específica guardada (resolución robusta)
|
|
563
|
+
const foundEntry = this.findToolEntry(toolId, tool);
|
|
564
|
+
if (foundEntry) {
|
|
565
|
+
const rule = foundEntry.entry;
|
|
566
|
+
const resolvedToolId = foundEntry.toolId;
|
|
567
|
+
|
|
568
|
+
if (rule.policy === TOOL_POLICIES.ALLOW) {
|
|
569
|
+
// Evaluar restricciones granulares si existen
|
|
570
|
+
if (rule.constraints) {
|
|
571
|
+
const constraintEval = evaluateConstraints(rule.constraints, args);
|
|
572
|
+
if (constraintEval.status === TOOL_POLICIES.DENY) {
|
|
573
|
+
return {
|
|
574
|
+
requiresApproval: false,
|
|
575
|
+
status: TOOL_POLICIES.DENY,
|
|
576
|
+
reason: constraintEval.reason || 'constraint_violation_denied',
|
|
577
|
+
toolId: resolvedToolId,
|
|
578
|
+
serverName,
|
|
579
|
+
originalName,
|
|
580
|
+
details: constraintEval.details
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (constraintEval.status === TOOL_POLICIES.ASK) {
|
|
584
|
+
return {
|
|
585
|
+
requiresApproval: true,
|
|
586
|
+
status: TOOL_POLICIES.ASK,
|
|
587
|
+
reason: constraintEval.reason || 'constraint_outside_scope',
|
|
588
|
+
toolId: resolvedToolId,
|
|
589
|
+
serverName,
|
|
590
|
+
originalName,
|
|
591
|
+
details: constraintEval.details
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Registrar último uso
|
|
597
|
+
rule.lastUsedAt = Date.now();
|
|
598
|
+
return {
|
|
599
|
+
requiresApproval: false,
|
|
600
|
+
status: TOOL_POLICIES.ALLOW,
|
|
601
|
+
reason: 'granular_allow_rule',
|
|
602
|
+
toolId: resolvedToolId,
|
|
603
|
+
serverName,
|
|
604
|
+
originalName,
|
|
605
|
+
constraints: rule.constraints || null
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
if (rule.policy === TOOL_POLICIES.DENY) {
|
|
609
|
+
return {
|
|
610
|
+
requiresApproval: false,
|
|
611
|
+
status: TOOL_POLICIES.DENY,
|
|
612
|
+
reason: 'granular_deny_rule',
|
|
613
|
+
toolId: resolvedToolId,
|
|
614
|
+
serverName,
|
|
615
|
+
originalName
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
// Si explícitamente se configuró como 'ask'
|
|
619
|
+
return {
|
|
620
|
+
requiresApproval: true,
|
|
621
|
+
status: TOOL_POLICIES.ASK,
|
|
622
|
+
reason: 'granular_ask_rule',
|
|
623
|
+
toolId: resolvedToolId,
|
|
624
|
+
serverName,
|
|
625
|
+
originalName
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 4. Por defecto en MCP: solicitar autorización interactiva
|
|
630
|
+
return {
|
|
631
|
+
requiresApproval: true,
|
|
632
|
+
status: TOOL_POLICIES.ASK,
|
|
633
|
+
reason: 'mcp_default_ask',
|
|
634
|
+
toolId,
|
|
635
|
+
serverName,
|
|
636
|
+
originalName
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const manager = new ToolSecurityManager();
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
STORAGE_KEY,
|
|
645
|
+
GLOBAL_POLICIES,
|
|
646
|
+
TOOL_POLICIES,
|
|
647
|
+
ToolSecurityManager,
|
|
648
|
+
evaluatePathConstraint,
|
|
649
|
+
evaluateCommandConstraint,
|
|
650
|
+
evaluateConstraints,
|
|
651
|
+
manager
|
|
652
|
+
};
|
|
653
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Tools de ZeroChat
|
|
2
|
+
|
|
3
|
+
Cada tool nativa vive en `js/tools/builtin/<nombre>.tool.js` y exporta un módulo con `id`, `definition` y `createTool(Tool)`.
|
|
4
|
+
|
|
5
|
+
El contrato obligatorio es:
|
|
6
|
+
|
|
7
|
+
- `definition`: nombre, descripción y JSON Schema de Function Calling.
|
|
8
|
+
- `settings`: descriptor de configuración.
|
|
9
|
+
- `execute(args, context)`: ejecución; consume dependencias mediante `context.services`.
|
|
10
|
+
- `result`: adaptadores `toModel` y/o `toMarkdown`.
|
|
11
|
+
- `view`: tarjeta opcional (`createLiveCard`, `updateLiveCard`, `renderHistoricalCard`).
|
|
12
|
+
|
|
13
|
+
No usar `ui` ni `handler`: fueron eliminados. Para una dependencia nueva, declárala en `js/tools/tool-runtime.js`, inyéctala en pruebas y evita acceder a globals desde `agent-core.js`.
|
|
14
|
+
|
|
15
|
+
Las herramientas de conocimiento local usan cuatro operaciones canónicas: `list_documents`, `search_knowledge_base`, `read_knowledge_chunk` y `read_knowledge_image`. La última recupera bajo demanda una imagen ya extraída del documento; el modelo decide usarla únicamente si puede analizar imágenes. El registro y la ejecución del agente usan exclusivamente las definiciones declaradas por estos módulos.
|
|
16
|
+
|
|
17
|
+
## Ejecución agéntica
|
|
18
|
+
|
|
19
|
+
`AgentRuntime` (`js/agent-core.js`) es el único bucle agéntico. `chat-engine.js` prepara el contexto y adapta sus eventos al DOM, pero no ejecuta iteraciones ni herramientas.
|
|
20
|
+
|
|
21
|
+
Toda ejecución pasa por `ToolExecutor`. Este resuelve la herramienta en `ToolRegistry`, evalúa la política de `ChatToolSecurity` y solo después invoca `tool.execute`. Las herramientas MCP que requieren confirmación se bloquean si no existe una interfaz que pueda recoger una decisión explícita del usuario.
|
|
22
|
+
|
|
23
|
+
## Herramienta de ejecución de código (`execute_javascript`)
|
|
24
|
+
|
|
25
|
+
`execute_javascript` (`js/tools/builtin/execute-javascript.tool.js` y `js/sandbox.js`) es una herramienta diseñada para asistir al modelo en cálculos numéricos, operaciones algorítmicas complejas y procesamiento de datos en tiempo real.
|
|
26
|
+
|
|
27
|
+
- **Mecanismo de ejecución**: Se ejecuta en un Web Worker efímero en un hilo separado del navegador (con fallback controlado en entornos sin soporte de Worker), evitando congelar el hilo principal de la interfaz ante bucles o algoritmos pesados.
|
|
28
|
+
- **Control de tiempo y recursos**: Aplica un límite estricto de tiempo (`timeoutMs`, por defecto 2500ms) que termina forzosamente el Worker (`worker.terminate()`) si se sobrepasa, además de truncar salidas excesivas y limitar las llamadas a consola.
|
|
29
|
+
- **Filtro de seguridad en el Worker**: Dentro del entorno del Worker se neutralizan APIs de red y spawning (`fetch`, `XMLHttpRequest`, `WebSocket`, `importScripts`, `Worker`) para prevenir llamadas externas no intencionadas durante cálculos.
|
|
30
|
+
- **Vigilancia del usuario técnico**: Esta herramienta **no** es un sandbox sellado a nivel de sistema operativo ni una máquina virtual impermeable. Está concebida como una utilidad para agilizar cálculos del modelo con la visibilidad, conocimiento y supervisión activa de un usuario técnico. El usuario puede habilitar o inhabilitar la herramienta en cualquier momento desde la configuración.
|
|
31
|
+
|
|
32
|
+
## Nombres MCP
|
|
33
|
+
|
|
34
|
+
El contrato público es `zmcp_<herramienta>` para herramientas locales y
|
|
35
|
+
`mcp_<servicio>_<herramienta>` para servicios externos, tanto directos como
|
|
36
|
+
agregados por el host. Ejemplo: `mcp_browser_service_browser_navigate`.
|
|
37
|
+
El host publica el nombre definitivo; el proveedor lo valida sin añadir prefijos.
|
|
38
|
+
`metadata.mcpServerId` y `metadata.originalName` conservan la identidad remota.
|
|
39
|
+
Las llamadas del host solo resuelven herramientas actualmente anunciadas.
|
|
40
|
+
|
|
41
|
+
Los componentes se codifican sin pérdida: se conservan `a-y` y `0-9`;
|
|
42
|
+
solo en la herramienta se conserva también `_`. Cualquier otro carácter,
|
|
43
|
+
incluida `z`, se representa como `z<hexadecimal del punto de código>z`.
|
|
44
|
+
Esto distingue mayúsculas, puntuación y límites entre servicio y herramienta
|
|
45
|
+
sin depender del orden de descubrimiento. Los componentes vacíos, los nombres
|
|
46
|
+
públicos de más de 64 caracteres y los duplicados se rechazan explícitamente.
|
|
47
|
+
|
|
48
|
+
El registro no genera alias MCP ni elimina sus guiones bajos para resolverlos.
|
|
49
|
+
Los permisos persistidos se consultan exclusivamente por identificador canónico,
|
|
50
|
+
nunca por nombre original, sufijo o alias. No existe migración de nombres antiguos.
|