terminal-smart-cli 0.98.1 → 0.99.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/README.md +7 -5
- package/bin/ts.js +344 -196
- package/hub/cli.js +255 -0
- package/hub/controle.js +262 -0
- package/hub/dpapi.js +55 -0
- package/hub/estado.js +274 -0
- package/hub/exportar-telas.js +149 -0
- package/hub/hub.js +717 -0
- package/hub/mascara.js +67 -0
- package/hub/parar.js +151 -0
- package/hub/principal.js +5 -0
- package/hub/proto/PROTOCOLO.md +789 -0
- package/hub/proto/ts-proto.js +1643 -0
- package/hub/relay-conexao.js +200 -0
- package/hub/telas-arquivo.js +130 -0
- package/hub/tmux.js +189 -0
- package/hub/vps/README.md +109 -0
- package/hub/vps/tmux-agentes.conf +5 -0
- package/hub/vps/ts-hub-exportar-telas.service +30 -0
- package/hub/vps/ts-hub-parar-consumir +67 -0
- package/hub/vps/ts-hub-parar.path +16 -0
- package/hub/vps/ts-hub-parar.service +23 -0
- package/hub/vps/ts-hub-parar.tmpfiles.conf +10 -0
- package/hub/vps/ts-hub.service +39 -0
- package/hub/vps/ts-tmux.service +19 -0
- package/lib/acp.js +8 -2
- package/lib/agent.js +183 -85
- package/lib/chat-local.js +139 -0
- package/lib/core.js +336 -23
- package/lib/doctor.js +13 -5
- package/lib/i18n.js +69 -31
- package/lib/ia-local.js +388 -0
- package/lib/intelligence-core.js +6 -1
- package/lib/keyring.js +0 -1
- package/lib/meta.js +8 -8
- package/lib/personal-agent-llm.js +99 -26
- package/lib/sentinela.js +2 -2
- package/lib/tools.js +638 -8
- package/package.json +4 -2
package/lib/tools.js
CHANGED
|
@@ -37,7 +37,8 @@ function _snapshot(absPath) {
|
|
|
37
37
|
|
|
38
38
|
// Gate de destrutivo: fonte ÚNICA no núcleo canônico (lib/core.js). Reexportado aqui
|
|
39
39
|
// pra compatibilidade (tools.isDestructive segue funcionando pra quem já importava).
|
|
40
|
-
const
|
|
40
|
+
const core = require('./core');
|
|
41
|
+
const { isDestructive } = core;
|
|
41
42
|
|
|
42
43
|
// `findstr`, `rg` e `grep` usam o código 1 para informar que a busca foi
|
|
43
44
|
// executada corretamente, mas não encontrou ocorrências. Para o harness isso
|
|
@@ -397,18 +398,592 @@ function _abs(p, base) {
|
|
|
397
398
|
// (skill_gerenciar/lembrar passam por aprovação; hooks/mcp são só do humano) —
|
|
398
399
|
// prompt-injection persistente. Recusa com o caminho certo indicado.
|
|
399
400
|
const _TS_HOME = path.join(os.homedir(), '.ts');
|
|
401
|
+
// O caminho é ~/.ts ou fica dentro dela? Mesma régua da leitura: caminho CANÔNICO sem caixa
|
|
402
|
+
// (Windows/macOS) e, para os outros nomes da mesma pasta (\\localhost\C$\…, nome 8.3, junção,
|
|
403
|
+
// unidade mapeada, caixa trocada), a IDENTIDADE (volume + índice) do alvo e de cada pasta acima
|
|
404
|
+
// dele. Antes era texto puro com startsWith: `<home>\.TS\providers.json` passava. Quem chama
|
|
405
|
+
// vai abrir o caminho de qualquer forma, então aqui o stat vale também para UNC.
|
|
406
|
+
function _statId(p) {
|
|
407
|
+
try { const s = fs.statSync(p, { bigint: true }); return s.ino ? s.dev + ':' + s.ino : null; } catch (_) { return null; }
|
|
408
|
+
}
|
|
409
|
+
function _insideTsHome(p) {
|
|
410
|
+
const abs = path.resolve(p);
|
|
411
|
+
const home = _normCase(_canonPath(_TS_HOME));
|
|
412
|
+
const c = _normCase(_canonPath(abs));
|
|
413
|
+
if (c === home || c.startsWith(home + path.sep)) return true;
|
|
414
|
+
const homeId = _statId(_TS_HOME);
|
|
415
|
+
if (!homeId) return false;
|
|
416
|
+
for (let cur = abs; ; ) {
|
|
417
|
+
if (_statId(cur) === homeId) return true;
|
|
418
|
+
const up = path.dirname(cur);
|
|
419
|
+
if (up === cur) break;
|
|
420
|
+
cur = up;
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const _MASK_WRITE_REFUSAL = 'RECUSADO: o texto traz a máscara "' + core.SECRET_MASK + '", que esconde um segredo na leitura — gravar isso trocaria o valor real pela máscara. Edite com editar_arquivo só o trecho que precisa mudar, sem a linha mascarada; se for preciso trocar o segredo, peça ao usuário.';
|
|
400
425
|
function _guardTsHome(p) {
|
|
401
426
|
const abs = path.resolve(p);
|
|
402
|
-
|
|
427
|
+
// _tsSecretMatch: o mesmo ARQUIVO de uma credencial do ts com outro nome fora de ~/.ts
|
|
428
|
+
// (hardlink) — gravar nele reescreveria a credencial
|
|
429
|
+
if (_insideTsHome(abs) || _tsSecretMatch(abs)) {
|
|
403
430
|
return 'RECUSADO: "' + abs + '" está na área de configuração do ts (~/.ts) — ferramentas de arquivo NÃO escrevem aqui. Pra skill use skill_gerenciar; pra memória use lembrar; hooks/mcp/config só o USUÁRIO altera manualmente. Não tente por shell — será recusado também.';
|
|
404
431
|
}
|
|
405
432
|
return null;
|
|
406
433
|
}
|
|
434
|
+
// LEITURA: o que ler_arquivo devolve vira contexto do modelo — e vai pro provedor de IA.
|
|
435
|
+
// Em ~/.ts ficam o token de sessão e o perfil SSH (config.json), as chaves BYOK
|
|
436
|
+
// (providers.json), a auth dos servidores MCP (mcp*.json) e o histórico salvo das sessões
|
|
437
|
+
// (sessions; no CLI, as sessões do `ts agente --continuar` ficam em agente/).
|
|
438
|
+
// Skills e memória continuam legíveis. Compara pelo caminho CANÔNICO, sem caixa no Windows
|
|
439
|
+
// e no macOS (sistemas de arquivos sem distinção de caixa por padrão), e pela identidade do
|
|
440
|
+
// arquivo (outros caminhos para o mesmo conteúdo).
|
|
441
|
+
const _TS_SECRET_READ = (rel) => /^(?:config\.json|providers\.json|mcp[^\\\/]*|sessions|agente)(?:[\\\/]|$)/i.test(rel);
|
|
442
|
+
// Caminho canônico: realpath NATIVO (no Windows expande o nome curto 8.3 — PROVID~1.JSO —,
|
|
443
|
+
// tira o "::$DATA" e devolve a caixa gravada no disco; resolve junções e links), com a versão
|
|
444
|
+
// JS de reserva. Arquivo que não existe: tira o que o Windows ignoraria ao abrir (":stream",
|
|
445
|
+
// pontos e espaços no fim de cada parte).
|
|
446
|
+
function _canonPath(p) {
|
|
447
|
+
const abs = path.resolve(p);
|
|
448
|
+
if (!_remoteUnc(abs)) {
|
|
449
|
+
try { return fs.realpathSync.native(abs); } catch (_) {}
|
|
450
|
+
try { return fs.realpathSync(abs); } catch (_) {}
|
|
451
|
+
}
|
|
452
|
+
if (process.platform !== 'win32') return abs;
|
|
453
|
+
const root = path.parse(abs).root;
|
|
454
|
+
return root + abs.slice(root.length).split(/[\\\/]+/).map(seg => seg.replace(/:.*$/, '').replace(/[. ]+$/, '')).join(path.sep);
|
|
455
|
+
}
|
|
456
|
+
const _normCase = (s) => (process.platform === 'win32' || process.platform === 'darwin') ? s.toLowerCase() : s;
|
|
457
|
+
// O caminho é (ou fica dentro de) um item protegido de ~/.ts? Devolve o caminho canônico ou
|
|
458
|
+
// null. `ids` (opcional) evita reler a pasta quando o chamador testa vários caminhos.
|
|
459
|
+
// fns (opcional): { canon, fid } com memória — a avaliação de um comando de shell testa dezenas
|
|
460
|
+
// de palavras que dividem as mesmas pastas acima (_tsShellCtx).
|
|
461
|
+
function _tsSecretMatch(raw, ids, fns) {
|
|
462
|
+
const canon = (fns && fns.canon) || _canonPath, fid = (fns && fns.fid) || _fileId;
|
|
463
|
+
const abs = canon(raw);
|
|
464
|
+
const rel = path.relative(_normCase(canon(_TS_HOME)), _normCase(abs));
|
|
465
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel) && _TS_SECRET_READ(rel)) return abs;
|
|
466
|
+
// Mesmo arquivo com outro caminho: o compartilhamento administrativo (\\localhost\C$\...,
|
|
467
|
+
// \\127.0.0.1\c$, \\?\UNC\...), unidade mapeada ou subst e hardlink não passam pela
|
|
468
|
+
// comparação de texto — o realpath mantém o UNC. Compara a IDENTIDADE (volume + índice do
|
|
469
|
+
// arquivo) do alvo e de cada pasta acima dele com a dos itens protegidos de ~/.ts.
|
|
470
|
+
const set = ids || _tsSecretIds();
|
|
471
|
+
if (!set.size) return null;
|
|
472
|
+
for (let cur = abs; ; ) {
|
|
473
|
+
const id = fid(cur);
|
|
474
|
+
if (id && set.has(id)) return abs;
|
|
475
|
+
const up = path.dirname(cur);
|
|
476
|
+
if (up === cur) break;
|
|
477
|
+
cur = up;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
function _guardTsSecretRead(p) {
|
|
482
|
+
const raw = path.resolve(p);
|
|
483
|
+
// stream alternativo do NTFS (arquivo:stream, arquivo::$DATA): é outro nome para o conteúdo
|
|
484
|
+
// do arquivo e driblava a comparação de caminho. ler_arquivo não precisa disso em lugar nenhum.
|
|
485
|
+
if (process.platform === 'win32' && raw.slice(path.parse(raw).root.length).includes(':')) {
|
|
486
|
+
return 'RECUSADO: "' + raw + '" usa um stream alternativo do NTFS (nome:stream) — ler_arquivo só lê arquivos pelo nome normal.';
|
|
487
|
+
}
|
|
488
|
+
const abs = _tsSecretMatch(raw);
|
|
489
|
+
if (!abs) return null;
|
|
490
|
+
return 'RECUSADO: "' + abs + '" guarda credenciais do ts (token de sessão, senha SSH, chaves de IA, auth de MCP ou histórico de sessões) — ferramentas de leitura não abrem esse arquivo, porque o conteúdo iria para o modelo. Se o usuário precisar de algo daí, peça que ele consulte manualmente.';
|
|
491
|
+
}
|
|
492
|
+
// Identidade "volume:índice" seguindo links. bigint porque no Windows o índice do NTFS pode
|
|
493
|
+
// passar de 2^53; índice 0 (sistema de arquivos sem índice estável) não identifica nada.
|
|
494
|
+
function _fileId(p) {
|
|
495
|
+
if (_remoteUnc(path.resolve(p))) return null;
|
|
496
|
+
try { const s = fs.statSync(p, { bigint: true }); return s.ino ? s.dev + ':' + s.ino : null; } catch (_) { return null; }
|
|
497
|
+
}
|
|
498
|
+
// Caminho UNC de OUTRA máquina (\\host\share, //host/x, \\?\UNC\host\…): realpath e stat abririam
|
|
499
|
+
// uma conexão SMB/WebDAV com um host tirado do texto do comando — o Windows autentica sozinho
|
|
500
|
+
// (NTLM) nesse host e o processo trava por segundos esperando a rede. As checagens de ~/.ts não
|
|
501
|
+
// tocam nesses caminhos: fica só a comparação de texto. Contam como locais localhost, 127.0.0.1,
|
|
502
|
+
// ::1, o nome desta máquina e o host do próprio ~/.ts (perfil em rede); \\?\C:\ e \\.\C:\ são a
|
|
503
|
+
// unidade local. Outros dispositivos (\\.\pipe\…, \\?\Volume{…}) também ficam sem disco.
|
|
504
|
+
const _LOCAL_UNC_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]', '0--1.ipv6-literal.net']);
|
|
505
|
+
// host do UNC; null = \\?\C:\… ou \\.\C:\… (unidade local); '' = dispositivo sem host
|
|
506
|
+
function _uncHost(abs) {
|
|
507
|
+
const parts = String(abs).slice(2).split(/[\\\/]+/);
|
|
508
|
+
if (parts[0] !== '?' && parts[0] !== '.') return parts[0] || '';
|
|
509
|
+
if (/^[A-Za-z]:$/.test(parts[1] || '')) return null;
|
|
510
|
+
return /^unc$/i.test(parts[1] || '') ? (parts[2] || '') : '';
|
|
511
|
+
}
|
|
512
|
+
for (const h of [os.hostname(), /^[\\\/]{2}/.test(_TS_HOME) ? _uncHost(_TS_HOME) : '']) if (h) _LOCAL_UNC_HOSTS.add(String(h).toLowerCase());
|
|
513
|
+
function _remoteUnc(abs) {
|
|
514
|
+
if (process.platform !== 'win32' || !/^[\\\/]{2}/.test(String(abs))) return false;
|
|
515
|
+
const host = _uncHost(abs);
|
|
516
|
+
return host !== null && !_LOCAL_UNC_HOSTS.has(host.toLowerCase());
|
|
517
|
+
}
|
|
518
|
+
// Leitura do ler_arquivo pelo DESCRITOR aberto. Confere a identidade (volume:índice) do arquivo
|
|
519
|
+
// aberto e de cada pasta acima dele contra os itens protegidos de ~/.ts ANTES de ler: pega
|
|
520
|
+
// qualquer apelido do mesmo arquivo que o _guardTsSecretRead não reconhece pelo caminho — UNC
|
|
521
|
+
// com o IP de rede desta máquina, 127.x, nome completo (FQDN), unidade mapeada — e fecha a janela
|
|
522
|
+
// entre checar e ler. Aqui não há rede a evitar: o arquivo vai ser aberto de qualquer forma.
|
|
523
|
+
// Devolve o texto ou { erro }.
|
|
524
|
+
function _readTextNotTsSecret(p) {
|
|
525
|
+
const ids = _tsSecretIds();
|
|
526
|
+
const recusa = () => ({ erro: 'RECUSADO: "' + path.resolve(p) + '" é o mesmo arquivo de um item de ~/.ts que guarda credenciais do ts — ferramentas de leitura não abrem esse arquivo, porque o conteúdo iria para o modelo. Se o usuário precisar de algo daí, peça que ele consulte manualmente.' });
|
|
527
|
+
const fd = fs.openSync(p, 'r');
|
|
528
|
+
try {
|
|
529
|
+
if (ids.size) {
|
|
530
|
+
const s = fs.fstatSync(fd, { bigint: true });
|
|
531
|
+
if (s.ino && ids.has(s.dev + ':' + s.ino)) return recusa();
|
|
532
|
+
for (let cur = path.dirname(path.resolve(p)); ; ) {
|
|
533
|
+
try { const d = fs.statSync(cur, { bigint: true }); if (d.ino && ids.has(d.dev + ':' + d.ino)) return recusa(); } catch (_) {}
|
|
534
|
+
const up = path.dirname(cur);
|
|
535
|
+
if (up === cur) break;
|
|
536
|
+
cur = up;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return fs.readFileSync(fd, 'utf8');
|
|
540
|
+
} finally {
|
|
541
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function _tsSecretIds() {
|
|
545
|
+
const ids = new Set();
|
|
546
|
+
let nomes = [];
|
|
547
|
+
try { nomes = fs.readdirSync(_TS_HOME); } catch (_) { return ids; }
|
|
548
|
+
for (const n of nomes) {
|
|
549
|
+
if (!_TS_SECRET_READ(n)) continue;
|
|
550
|
+
const id = _fileId(path.join(_TS_HOME, n));
|
|
551
|
+
if (id) ids.add(id);
|
|
552
|
+
}
|
|
553
|
+
return ids;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── SEGREDOS CONHECIDOS: rede de segurança por VALOR ────────────────────────
|
|
557
|
+
// As recusas de leitura acima fecham os caminhos conhecidos; sempre vai existir outro (grep
|
|
558
|
+
// recursivo no home, tar | base64, cópia antiga, um link). O ts conhece os próprios segredos:
|
|
559
|
+
// o token de sessão e a senha SSH (config.json), as chaves BYOK (providers.json) e a auth dos
|
|
560
|
+
// servidores MCP (mcp*.json). Carrega os VALORES (8+ caracteres) e toda saída de ferramenta
|
|
561
|
+
// passa pelo mascarador do núcleo (core.buildSecretMasker: o valor e as variações óbvias, como
|
|
562
|
+
// base64 e hexadecimal) antes de virar contexto do modelo. Os valores ficam só na memória do
|
|
563
|
+
// processo: nunca são impressos nem registrados, e nada aqui os manda para fora.
|
|
564
|
+
const _SECRET_FIELD_RE = /(?:^|_)(?:pass|passwd|password|pwd|senha|secret|token|key|apikey|auth|authorization|credential|credentials)$/;
|
|
565
|
+
const _secretFieldName = (k) => _SECRET_FIELD_RE.test(String(k).replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase());
|
|
566
|
+
function _collectSecretValues(node, out, field, depth) {
|
|
567
|
+
if (depth > 8 || out.length > 500) return;
|
|
568
|
+
if (typeof node === 'string') {
|
|
569
|
+
if (field != null && _secretFieldName(field)) {
|
|
570
|
+
out.push(node);
|
|
571
|
+
// auth de MCP: "Header-Name: valor" ou "Bearer x" — o valor sozinho também é segredo
|
|
572
|
+
const header = node.match(/^[A-Za-z0-9-]+\s*:\s*(\S.*)$/);
|
|
573
|
+
if (header) out.push(header[1]);
|
|
574
|
+
const scheme = node.match(/^(?:[A-Za-z0-9-]+\s*:\s*)?(?:Bearer|Basic|Token|Bot)\s+(\S.*)$/i);
|
|
575
|
+
if (scheme) out.push(scheme[1]);
|
|
576
|
+
}
|
|
577
|
+
// URL com credencial (endpoint de MCP, baseUrl): senha do usuário:senha@ e parâmetros
|
|
578
|
+
// com nome de segredo (?token=…, ?api_key=…)
|
|
579
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node)) {
|
|
580
|
+
try {
|
|
581
|
+
const u = new URL(node);
|
|
582
|
+
if (u.password) out.push(decodeURIComponent(u.password));
|
|
583
|
+
for (const [k, v] of u.searchParams) if (_secretFieldName(k)) out.push(v);
|
|
584
|
+
} catch (_) {}
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (Array.isArray(node)) { for (const x of node) _collectSecretValues(x, out, field, depth + 1); return; }
|
|
589
|
+
if (node && typeof node === 'object') for (const k of Object.keys(node)) _collectSecretValues(node[k], out, k, depth + 1);
|
|
590
|
+
}
|
|
591
|
+
const _KNOWN_SECRET_FILE = (n) => /^(?:config|providers)\.json$/i.test(n) || /^mcp[^\\\/]*\.json$/i.test(n);
|
|
592
|
+
let _knownCache = { sig: null, values: [] };
|
|
593
|
+
// Valores dos segredos do ts. Releitura só quando algum arquivo muda (nome:mtime:tamanho).
|
|
594
|
+
function knownSecretValues() {
|
|
595
|
+
let nomes = [];
|
|
596
|
+
try { nomes = fs.readdirSync(_TS_HOME).filter(_KNOWN_SECRET_FILE).sort(); } catch (_) { nomes = []; }
|
|
597
|
+
const sig = nomes.map(n => {
|
|
598
|
+
try { const s = fs.statSync(path.join(_TS_HOME, n)); return s.isFile() && s.size <= 4 * 1024 * 1024 ? `${n}:${s.mtimeMs}:${s.size}` : ''; } catch (_) { return ''; }
|
|
599
|
+
}).join('|');
|
|
600
|
+
if (sig === _knownCache.sig) return _knownCache.values;
|
|
601
|
+
const out = [];
|
|
602
|
+
for (const n of nomes) {
|
|
603
|
+
try {
|
|
604
|
+
const p = path.join(_TS_HOME, n);
|
|
605
|
+
if (fs.statSync(p).size > 4 * 1024 * 1024) continue;
|
|
606
|
+
_collectSecretValues(JSON.parse(fs.readFileSync(p, 'utf8')), out, null, 0);
|
|
607
|
+
} catch (_) { /* arquivo ausente ou inválido: nada a carregar dele */ }
|
|
608
|
+
}
|
|
609
|
+
_knownCache = { sig, values: [...new Set(out.filter(v => typeof v === 'string' && v.trim().length >= core.SECRET_MIN_LEN))] };
|
|
610
|
+
return _knownCache.values;
|
|
611
|
+
}
|
|
612
|
+
// Valores dos segredos do ts + extra: segredos da execução que não estão em arquivo (o token
|
|
613
|
+
// passado ao executor, a chave de IA em uso). Aceita string ou o objeto de credencial ({ key }).
|
|
614
|
+
// É o que vai em core.redactForApproval({ known }) no comando mostrado para aprovação.
|
|
615
|
+
function knownSecretsFor(extra) {
|
|
616
|
+
const ext = (Array.isArray(extra) ? extra : [extra])
|
|
617
|
+
.map(v => (v && typeof v === 'object' ? v.key : v))
|
|
618
|
+
.filter(v => typeof v === 'string' && v.trim().length >= core.SECRET_MIN_LEN);
|
|
619
|
+
return knownSecretValues().concat(ext);
|
|
620
|
+
}
|
|
621
|
+
let _maskerCache = { key: null, masker: null };
|
|
622
|
+
// Mascara (folha a folha, sem alterar a entrada) toda ocorrência de um segredo conhecido.
|
|
623
|
+
function maskKnownSecrets(value, extra) {
|
|
624
|
+
const vals = knownSecretsFor(extra);
|
|
625
|
+
const key = vals.join('\u0000');
|
|
626
|
+
if (_maskerCache.key !== key) _maskerCache = { key, masker: core.buildSecretMasker(vals) };
|
|
627
|
+
return _maskerCache.masker.size ? _maskerCache.masker.deep(value) : value;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ── LEITURA das credenciais do ts POR SHELL ──────────────────────────────────
|
|
631
|
+
// O que ler_arquivo recusa também não sai por executar_comando. Um trecho do comando que CITA
|
|
632
|
+
// ou recebe como argumento um item protegido de ~/.ts (config.json, providers.json, mcp*,
|
|
633
|
+
// sessions, agente) — ou a pasta ~/.ts inteira — é RECUSADO, seja qual for o programa: cat, type,
|
|
634
|
+
// Get-Content, jq, node -p "require(…)", tar | base64, zip, Compress-Archive… Antes só uma lista
|
|
635
|
+
// de leitores conhecidos era barrada e `jq . ~/.ts/providers.json` passava sem aprovação,
|
|
636
|
+
// inclusive em --yes. Fica livre só o que não lê o conteúdo (_tsSegmentNonRead): listar ou ver
|
|
637
|
+
// metadados (ls, dir, Get-ChildItem, stat, find sem leitor no -exec), criar e apagar (o apagar
|
|
638
|
+
// recursivo segue no gate de destrutivo), mudar permissão, ecoar o caminho, `sed -i` (escrita no
|
|
639
|
+
// lugar) e a cópia/movimento cuja ORIGEM não é protegida — copiar PARA dentro de ~/.ts é escrita
|
|
640
|
+
// e cai no gate de ~/.ts (touchesTsConfig), como `> ~/.ts/mcp.json`.
|
|
641
|
+
// Duas checagens em cada trecho: o TEXTO (".ts/" seguido de item protegido, de curinga ou do fim
|
|
642
|
+
// do caminho, depois de um prefixo que é a pasta pessoal — ~, $HOME, %USERPROFILE%,
|
|
643
|
+
// $env:USERPROFILE, /home/<usuário>, C:\Users\<usuário>; ver _tsTextCitesSecret) e cada argumento
|
|
644
|
+
// EXPANDIDO e resolvido (um `cd` anterior na mesma linha conta) pela régua do ler_arquivo:
|
|
645
|
+
// caminho canônico e identidade (nome 8.3, UNC local, link). A busca RECURSIVA que entraria em
|
|
646
|
+
// ~/.ts (grep -r, rg --hidden, findstr /s, Get-ChildItem -Recurse | Select-String, find -exec)
|
|
647
|
+
// também é recusada, com a dica de excluir a pasta. Nada disso cobre todo jeito de ler um arquivo
|
|
648
|
+
// por shell: a saída de qualquer comando ainda passa por redactSecrets e pela máscara dos
|
|
649
|
+
// segredos conhecidos (maskKnownSecrets).
|
|
650
|
+
const _TS_READERS = new Set(['cat', 'tac', 'nl', 'type', 'more', 'less', 'head', 'tail', 'get-content', 'gc', 'findstr', 'select-string', 'sls', 'grep', 'egrep', 'fgrep', 'rg', 'awk', 'sed', 'strings', 'xxd', 'od', 'hexdump', 'base64']);
|
|
651
|
+
// cópia e movimento: só a ORIGEM conta
|
|
652
|
+
const _TS_COPIERS = new Set(['copy', 'xcopy', 'robocopy', 'cp', 'copy-item', 'cpi', 'mv', 'move', 'move-item', 'mi', 'ren', 'rename', 'rename-item', 'rni']);
|
|
653
|
+
const _TS_CD = new Set(['cd', 'chdir', 'pushd', 'set-location', 'sl', 'push-location']);
|
|
654
|
+
// no PowerShell o pipe passa o ARQUIVO, não o texto: `Get-ChildItem ~/.ts/sessions | Get-Content`
|
|
655
|
+
const _TS_PIPE_READERS = new Set(['get-content', 'gc', 'select-string', 'sls', 'copy-item', 'cpi']);
|
|
656
|
+
// programas que leem, transformam ou transmitem conteúdo: no mesmo trecho de um comando que
|
|
657
|
+
// "não lê" (find … -exec cat, ls $(cat …), xargs cat), tiram a isenção
|
|
658
|
+
const _TS_CONTENT_CMDS = new Set([..._TS_READERS, 'node', 'nodejs', 'deno', 'bun', 'python', 'python3', 'py', 'perl', 'ruby', 'php', 'lua',
|
|
659
|
+
'powershell', 'pwsh', 'cmd', 'bash', 'sh', 'zsh', 'dash', 'ksh', 'fish', 'wsl', 'jq', 'yq', 'tar', 'zip', '7z', '7za', 'gzip', 'bzip2', 'xz', 'zstd',
|
|
660
|
+
'compress-archive', 'certutil', 'openssl', 'gpg', 'curl', 'wget', 'iwr', 'irm', 'invoke-webrequest', 'invoke-restmethod', 'scp', 'rsync',
|
|
661
|
+
'diff', 'cmp', 'sort', 'uniq', 'cut', 'tr', 'iconv', 'vim', 'vi', 'nano', 'code', 'notepad', 'get-filehash', 'md5sum', 'sha256sum', 'import-csv']);
|
|
662
|
+
// o que pode citar um item protegido de ~/.ts sem ler o conteúdo dele
|
|
663
|
+
const _TS_NONREAD = new Set(['ls', 'dir', 'gci', 'get-childitem', 'tree', 'stat', 'du', 'file', 'find', 'where', 'which', 'test-path', 'get-item', 'gi',
|
|
664
|
+
'get-acl', 'resolve-path', 'rvpa', 'realpath', 'readlink', 'basename', 'dirname', 'echo', 'printf', 'write-host', 'mkdir', 'md', 'new-item', 'ni',
|
|
665
|
+
'touch', 'rm', 'del', 'erase', 'rd', 'rmdir', 'remove-item', 'ri', 'chmod', 'chown', 'chgrp', 'chattr', 'icacls', 'attrib', 'takeown']);
|
|
666
|
+
const _TS_CODE_READ_RE = /\b(?:readFileSync|readFile|ReadAllText|ReadAllBytes|ReadAllLines|OpenText|OpenRead|ReadToEnd|open|require|import)\s*\(/i;
|
|
667
|
+
// prefixos que só executam o comando seguinte
|
|
668
|
+
const _TS_WRAPPERS = new Set(['sudo', 'nohup', 'time', 'command', 'builtin', 'exec']);
|
|
669
|
+
// ".ts" CITADO no texto como a pasta do ts: seguido de separador e item protegido (grupo 1), de
|
|
670
|
+
// um nome com curinga (grupo 2: *.json, {config,providers}.json) ou do fim do caminho (a pasta
|
|
671
|
+
// inteira: grep -r, findstr /s, xcopy). Quem decide se é caminho é o que vem ANTES, em
|
|
672
|
+
// _tsTextCitesSecret: ".ts" também é a extensão do TypeScript e "\" também é o escape de regex.
|
|
673
|
+
const _TS_TEXT_RE = /\.ts(?:[\\\/]+(?:(config\.json|providers\.json|mcp|sessions|agente)|[^\s\\\/"';|&]*([*?[{]))|[\\\/]*(?=$|[\s"';|&)]))/gi;
|
|
674
|
+
const _TS_TEXT_DELIM = /[\s"'`=(<>,;|&]/;
|
|
675
|
+
// raiz de pasta: ~, %USERPROFILE% (%HOMEDRIVE%%HOMEPATH%), $HOME, ${HOME}, $env:USERPROFILE, C:
|
|
676
|
+
const _TS_ROOT_VAR = String.raw`(?:~|%[A-Za-z_][\w()]*%|\$\{?(?:env:)?[A-Za-z_]\w*\}?|[A-Za-z]:)`;
|
|
677
|
+
const _TS_ROOT_VAR_ONLY_RE = new RegExp(String.raw`^${_TS_ROOT_VAR}+$`);
|
|
678
|
+
// nome de pasta: sem metacaractere de regex (o prefixo já para nos delimitadores acima)
|
|
679
|
+
const _TS_DIR_NAME = String.raw`[^\\\/*?^()[\]{}+]+`;
|
|
680
|
+
// antes de "/.ts": raiz (acima, . ou ..) ou caminho absoluto do Unix, com qualquer separador
|
|
681
|
+
const _TS_DIR_ANY_RE = new RegExp(String.raw`^(?:(?:${_TS_ROOT_VAR}+|\.{1,2})(?=[\\\/]|$)|(?=[\\\/]))(?:[\\\/]+${_TS_DIR_NAME})*$`);
|
|
682
|
+
// antes de "\.ts": caminho Windows com raiz de verdade (acima ou \\host) e só "\" como separador
|
|
683
|
+
const _TS_DIR_WIN_RE = new RegExp(String.raw`^(?:${_TS_ROOT_VAR}+|\\\\[^\\\/\s]+)(?:\\+${_TS_DIR_NAME})*$`);
|
|
684
|
+
// pasta pessoal de QUALQUER usuário (outro perfil também guarda credenciais do ts)
|
|
685
|
+
const _TS_HOME_LIKE_RE = /(?:^|[\\\/])(?:Users|home|Documents and Settings)[\\\/]+[^\\\/]+[\\\/]*$|^[\\\/]+(?:root|var[\\\/]+root)[\\\/]*$/i;
|
|
686
|
+
// O prefixo antes de "/.ts" é uma pasta pessoal? Variável de raiz sozinha (~, $HOME,
|
|
687
|
+
// %USERPROFILE%… — uma variável desconhecida também conta, por cautela), ~usuario, /home/<x>,
|
|
688
|
+
// C:\Users\<x>, /root, ou um caminho que expande para a pasta pessoal deste usuário. Pasta
|
|
689
|
+
// ".ts" de PROJETO (docs/.ts, C:/Users/x/proj/.ts) não é: quem pega o caso real é o argumento
|
|
690
|
+
// expandido (_tsShellWordIsSecret).
|
|
691
|
+
function _tsHomeLike(prefix, cwd) {
|
|
692
|
+
const p = String(prefix || '');
|
|
693
|
+
if (_TS_ROOT_VAR_ONLY_RE.test(p) && !/^[A-Za-z]:$/.test(p)) return true;
|
|
694
|
+
if (/^~[\w.-]+$/.test(p) || _TS_HOME_LIKE_RE.test(p)) return true;
|
|
695
|
+
const abs = _expandShellPath(p, cwd || process.cwd());
|
|
696
|
+
if (!abs || _remoteUnc(abs)) return false;
|
|
697
|
+
return _normCase(_canonPath(abs)) === _normCase(_canonPath(os.homedir()));
|
|
698
|
+
}
|
|
699
|
+
// O texto cita ~/.ts/{item protegido}, um curinga lá dentro ou a pasta inteira?
|
|
700
|
+
// - nada antes (início, espaço, aspas, =, (, <): ".ts/providers.json" relativo — item ou curinga,
|
|
701
|
+
// quando a pasta de trabalho é a pessoal (senão é uma pasta .ts do projeto);
|
|
702
|
+
// - "/" antes: depois de uma pasta pessoal (~/.ts, $HOME/.ts, /home/x/.ts, C:/Users/x/.ts); sem
|
|
703
|
+
// prefixo, o item protegido ou o curinga ('/.ts/providers.json' concatenado num código). Assim
|
|
704
|
+
// sed 's/\.js$/.ts/' e docs/.ts/config.json não são a pasta do ts;
|
|
705
|
+
// - "\" antes: depois de um caminho Windows com raiz que é pasta pessoal (%USERPROFILE%\.ts,
|
|
706
|
+
// $env:USERPROFILE\.ts, C:\Users\x\.ts, \\host\c$\Users\x\.ts); sem prefixo, só o item protegido
|
|
707
|
+
// com "\" também DEPOIS ('\.ts\providers.json' concatenado). "\.ts/mcp", grep "utils\.ts",
|
|
708
|
+
// rg "\w\.ts", "/utils\.ts", awk '/\.ts/' e "\.ts/[a-z]*" são regex. A recusa é definitiva,
|
|
709
|
+
// então na dúvida o texto fica livre: o argumento expandido e resolvido (_tsShellWordIsSecret)
|
|
710
|
+
// continua pegando o caminho real em qualquer forma.
|
|
711
|
+
function _tsTextCitesSecret(text, cwd) {
|
|
712
|
+
const s = String(text || '');
|
|
713
|
+
for (const m of s.matchAll(_TS_TEXT_RE)) {
|
|
714
|
+
const named = m[1] != null, glob = m[2] != null;
|
|
715
|
+
let j = m.index;
|
|
716
|
+
while (j > 0 && (s[j - 1] === '\\' || s[j - 1] === '/')) j--;
|
|
717
|
+
const sep = s.slice(j, m.index);
|
|
718
|
+
let k = j;
|
|
719
|
+
while (k > 0 && !_TS_TEXT_DELIM.test(s[k - 1])) k--;
|
|
720
|
+
const prefix = s.slice(k, j).replace(/^(?:-{1,2}[A-Za-z][\w-]*:|@)/, ''); // -Path:C:\…, curl @arquivo
|
|
721
|
+
const after = s[m.index + 3] || ''; // separador logo depois de ".ts"
|
|
722
|
+
let ok;
|
|
723
|
+
if (!sep) ok = !prefix && (named || glob) && (cwd == null || _tsHomeLike('.', cwd));
|
|
724
|
+
else if (sep.includes('\\')) ok = prefix ? (_TS_DIR_WIN_RE.test(prefix) && _tsHomeLike(prefix, cwd)) : (named && after === '\\');
|
|
725
|
+
else ok = prefix ? (_TS_DIR_ANY_RE.test(prefix) && _tsHomeLike(prefix, cwd)) : (named || glob);
|
|
726
|
+
if (ok) return true;
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
const _TS_SHELL_READ_REFUSAL = 'RECUSADO: o comando lê credenciais do ts em ~/.ts (config.json, providers.json, mcp*, sessions ou agente: token de sessão, senha SSH, chaves de IA, auth de MCP, histórico de sessões). A saída iria para o modelo — nem ler_arquivo nem comandos (leitura, cópia, compactação, interpretadores) abrem esses arquivos. Se o usuário precisar de algo daí, peça que ele consulte manualmente; para ver só os nomes, use listar_diretorio.';
|
|
731
|
+
const _tsRecursiveRefusal = (dir) => 'RECUSADO: a busca recursiva a partir de "' + dir + '" entraria em ~/.ts, onde ficam as credenciais do ts (token de sessão, chaves de IA, auth de MCP, histórico de sessões) — a saída iria para o modelo. Busque numa pasta abaixo (a do projeto) ou exclua a pasta do ts: grep -r --exclude-dir=.ts …, rg -g "!.ts" …, find … -path "*/.ts" -prune -o ….';
|
|
732
|
+
|
|
733
|
+
// Trechos do comando, divididos em ; & | e quebra de linha FORA de aspas. pipe=true quando o
|
|
734
|
+
// trecho veio depois de um `|` simples.
|
|
735
|
+
function _shellSegments(cmd) {
|
|
736
|
+
const out = []; const s = String(cmd);
|
|
737
|
+
let cur = '', q = null, pipe = false;
|
|
738
|
+
for (let i = 0; i < s.length; i++) {
|
|
739
|
+
const ch = s[i];
|
|
740
|
+
if (q) { cur += ch; if (ch === q) q = null; continue; }
|
|
741
|
+
if (ch === '"' || ch === "'") { q = ch; cur += ch; continue; }
|
|
742
|
+
if (ch === ';' || ch === '&' || ch === '|' || ch === '\n' || ch === '\r') {
|
|
743
|
+
out.push({ text: cur, pipe });
|
|
744
|
+
cur = '';
|
|
745
|
+
const dbl = (ch === '&' || ch === '|') && s[i + 1] === ch;
|
|
746
|
+
pipe = ch === '|' && !dbl;
|
|
747
|
+
if (dbl) i++;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
cur += ch;
|
|
751
|
+
}
|
|
752
|
+
out.push({ text: cur, pipe });
|
|
753
|
+
return out;
|
|
754
|
+
}
|
|
755
|
+
// Palavras do trecho. top: o conteúdo entre aspas vale inteiro (posição dos argumentos);
|
|
756
|
+
// flat: o conteúdo entre aspas também é quebrado em palavras (`powershell -Command "gc …"`).
|
|
757
|
+
// Abertura de subcomando ($(, `, () fica fora da palavra: `ls $(cat x)` tem a palavra "cat".
|
|
758
|
+
function _shellWords(seg) {
|
|
759
|
+
const top = [], flat = [];
|
|
760
|
+
const re = /"([^"]*)"|'([^']*)'|([^\s"']+)/g;
|
|
761
|
+
const clean = (w) => w.replace(/^(?:[@(`]|\$\()+|[;,)`]+$/g, '');
|
|
762
|
+
let m;
|
|
763
|
+
while ((m = re.exec(seg))) {
|
|
764
|
+
const q = m[1] != null ? m[1] : m[2];
|
|
765
|
+
if (q != null) {
|
|
766
|
+
if (q) top.push(q);
|
|
767
|
+
for (const w of q.split(/\s+/)) if (clean(w)) flat.push(clean(w));
|
|
768
|
+
} else if (clean(m[3])) { top.push(clean(m[3])); flat.push(clean(m[3])); }
|
|
769
|
+
}
|
|
770
|
+
return { top, flat };
|
|
771
|
+
}
|
|
772
|
+
const _shellCmdName = (w) => String(w).split(/[\\\/]/).pop().toLowerCase().replace(/\.(?:exe|com)$/, '');
|
|
773
|
+
// Argumento → caminho absoluto, expandindo %VAR%, $env:VAR, $HOME, $VAR e ~ (como o shell faria).
|
|
774
|
+
function _expandShellPath(w, cwd) {
|
|
775
|
+
let t = String(w);
|
|
776
|
+
const named = t.match(/^-{1,2}[A-Za-z][\w-]*[=:](.+)$/); // --arquivo=~/.ts/x, -Path:C:\x
|
|
777
|
+
if (named) t = named[1];
|
|
778
|
+
const env = (n) => (process.env[n] != null ? process.env[n] : null);
|
|
779
|
+
t = t.replace(/%([A-Za-z_][\w()]*)%/g, (m, n) => env(n) ?? m)
|
|
780
|
+
.replace(/\$\{?env:([A-Za-z_]\w*)\}?/gi, (m, n) => env(n) ?? m)
|
|
781
|
+
.replace(/\$\{?HOME\}?(?!\w)/gi, () => os.homedir())
|
|
782
|
+
.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (m, n) => env(n) ?? m);
|
|
783
|
+
if (/^~(?=$|[\\\/])/.test(t)) t = os.homedir() + t.slice(1);
|
|
784
|
+
try { return path.resolve(cwd, t); } catch (_) { return null; }
|
|
785
|
+
}
|
|
786
|
+
function _tsShellCtx() {
|
|
787
|
+
// realpath e stat com memória nesta avaliação: as palavras de um comando dividem as pastas acima
|
|
788
|
+
const memo = (fn) => { const m = new Map(); return (p) => { if (!m.has(p)) m.set(p, fn(p)); return m.get(p); }; };
|
|
789
|
+
const canon = memo(_canonPath), fid = memo(_fileId);
|
|
790
|
+
const ctx = { ids: _tsSecretIds(), home: _normCase(canon(_TS_HOME)), homeId: fid(_TS_HOME), _up: null, canon, fid };
|
|
791
|
+
// identidade de ~/.ts e de cada pasta acima dela (raiz de busca recursiva com outro nome)
|
|
792
|
+
ctx.upIds = () => {
|
|
793
|
+
if (ctx._up) return ctx._up;
|
|
794
|
+
ctx._up = new Set();
|
|
795
|
+
for (let cur = _TS_HOME; ; ) {
|
|
796
|
+
const id = ctx.fid(cur);
|
|
797
|
+
if (id) ctx._up.add(id);
|
|
798
|
+
const up = path.dirname(cur);
|
|
799
|
+
if (up === cur) break;
|
|
800
|
+
cur = up;
|
|
801
|
+
}
|
|
802
|
+
return ctx._up;
|
|
803
|
+
};
|
|
804
|
+
return ctx;
|
|
805
|
+
}
|
|
806
|
+
const _isTsHomeDir = (abs, ctx) => _normCase(ctx.canon(abs)) === ctx.home || (!!ctx.homeId && ctx.fid(abs) === ctx.homeId);
|
|
807
|
+
// Base de um caminho com curinga (*.json, {a,b}): a pasta onde ele começa. O "?" de \\?\ e o "."
|
|
808
|
+
// de \\.\ (prefixos de caminho do Windows) não são curinga.
|
|
809
|
+
function _globBase(abs) {
|
|
810
|
+
const off = /^\\\\[?.]\\/.test(abs) ? 4 : 0;
|
|
811
|
+
const g = abs.slice(off).search(/[*?[{]/);
|
|
812
|
+
return g >= 0 ? abs.slice(0, off + g).replace(/[^\\\/]*$/, '') : abs;
|
|
813
|
+
}
|
|
814
|
+
function _tsShellWordIsSecret(w, cwd, ctx) {
|
|
815
|
+
if (_tsTextCitesSecret(w, cwd)) return true;
|
|
816
|
+
const abs = _expandShellPath(w, cwd);
|
|
817
|
+
// UNC de outra máquina: só o texto vale (nenhum realpath/stat abre conexão com esse host)
|
|
818
|
+
if (!abs || _remoteUnc(abs)) return false;
|
|
819
|
+
const base = _globBase(abs);
|
|
820
|
+
if (base !== abs) return !!base && (_isTsHomeDir(base, ctx) || !!_tsSecretMatch(base, ctx.ids, ctx));
|
|
821
|
+
return _isTsHomeDir(abs, ctx) || !!_tsSecretMatch(abs, ctx.ids, ctx);
|
|
822
|
+
}
|
|
823
|
+
// A pasta é ~/.ts ou fica ACIMA dela (uma busca recursiva a partir daí entra em ~/.ts)?
|
|
824
|
+
// posixGlob: o curinga é expandido no estilo do shell POSIX (grep, rg, find), em que "*" não pega
|
|
825
|
+
// nome começado por ponto — `grep -r x *` na pasta pessoal não entra em .ts; só entra se a base
|
|
826
|
+
// do curinga estiver ACIMA da pasta pessoal (C:/Users/* → C:/Users/<você>/.ts).
|
|
827
|
+
function _tsRootCoversTsHome(w, cwd, ctx, posixGlob) {
|
|
828
|
+
const abs = _expandShellPath(w, cwd);
|
|
829
|
+
if (!abs || _remoteUnc(abs)) return null;
|
|
830
|
+
const dir = _globBase(abs);
|
|
831
|
+
if (!dir) return null;
|
|
832
|
+
const c = _normCase(ctx.canon(dir));
|
|
833
|
+
const withSep = c.endsWith(path.sep) ? c : c + path.sep;
|
|
834
|
+
if (posixGlob && dir !== abs && /^[*?[]/.test(abs.slice(dir.length))) {
|
|
835
|
+
const parent = path.dirname(ctx.home);
|
|
836
|
+
return parent !== c && parent.startsWith(withSep) ? dir : null;
|
|
837
|
+
}
|
|
838
|
+
if (c === ctx.home || ctx.home.startsWith(withSep)) return dir;
|
|
839
|
+
const id = ctx.fid(dir);
|
|
840
|
+
return id && ctx.upIds().has(id) ? dir : null;
|
|
841
|
+
}
|
|
842
|
+
// exclusão explícita da pasta do ts na própria busca
|
|
843
|
+
const _TS_EXCLUDE_RE = /--exclude-dir(?:=|\s+)["']?(?:\*\*\/)?\.ts\b|(?:-g|--glob|--iglob)(?:=|\s*)["']?!(?:\*\*\/)?\.ts\b|-Exclude\s+["']?\.ts\b|-prune\b/i;
|
|
844
|
+
const _TS_GREPS = new Set(['grep', 'egrep', 'fgrep', 'zgrep']);
|
|
845
|
+
const _TS_LISTERS = new Set(['get-childitem', 'gci', 'dir', 'ls', 'find']);
|
|
846
|
+
// Busca recursiva que entra em ~/.ts: { read: true, dir } (lê conteúdo: grep -r, rg --hidden,
|
|
847
|
+
// findstr /s, lista recursiva + leitor no mesmo trecho) ou { read: false, dir } (só lista — vale se
|
|
848
|
+
// o trecho seguinte do pipe ler os arquivos), ou null. Raiz omitida = pasta de trabalho.
|
|
849
|
+
function _tsRecursiveReach(flat, names, seg, cwd, ctx) {
|
|
850
|
+
if (_TS_EXCLUDE_RE.test(seg)) return null;
|
|
851
|
+
const covers = (words, posixGlob = true) => { for (const w of words) { const d = _tsRootCoversTsHome(w, cwd, ctx, posixGlob); if (d) return d; } return null; };
|
|
852
|
+
for (let i = 0; i < names.length; i++) {
|
|
853
|
+
const n = names[i];
|
|
854
|
+
let args = flat.slice(i + 1);
|
|
855
|
+
const stop = args.findIndex(w => w === '|' || w === ';' || w === '&&' || w === '||');
|
|
856
|
+
if (stop >= 0) args = args.slice(0, stop);
|
|
857
|
+
const nonflag = args.filter(w => !/^-/.test(w));
|
|
858
|
+
const roots = () => (nonflag.length <= 1 ? ['.'] : nonflag.slice(1));
|
|
859
|
+
let dir = null;
|
|
860
|
+
if (_TS_GREPS.has(n)) {
|
|
861
|
+
const rec = args.some(w => (/^-[A-Za-z0-9]+$/.test(w) && /[rR]/.test(w)) || /^--(?:recursive|dereference-recursive|directories=recurse)$/.test(w))
|
|
862
|
+
|| /(?:^|\s)-d\s+recurse\b/.test(args.join(' '));
|
|
863
|
+
if (rec) dir = covers(roots());
|
|
864
|
+
} else if (n === 'rg' || n === 'ag') {
|
|
865
|
+
// rg e ag pulam pastas ocultas; só entram em .ts com --hidden, -., -uu (rg) ou -u (ag)
|
|
866
|
+
const hidden = args.some(w => w === '--hidden' || w === '--unrestricted'
|
|
867
|
+
|| (/^-[A-Za-z.]+$/.test(w) && (w.includes('.') || (w.match(/u/g) || []).length >= (n === 'ag' ? 1 : 2))));
|
|
868
|
+
if (hidden) dir = covers(roots());
|
|
869
|
+
} else if (n === 'findstr') {
|
|
870
|
+
// chaves do findstr: /s, /si, -s… (com ":" são /c:texto, /g:arquivo — não contam)
|
|
871
|
+
const sw = args.filter(w => /^[\/-]/.test(w));
|
|
872
|
+
if (sw.some(w => /^[\/-][a-z]+$/i.test(w) && /s/i.test(w))) {
|
|
873
|
+
const plain = args.filter(w => !/^[\/-]/.test(w));
|
|
874
|
+
const specs = sw.some(w => /^[\/-][cg]:/i.test(w)) ? plain : plain.slice(1);
|
|
875
|
+
// findstr é do Windows: o curinga pega nomes com ponto (sem a regra do shell POSIX)
|
|
876
|
+
dir = covers(specs.map(s => path.dirname(_expandShellPath(s, cwd) || s)), false);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (dir) return { read: true, dir };
|
|
880
|
+
if (_TS_LISTERS.has(n)) {
|
|
881
|
+
const rec = n === 'find' || args.some(w => /^-rec/i.test(w) || /^-r$/i.test(w) || /^\/s$/i.test(w));
|
|
882
|
+
if (!rec) continue;
|
|
883
|
+
let rs;
|
|
884
|
+
if (n === 'find') {
|
|
885
|
+
const lead = [];
|
|
886
|
+
for (const w of args) { if (/^[-(!]/.test(w)) break; lead.push(w); }
|
|
887
|
+
rs = lead.length ? lead : ['.'];
|
|
888
|
+
} else {
|
|
889
|
+
rs = [];
|
|
890
|
+
for (let a = 0; a < args.length; a++) {
|
|
891
|
+
const w = args[a];
|
|
892
|
+
if (/^-(?:Path|LiteralPath)$/i.test(w)) { if (args[a + 1]) rs.push(args[++a]); continue; }
|
|
893
|
+
if (/^-(?:Filter|Include|Exclude|Depth|Attributes)$/i.test(w)) { a++; continue; }
|
|
894
|
+
if (!/^[-\/]/.test(w)) rs.push(w);
|
|
895
|
+
}
|
|
896
|
+
if (!rs.length) rs = ['.'];
|
|
897
|
+
}
|
|
898
|
+
const d = covers(rs, n === 'find' || n === 'ls');
|
|
899
|
+
if (!d) continue;
|
|
900
|
+
// lista recursiva com leitor no MESMO trecho (find -exec cat, PowerShell entre aspas com
|
|
901
|
+
// "| Select-String", for /f … type): lê
|
|
902
|
+
if (names.slice(i + 1).some(x => _TS_CONTENT_CMDS.has(x))) return { read: true, dir: d };
|
|
903
|
+
return { read: false, dir: d };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
// Trecho que cita item protegido sem ler o conteúdo (ver o comentário da seção)?
|
|
909
|
+
function _tsSegmentNonRead(head, names, flat, raw) {
|
|
910
|
+
if (/\$\(|`|<\(/.test(raw) || raw.includes('<') || _TS_CODE_READ_RE.test(raw)) return false;
|
|
911
|
+
if (names.slice(1).some(n => _TS_CONTENT_CMDS.has(n))) return false;
|
|
912
|
+
// copiador: a origem já foi conferida (_tsCopyOrigins); o que sobra citado é o destino
|
|
913
|
+
if (_TS_NONREAD.has(head) || _TS_COPIERS.has(head)) return true;
|
|
914
|
+
return head === 'sed' && flat.some(w => /^(?:-i\S*|--in-place\S*)$/.test(w));
|
|
915
|
+
}
|
|
916
|
+
// Cópia/movimento no trecho: as ORIGENS (o destino fica de fora), ou null sem copiador.
|
|
917
|
+
function _tsCopyOrigins(top, flat) {
|
|
918
|
+
for (const words of [top, flat]) {
|
|
919
|
+
const ci = words.findIndex(w => _TS_COPIERS.has(_shellCmdName(w)));
|
|
920
|
+
if (ci < 0) continue;
|
|
921
|
+
const kind = _shellCmdName(words[ci]);
|
|
922
|
+
const winSwitch = kind === 'copy' || kind === 'xcopy' || kind === 'robocopy' || kind === 'move';
|
|
923
|
+
const args = []; let named = false;
|
|
924
|
+
for (let i = ci + 1; i < words.length; i++) {
|
|
925
|
+
const w = words[i];
|
|
926
|
+
if (/^(?:copy-item|cpi|move-item|mi|rename-item|rni)$/.test(kind) && /^-(?:d(?:estination|est)?|newname)$/i.test(w)) { i++; named = true; continue; }
|
|
927
|
+
if (/^-/.test(w) || (winSwitch && /^\/[A-Za-z?][\w:+-]*$/.test(w))) continue;
|
|
928
|
+
args.push(w);
|
|
929
|
+
}
|
|
930
|
+
return { kind, origins: kind === 'robocopy' ? args.slice(0, 1) : (named || args.length < 2 ? args : args.slice(0, -1)) };
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function tsSecretReadReason(cmd, opts = {}) {
|
|
935
|
+
const text = String(cmd || '');
|
|
936
|
+
if (!text.trim()) return null;
|
|
937
|
+
let cwd = path.resolve(String(opts.cwd || process.cwd()));
|
|
938
|
+
let ctx = null;
|
|
939
|
+
const getCtx = () => ctx || (ctx = _tsShellCtx());
|
|
940
|
+
const secret = (w, dir) => _tsShellWordIsSecret(w, dir, getCtx());
|
|
941
|
+
const hit = (seg, dir) => {
|
|
942
|
+
if (_tsTextCitesSecret(seg, dir)) return true;
|
|
943
|
+
const { top, flat } = _shellWords(seg);
|
|
944
|
+
// teto de palavras: cada uma custa algumas chamadas ao disco
|
|
945
|
+
return [...new Set([...top, ...flat])].slice(0, 64).some(w => secret(w, dir));
|
|
946
|
+
};
|
|
947
|
+
let pipeline = [];
|
|
948
|
+
for (const part of _shellSegments(text)) {
|
|
949
|
+
if (!part.pipe) pipeline = [];
|
|
950
|
+
// destino de redirecionamento (> x, >> x, 2> x) é escrita, não leitura
|
|
951
|
+
const seg = part.text.replace(/(?:\d|&)?>>?\|?\s*(?:"[^"]*"|'[^']*'|[^\s"';|&<>]+)/g, ' ');
|
|
952
|
+
const { top, flat } = _shellWords(seg);
|
|
953
|
+
const head = top.length ? _shellCmdName(top[0]) : '';
|
|
954
|
+
if (_TS_CD.has(head)) {
|
|
955
|
+
const alvo = top.slice(1).find(w => !/^(?:\/d|-(?:Path|LiteralPath))$/i.test(w));
|
|
956
|
+
const d = alvo ? _expandShellPath(alvo, cwd) : null;
|
|
957
|
+
if (d) cwd = d;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const names = flat.map(_shellCmdName);
|
|
961
|
+
// cópia/movimento: a ORIGEM protegida recusa; o destino (copiar PARA dentro) é escrita
|
|
962
|
+
const copy = _tsCopyOrigins(top, flat);
|
|
963
|
+
if (copy && copy.origins.some(w => secret(w, cwd))) return _TS_SHELL_READ_REFUSAL;
|
|
964
|
+
const segHit = hit(seg, cwd);
|
|
965
|
+
// o programa de fato, sem prefixo (sudo ls ~/.ts, nohup, VAR=x comando)
|
|
966
|
+
let hi = 0;
|
|
967
|
+
while (hi < top.length - 1 && (_TS_WRAPPERS.has(_shellCmdName(top[hi])) || /^[A-Za-z_]\w*=/.test(top[hi]))) hi++;
|
|
968
|
+
// `<`, subcomando e leitura por código olham o trecho ORIGINAL (a limpeza dos
|
|
969
|
+
// redirecionamentos acima pode levar junto um pedaço de `=>` de arrow function)
|
|
970
|
+
if (segHit && !_tsSegmentNonRead(top.length ? _shellCmdName(top[hi]) : '', names, flat, part.text)) return _TS_SHELL_READ_REFUSAL;
|
|
971
|
+
const rec = _tsRecursiveReach(flat, names, part.text, cwd, getCtx());
|
|
972
|
+
if (rec && rec.read) return _tsRecursiveRefusal(rec.dir);
|
|
973
|
+
if (part.pipe) {
|
|
974
|
+
const reads = _TS_PIPE_READERS.has(head) || (head === 'xargs' && names.slice(1).some(n => _TS_CONTENT_CMDS.has(n)));
|
|
975
|
+
const from = reads && pipeline.find(p => p.hit || p.rec);
|
|
976
|
+
if (from) return from.hit ? _TS_SHELL_READ_REFUSAL : _tsRecursiveRefusal(from.rec);
|
|
977
|
+
}
|
|
978
|
+
pipeline.push({ hit: segHit, rec: rec ? rec.dir : null });
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
407
982
|
|
|
408
983
|
// ── RECUSA INSTANTÂNEA de comandos AUTO-DESTRUTIVOS ──────────────────────────
|
|
409
984
|
// Diferente do gate destrutivo comum (que PEDE aprovação humana): estes comandos matam a
|
|
410
985
|
// PRÓPRIA missão — o processo do agente, o dev server de que a missão depende — ou a
|
|
411
|
-
// máquina inteira. Esperar aprovação (
|
|
986
|
+
// máquina inteira. Esperar aprovação (que já chegou a custar ~150s POR tentativa)
|
|
412
987
|
// por algo que JAMAIS deve rodar é desperdício puro: recusamos NA HORA, apontando a
|
|
413
988
|
// alternativa cirúrgica. Retorna a mensagem (motivo pro modelo) quando é auto-destrutivo,
|
|
414
989
|
// ou null quando é seguro (aí segue o fluxo normal, inclusive o gate de aprovação comum).
|
|
@@ -662,7 +1237,15 @@ function _vizinhos(absPath) {
|
|
|
662
1237
|
} catch (_) { return []; }
|
|
663
1238
|
}
|
|
664
1239
|
|
|
1240
|
+
// Executor público: roda a ferramenta e passa TODA a saída pela máscara dos segredos conhecidos
|
|
1241
|
+
// do ts (token de sessão, chaves BYOK, senha SSH, auth de MCP e o token recebido em opts) antes de
|
|
1242
|
+
// ela virar contexto do modelo, log ou evidência. Se a máscara falhar, o resultado é descartado.
|
|
665
1243
|
async function execute(name, input, opts = {}) {
|
|
1244
|
+
const raw = await _executeTool(name, input, opts);
|
|
1245
|
+
try { return maskKnownSecrets(raw, [opts.token]); }
|
|
1246
|
+
catch (_) { return { erro: 'Resultado descartado: a verificação de segredos na saída falhou.' }; }
|
|
1247
|
+
}
|
|
1248
|
+
async function _executeTool(name, input, opts = {}) {
|
|
666
1249
|
// BASE de trabalho: cwd da sessão do agente (opts.baseDir) → pasta confinada da
|
|
667
1250
|
// missão (opts.confineDir) → process.cwd(). É a raiz de todo caminho relativo.
|
|
668
1251
|
const baseDir = opts.baseDir || opts.confineDir || process.cwd();
|
|
@@ -692,6 +1275,9 @@ async function execute(name, input, opts = {}) {
|
|
|
692
1275
|
const comando = String(input.comando || '');
|
|
693
1276
|
{ const scope = _commandScopeViolation(comando, opts.confineDir); if (scope) return { erro: scope }; }
|
|
694
1277
|
{ const exfil = secretExfiltrationReason(comando); if (exfil) return { erro: exfil }; }
|
|
1278
|
+
// credenciais do ts por shell: mesma recusa do ler_arquivo (defesa em profundidade — o
|
|
1279
|
+
// gate do agent.js já pega antes)
|
|
1280
|
+
{ const tr = tsSecretReadReason(comando, { cwd: baseDir }); if (tr) return { erro: tr }; }
|
|
695
1281
|
// RECUSA INSTANTÂNEA (defesa em profundidade — o gate do agent.js já pega antes):
|
|
696
1282
|
// comando auto-destrutivo NUNCA roda e NUNCA espera aprovação.
|
|
697
1283
|
{ const sd = selfDestructiveReason(comando, { cwd: baseDir }); if (sd) return { erro: sd }; }
|
|
@@ -765,7 +1351,16 @@ async function execute(name, input, opts = {}) {
|
|
|
765
1351
|
}
|
|
766
1352
|
case 'ler_arquivo': {
|
|
767
1353
|
const p = _abs(input.caminho, baseDir);
|
|
1354
|
+
// Recusa os arquivos de credenciais do ts. O conteúdo devolvido NÃO passa por
|
|
1355
|
+
// redactSecrets, de propósito: editar_arquivo precisa do texto exato (o "buscar" sai do
|
|
1356
|
+
// que foi lido) e, com máscara, a edição falharia ou gravaria ••••REDACTED no arquivo.
|
|
1357
|
+
// Segredo de projeto (.env, config com senha) chega inteiro ao modelo — decisão, não esquecimento.
|
|
1358
|
+
// A exceção são os segredos do PRÓPRIO ts (maskKnownSecrets, no execute): se o .env tiver a
|
|
1359
|
+
// mesma chave do providers.json, ela chega mascarada — e escrever_arquivo/editar_arquivo
|
|
1360
|
+
// recusam gravar a máscara por cima do valor real.
|
|
1361
|
+
// O escopo vem antes: ele recusa pelo texto, sem tocar no disco, o que está fora da missão.
|
|
768
1362
|
if (_outsideScope(p)) return _scopeError(p);
|
|
1363
|
+
{ const g = _guardTsSecretRead(p); if (g) return { erro: g }; }
|
|
769
1364
|
if (BIN_RE.test(p)) return { erro: 'Arquivo binário — ler_arquivo só lê texto. Use executar_comando com uma ferramenta adequada.' };
|
|
770
1365
|
// Não achou? Sugere nomes PARECIDOS na mesma pasta (o modelo chuta o nome do recurso).
|
|
771
1366
|
if (!fs.existsSync(p)) {
|
|
@@ -773,7 +1368,8 @@ async function execute(name, input, opts = {}) {
|
|
|
773
1368
|
return { erro: 'Arquivo não encontrado: ' + p + (v.length ? '. Nomes parecidos na mesma pasta: ' + v.join(', ') + ' — confira o nome EXATO (maiúsculas/plural contam).' : '. Confira o caminho (use listar_diretorio/buscar_arquivos).') };
|
|
774
1369
|
}
|
|
775
1370
|
try { if (fs.statSync(p).isDirectory()) return { erro: p + ' é uma PASTA, não um arquivo. Use listar_diretorio pra ver o conteúdo.' }; } catch (_) {}
|
|
776
|
-
const txt =
|
|
1371
|
+
const txt = _readTextNotTsSecret(p);
|
|
1372
|
+
if (typeof txt !== 'string') return txt;
|
|
777
1373
|
const linhas = txt.split('\n'); const total = linhas.length; const MAXL = 400;
|
|
778
1374
|
const ini = parseInt(input.inicio) || 0;
|
|
779
1375
|
if (ini > 0) {
|
|
@@ -828,6 +1424,15 @@ async function execute(name, input, opts = {}) {
|
|
|
828
1424
|
}
|
|
829
1425
|
}
|
|
830
1426
|
if (_outsideScope(p)) return _scopeError(p);
|
|
1427
|
+
// o rebase pode ter trocado o alvo: a pasta do ts é conferida de novo no caminho final
|
|
1428
|
+
if (_rebased) { const g = _guardTsHome(p); if (g) return { erro: g }; }
|
|
1429
|
+
// A leitura mascara os segredos conhecidos do ts: regravar o arquivo inteiro com o texto
|
|
1430
|
+
// lido trocaria o valor real pela máscara (ex.: .env com a mesma chave do providers.json).
|
|
1431
|
+
if (String(input.conteudo ?? '').includes(core.SECRET_MASK)) {
|
|
1432
|
+
let atual = '';
|
|
1433
|
+
try { if (fs.statSync(p).isFile()) atual = fs.readFileSync(p, 'utf8'); } catch (_) {}
|
|
1434
|
+
if (atual && !atual.includes(core.SECRET_MASK)) return { erro: _MASK_WRITE_REFUSAL };
|
|
1435
|
+
}
|
|
831
1436
|
const held = _acquireFileMutation(p); if (held.erro) return { erro: held.erro };
|
|
832
1437
|
try {
|
|
833
1438
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
@@ -860,14 +1465,24 @@ async function execute(name, input, opts = {}) {
|
|
|
860
1465
|
}
|
|
861
1466
|
}
|
|
862
1467
|
if (_outsideScope(p)) return _scopeError(p);
|
|
1468
|
+
{ const g = _guardTsHome(p); if (g) return { erro: g }; } // de novo, no alvo final do rebase
|
|
1469
|
+
// Sem "substituir" a edição APAGAVA o trecho buscado ('' por padrão). Apagar é pedido
|
|
1470
|
+
// explícito: substituir vazio ("").
|
|
1471
|
+
if (input.substituir === undefined || input.substituir === null) {
|
|
1472
|
+
return { erro: 'Informe substituir (o texto novo). Para apagar o trecho buscado, passe substituir vazio ("").' };
|
|
1473
|
+
}
|
|
863
1474
|
const held = _acquireFileMutation(p); if (held.erro) return { erro: held.erro };
|
|
864
1475
|
try {
|
|
865
1476
|
if (!fs.existsSync(p)) {
|
|
866
1477
|
const v = _vizinhos(p);
|
|
867
1478
|
return { erro: 'Arquivo não existe: ' + p + (v.length ? '. Nomes parecidos na mesma pasta: ' + v.join(', ') + ' — confira se não errou o nome.' : '') + ' Pra criar arquivo NOVO use escrever_arquivo.' };
|
|
868
1479
|
}
|
|
869
|
-
|
|
1480
|
+
// Lê pelo descritor com a mesma checagem de identidade do ler_arquivo: um apelido de
|
|
1481
|
+
// credencial do ts (hardlink, UNC por IP) não chega à mensagem de "Linha parecida" abaixo.
|
|
1482
|
+
const orig = _readTextNotTsSecret(p);
|
|
1483
|
+
if (typeof orig !== 'string') return orig;
|
|
870
1484
|
let buscar = String(input.buscar ?? ''), substituir = String(input.substituir ?? '');
|
|
1485
|
+
if (substituir.includes(core.SECRET_MASK) && !orig.includes(core.SECRET_MASK)) return { erro: _MASK_WRITE_REFUSAL };
|
|
871
1486
|
if (substituir.includes('\uFFFD') && !orig.includes('\uFFFD')) return { erro: 'CODIFICAÇÃO BLOQUEADA: a edição introduziria o caractere de substituição �. Releia a fonte em UTF-8 e envie o texto correto.' };
|
|
872
1487
|
if (!buscar) return { erro: 'buscar vazio.' };
|
|
873
1488
|
// tolerância CRLF: o modelo manda \n mas arquivo Windows tem \r\n — normaliza o par
|
|
@@ -881,7 +1496,7 @@ async function execute(name, input, opts = {}) {
|
|
|
881
1496
|
const lines = orig.split(/\r?\n/);
|
|
882
1497
|
const idx = alvo ? lines.findIndex(l => l.includes(alvo.slice(0, 25))) : -1;
|
|
883
1498
|
return { erro: 'Trecho de "buscar" NÃO encontrado — tem que ser EXATO (mesmos espaços/indentação). '
|
|
884
|
-
+ (idx >= 0 ? `Linha parecida (linha ${idx + 1}): ${lines[idx].slice(0, 160)} — releia com ler_arquivo e copie exato.` : 'Releia o arquivo com ler_arquivo e copie o trecho exato.') };
|
|
1499
|
+
+ (idx >= 0 ? `Linha parecida (linha ${idx + 1}): ${core.redactSecrets(lines[idx]).slice(0, 160)} — releia com ler_arquivo e copie exato.` : 'Releia o arquivo com ler_arquivo e copie o trecho exato.') };
|
|
885
1500
|
}
|
|
886
1501
|
if (count > 1 && !input.todas) return { erro: `Trecho AMBÍGUO: ${count} ocorrências. Inclua mais linhas de contexto em "buscar" pra ficar único, ou passe todas:true.` };
|
|
887
1502
|
const novo = input.todas ? orig.split(buscar).join(substituir) : orig.replace(buscar, substituir);
|
|
@@ -893,6 +1508,7 @@ async function execute(name, input, opts = {}) {
|
|
|
893
1508
|
case 'restaurar_arquivo': {
|
|
894
1509
|
const p = _abs(input.caminho, baseDir);
|
|
895
1510
|
if (_outsideScope(p)) return _scopeError(p);
|
|
1511
|
+
{ const g = _guardTsHome(p); if (g) return { erro: g }; }
|
|
896
1512
|
const held = _acquireFileMutation(p); if (held.erro) return { erro: held.erro };
|
|
897
1513
|
try {
|
|
898
1514
|
const dir = path.join(BK_DIR, _bkKey(p));
|
|
@@ -920,6 +1536,10 @@ async function execute(name, input, opts = {}) {
|
|
|
920
1536
|
// próximos caminhos relativos (e comandos) a partir daqui.
|
|
921
1537
|
const alvo = _abs(input.caminho || input.diretorio || '.', baseDir);
|
|
922
1538
|
if (_outsideScope(alvo)) return _scopeError(alvo);
|
|
1539
|
+
// ~/.ts não vira pasta de trabalho: buscar_codigo indexaria as credenciais (o índice pula
|
|
1540
|
+
// ".ts" só como SUBpasta) e caminhos relativos cairiam nelas. Skills e memória continuam
|
|
1541
|
+
// legíveis pelo caminho completo.
|
|
1542
|
+
if (_insideTsHome(alvo)) return { erro: 'RECUSADO: "' + alvo + '" é a área de configuração do ts (~/.ts) — ela não vira pasta de trabalho, porque guarda credenciais (token de sessão, chaves de IA, auth de MCP). Para ler uma skill ou a memória, use ler_arquivo com o caminho completo; para ver os nomes, listar_diretorio.' };
|
|
923
1543
|
let st; try { st = fs.statSync(alvo); } catch (_) { const v = _vizinhos(alvo); return { erro: 'Pasta não existe: ' + alvo + (v.length ? '. Nomes parecidos na pasta acima: ' + v.join(', ') : '') + '. Confira o caminho (use listar_diretorio/buscar_arquivos).' }; }
|
|
924
1544
|
if (!st.isDirectory()) return { erro: 'Não é uma pasta: ' + alvo };
|
|
925
1545
|
return { ok: true, cwd: alvo, _setCwd: alvo };
|
|
@@ -947,6 +1567,8 @@ async function execute(name, input, opts = {}) {
|
|
|
947
1567
|
case 'buscar_codigo': {
|
|
948
1568
|
const consulta = String(input.consulta || '').trim();
|
|
949
1569
|
if (!consulta) return { erro: 'consulta vazia' };
|
|
1570
|
+
// dentro de ~/.ts o índice leria (e gravaria em .ts-indice.json) as credenciais do ts
|
|
1571
|
+
if (_insideTsHome(baseDir)) return { erro: 'RECUSADO: a pasta de trabalho está dentro de ~/.ts (configuração e credenciais do ts) — buscar_codigo não indexa essa área. Mude para a pasta do projeto com mudar_diretorio.' };
|
|
950
1572
|
const ix = require('./indice');
|
|
951
1573
|
let idx = ix.load(baseDir);
|
|
952
1574
|
let construiu = false;
|
|
@@ -1063,6 +1685,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1063
1685
|
case 'ler_documento': {
|
|
1064
1686
|
const target = _abs(input.caminho, baseDir);
|
|
1065
1687
|
if (_outsideScope(target)) return _scopeError(target);
|
|
1688
|
+
{ const g = _guardTsSecretRead(target); if (g) return { erro: g }; }
|
|
1066
1689
|
return await require('./office-readers').lerDocumento({ ...input, caminho: target });
|
|
1067
1690
|
}
|
|
1068
1691
|
case 'editar_documento': {
|
|
@@ -1070,6 +1693,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1070
1693
|
if (_outsideScope(target)) return _scopeError(target);
|
|
1071
1694
|
const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
|
|
1072
1695
|
if (copy && _outsideScope(copy)) return _scopeError(copy);
|
|
1696
|
+
for (const alvo of [target, copy]) { const g = alvo && _guardTsHome(alvo); if (g) return { erro: g }; }
|
|
1073
1697
|
const held = _acquireFileMutation(target); if (held.erro) return { erro: held.erro };
|
|
1074
1698
|
try {
|
|
1075
1699
|
const result = await require('./office-editors').editarDocumento({ ...input, caminho: target, caminho_copia: copy });
|
|
@@ -1082,6 +1706,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1082
1706
|
if (_outsideScope(target)) return _scopeError(target);
|
|
1083
1707
|
const copy = input.caminho_copia ? _abs(input.caminho_copia, baseDir) : undefined;
|
|
1084
1708
|
if (copy && _outsideScope(copy)) return _scopeError(copy);
|
|
1709
|
+
for (const alvo of [target, copy]) { const g = alvo && _guardTsHome(alvo); if (g) return { erro: g }; }
|
|
1085
1710
|
const held = _acquireFileMutation(target); if (held.erro) return { erro: held.erro };
|
|
1086
1711
|
try {
|
|
1087
1712
|
const result = await require('./office-editors').editarPlanilha({ ...input, caminho: target, caminho_copia: copy });
|
|
@@ -1092,6 +1717,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1092
1717
|
case 'ler_planilha': {
|
|
1093
1718
|
const target = _abs(input.caminho, baseDir);
|
|
1094
1719
|
if (_outsideScope(target)) return _scopeError(target);
|
|
1720
|
+
{ const g = _guardTsSecretRead(target); if (g) return { erro: g }; }
|
|
1095
1721
|
return await require('./office-readers').lerPlanilha({ ...input, caminho: target });
|
|
1096
1722
|
}
|
|
1097
1723
|
case 'status_microsoft365':
|
|
@@ -1182,6 +1808,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1182
1808
|
case 'ler_apresentacao': {
|
|
1183
1809
|
const target = _abs(input.caminho, baseDir);
|
|
1184
1810
|
if (_outsideScope(target)) return _scopeError(target);
|
|
1811
|
+
{ const g = _guardTsSecretRead(target); if (g) return { erro: g }; }
|
|
1185
1812
|
return await require('./office-readers').lerApresentacao({ ...input, caminho: target });
|
|
1186
1813
|
}
|
|
1187
1814
|
case 'android_parear':
|
|
@@ -1197,6 +1824,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1197
1824
|
case 'android_capturar_tela': {
|
|
1198
1825
|
const target = _abs(input.caminho, baseDir);
|
|
1199
1826
|
if (_outsideScope(target)) return _scopeError(target);
|
|
1827
|
+
{ const g = _guardTsHome(target); if (g) return { erro: g }; }
|
|
1200
1828
|
return await require('./device-tools').capturarTela({ ...input, caminho: target });
|
|
1201
1829
|
}
|
|
1202
1830
|
default: {
|
|
@@ -1212,5 +1840,7 @@ async function execute(name, input, opts = {}) {
|
|
|
1212
1840
|
} catch (e) { return { erro: String((e && e.message) || e).slice(0, 400) }; }
|
|
1213
1841
|
}
|
|
1214
1842
|
|
|
1215
|
-
module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, secretExfiltrationReason, buildProjectMap,
|
|
1216
|
-
|
|
1843
|
+
module.exports = { DEFS, execute, isDestructive, selfDestructiveReason, secretExfiltrationReason, tsSecretReadReason, buildProjectMap,
|
|
1844
|
+
knownSecretValues, knownSecretsFor, maskKnownSecrets,
|
|
1845
|
+
_test: { shellGotcha: _shellGotcha, normalizeWindowsReadOnlyCommand: _normalizeWindowsReadOnlyCommand, commandScopeViolation: _commandScopeViolation, isReadOnlySearchNoMatch: _isReadOnlySearchNoMatch, guardTsSecretRead: _guardTsSecretRead, remoteUnc: _remoteUnc,
|
|
1846
|
+
guardTsHome: _guardTsHome, tsTextCitesSecret: _tsTextCitesSecret } };
|