terminal-smart-cli 0.76.0 → 0.78.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/bin/ts.js +7 -3
- package/lib/agent.js +15 -3
- package/lib/core.js +21 -0
- package/lib/ssh.js +18 -7
- package/lib/tools.js +10 -2
- package/lib/verify.js +35 -4
- package/package.json +1 -1
package/bin/ts.js
CHANGED
|
@@ -37,10 +37,12 @@ function fmtK(n) { return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
|
|
|
37
37
|
// heredoc SQL a 1ª linha é o comando externo, ex "mysql -u root pw <<'SQL'"). Aceita:
|
|
38
38
|
// s/N = sim/não neste passo · a = aprovar TUDO na sessão (full-auto) · v = ver o comando inteiro
|
|
39
39
|
// Retorna true | false | 'all'.
|
|
40
|
-
async function askDestructive(cmd, { en = false } = {}) {
|
|
40
|
+
async function askDestructive(cmd, { en = false, warning = null } = {}) {
|
|
41
41
|
const raw = String(cmd || '');
|
|
42
42
|
const { shown, hidden } = ui.cmdSummary(raw);
|
|
43
43
|
const more = hidden > 0 ? C.dim(` … +${hidden} ${en ? 'lines' : 'linhas'}`) : '';
|
|
44
|
+
// efeito colateral GLOBAL (ex: ALTER USER root quebra o sudo mysql): avisa UMA vez, antes do prompt.
|
|
45
|
+
if (warning) console.log(C.warn(' ⚠ ' + (en ? 'side effect: ' : 'efeito colateral: ')) + C.dim(warning));
|
|
44
46
|
for (;;) {
|
|
45
47
|
const q = C.warn(' ▲ ' + (en ? 'destructive: ' : 'destrutivo: ')) + C.bold(shown) + more
|
|
46
48
|
+ C.dim(en ? ' [y/N · a=all · v=view] ' : ' [s/N · a=tudo · v=ver] ');
|
|
@@ -61,7 +63,7 @@ async function askApproveRoute(payload) {
|
|
|
61
63
|
const en = cfg.lang === 'en';
|
|
62
64
|
const isObj = payload && typeof payload === 'object';
|
|
63
65
|
const cmd = isObj ? payload.cmd : String(payload);
|
|
64
|
-
return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en }) : askLabelApprove(cmd, { en });
|
|
66
|
+
return (isObj && payload.kind === 'destructive') ? askDestructive(cmd, { en, warning: payload.warning }) : askLabelApprove(cmd, { en });
|
|
65
67
|
}
|
|
66
68
|
// Linha de STATUS por etapa (✓ verde / ✗ vermelho) sob a linha ⚙, com recuo `pad`.
|
|
67
69
|
function stepDoneLine({ ok, evidence }, pad = ' ') {
|
|
@@ -1802,7 +1804,9 @@ async function verificarCmd(args) {
|
|
|
1802
1804
|
}
|
|
1803
1805
|
|
|
1804
1806
|
if (!JSON_OUT) console.log('\n' + C.dim(en ? 'proving completion — ' : 'provando conclusão — ') + criteria.length + (en ? ' criterion(s)…' : ' critério(s)…'));
|
|
1805
|
-
|
|
1807
|
+
// remoteExec: liga o verificador ldd remoto à conexão SSH ativa (binário PW mora na VPS).
|
|
1808
|
+
const _ssh = require('../lib/ssh');
|
|
1809
|
+
const rep = await verify.runAll(criteria, { cwd, remoteExec: _ssh.isConnected() ? (cmd => _ssh.exec(cmd, { timeoutMs: 30000 })) : undefined });
|
|
1806
1810
|
if (JSON_OUT) { console.log(JSON.stringify(rep)); process.exit(rep.allOk ? 0 : 1); }
|
|
1807
1811
|
|
|
1808
1812
|
for (const r of rep.results) {
|
package/lib/agent.js
CHANGED
|
@@ -63,6 +63,11 @@ REGRAS:
|
|
|
63
63
|
- Windows: python -c multi-linha falha em silêncio — escreva um .py com escrever_arquivo e execute "python arquivo.py".
|
|
64
64
|
- Se faltar dependência, instale (winget/apt/pip/npm) e prossiga.
|
|
65
65
|
- LINUX/apt: NUNCA rode "apt-get install" cru. (a) Em máquina nova o unattended-upgrades segura o lock e a instalação PENDURA. (b) Install que baixa VÁRIOS pacotes (gcc, mariadb, tomcat…) leva mais de 60s e MORRE no timeout padrão → nada instala. Instale SEMPRE assim, com timeout LONGO: chame executar_comando com timeout_s: 300 e comando "sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 install -y <pacotes>" (Lock::Timeout ESPERA o lock; noninteractive+-y evita prompts sem TTY; timeout_s:300 dá 5min pro download). PPA com "add-apt-repository -y" + "apt-get update". Se um comando de install voltar vazio/sem efeito, quase sempre foi timeout curto — refaça com timeout_s: 300.
|
|
66
|
+
- FONTE AUTÊNTICA antes de improvisar: antes de GERAR do zero um arquivo estruturado (config, schema SQL, .conf, dados, manifesto), PROCURE se já existe a versão real — no projeto atual, no pack/instalação de referência que o usuário citou, ou numa instalação funcionante. Se existir, USE-A (copie/adapte o mínimo). A versão inventada de memória quase sempre sai sutilmente errada/incompleta (falta uma linha, uma procedure, um valor). Só improvise se comprovadamente não existir — e nesse caso AVISE que é uma reconstrução (pode ter erro) e peça pra validar.
|
|
67
|
+
- PRIVILÉGIO MÍNIMO em infra: pra dar acesso de uma aplicação a um serviço (banco, broker), crie uma CREDENCIAL DEDICADA com o mínimo de permissão (ex: CREATE USER 'app' + GRANT só no schema necessário) e aponte a app pra ela. NUNCA altere a autenticação/config GLOBAL existente (root/admin) só pra uma app conectar — isso afeta TUDO que já usa aquele acesso (backups, outros serviços, seu próprio acesso). Se precisar MESMO mudar algo global, AVISE explicitamente o que mais será afetado e por quê.
|
|
68
|
+
- NUNCA peça segredo ao usuário: jamais peça senha/chave/token/credencial no chat. Se uma operação remota (ssh/scp) falhar por autenticação, REUSE a conexão/chave que JÁ funcionou nesta sessão (mesmo host/user/chave — não reconstrua o comando do zero esquecendo o -i da chave). Se ainda assim não conectar, peça pro usuário EXECUTAR a ação — nunca entregar o segredo.
|
|
69
|
+
- NÃO DESISTA sem TENTAR: é PROIBIDO responder "não tenho acesso" / "não consigo" / "preciso que você me passe X" enquanto houver uma ferramenta que você ainda não usou pra tentar. Antes de declarar que algo é impossível, AJA: conecte (conectar_vps), procure (buscar_arquivos, ou grep/find via executar_comando/executar_remoto), leia (ler_arquivo). Ex.: pediram pra ler um código-fonte que "você não tem"? Se há uma VPS/pasta onde ele pode estar, CONECTE e procure (grep -rn "<símbolo>" <dir>) ANTES de dizer que não tem. Só afirme que não conseguiu DEPOIS de ter tentado de fato e mostre o erro/saída REAL que te barrou.
|
|
70
|
+
- BINÁRIO NATIVO: ao subir/instalar um executável numa máquina, valide com file (arquitetura: 32 vs 64-bit) E ldd (as bibliotecas resolvem?) ANTES de declarar "pronto/instalado" — "está no lugar" NÃO é "roda". Se ldd mostrar "not found", instale a lib faltante e revalide.
|
|
66
71
|
- Termine SEMPRE com um resumo curto: o que foi feito, resultado e caminhos de arquivos criados/alterados.
|
|
67
72
|
- Responda no idioma do usuário (padrão: português do Brasil).`
|
|
68
73
|
: 'You are the Terminal Smart agent running ON THIS machine via CLI, with REAL tools. Act for real — never say "I will do it" without calling the tool in the same turn, and never invent results that did not come from a tool.'
|
|
@@ -80,6 +85,11 @@ RULES:
|
|
|
80
85
|
- Windows: multi-line python -c fails silently — write a .py with escrever_arquivo and run "python file.py".
|
|
81
86
|
- If a dependency is missing, install it (winget/apt/pip/npm) and continue.
|
|
82
87
|
- LINUX/apt: NEVER run a bare "apt-get install" — on a freshly-created machine unattended-upgrades holds the apt lock and the install HANGS silently (no output). ALWAYS install like this: "sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 install -y <packages>" (the -o Lock::Timeout WAITS for the lock up to 10min instead of hanging; noninteractive+-y avoids prompts that block without a TTY). Use "add-apt-repository -y" + "apt-get update" for PPAs. A command that doesn't return in ~1min is likely stuck on the lock/prompt — don't wait forever.
|
|
88
|
+
- AUTHENTIC SOURCE before improvising: before GENERATING a structured file from scratch (config, SQL schema, .conf, data, manifest), SEARCH for whether a real version already exists — in the current project, in the reference pack/install the user mentioned, or in a working install. If it exists, USE IT (copy/adapt minimally). A version invented from memory almost always comes out subtly wrong/incomplete (a missing line, a missing procedure, a wrong value). Only improvise if it truly doesn't exist — and then WARN that it's a reconstruction (may have errors) and ask to validate.
|
|
89
|
+
- LEAST PRIVILEGE in infra: to give an app access to a service (DB, broker), create a DEDICATED credential with minimal permission (e.g. CREATE USER 'app' + GRANT on the needed schema only) and point the app at it. NEVER alter the existing GLOBAL auth/config (root/admin) just to make an app connect — that affects EVERYTHING already using that access (backups, other services, your own access). If you truly must change something global, explicitly WARN what else will be affected and why.
|
|
90
|
+
- NEVER ask the user for a secret: never ask for a password/key/token/credential in chat. If a remote op (ssh/scp) fails on auth, REUSE the connection/key that ALREADY worked this session (same host/user/key — don't rebuild the command dropping the -i key flag). If it still won't connect, ask the user to PERFORM the action — never to hand over the secret.
|
|
91
|
+
- DON'T GIVE UP without TRYING: it is FORBIDDEN to answer "I don't have access" / "I can't" / "I need you to give me X" while there's a tool you haven't used yet to try. Before declaring something impossible, ACT: connect (conectar_vps), search (buscar_arquivos, or grep/find via executar_comando/executar_remoto), read (ler_arquivo). E.g. asked to read source code you "don't have"? If there's a VPS/folder where it might live, CONNECT and search (grep -rn "<symbol>" <dir>) BEFORE saying you don't have it. Only claim you couldn't do it AFTER actually trying, and show the REAL error/output that blocked you.
|
|
92
|
+
- NATIVE BINARY: when uploading/installing an executable on a machine, validate with file (architecture: 32 vs 64-bit) AND ldd (do the libraries resolve?) BEFORE declaring "done/installed" — "it's in place" is NOT "it runs". If ldd shows "not found", install the missing lib and re-validate.
|
|
83
93
|
- ALWAYS end with a short summary: what was done, the result, and paths of created/changed files.
|
|
84
94
|
- Answer in the user's language.`);
|
|
85
95
|
const volatile = `\n\nMáquina: ${process.platform} ${os.release()} · host ${os.hostname()} · user ${os.userInfo().username}`
|
|
@@ -505,8 +515,8 @@ async function run(task, opts = {}) {
|
|
|
505
515
|
_warnedSigs.add(_sig); _loopWarned = true;
|
|
506
516
|
onStep({ name, detail: argsShort(name, input), loop: true });
|
|
507
517
|
result = { erro: lang !== 'en'
|
|
508
|
-
? `LOOP DETECTADO: você já fez "${name}(${argsShort(name, input)})" ${_nSig}× e o resultado NÃO muda. PARE de repetir —
|
|
509
|
-
: `LOOP DETECTED: you already did "${name}(${argsShort(name, input)})" ${_nSig}× and the result is NOT changing. STOP repeating —
|
|
518
|
+
? `LOOP DETECTADO: você já fez "${name}(${argsShort(name, input)})" ${_nSig}× e o resultado NÃO muda. PARE de repetir. Chutar não resolve — vá à CAUSA RAIZ: LEIA a fonte autêntica (o código-fonte que gera o erro, o .conf/manual real, o log completo) com ler_arquivo/buscar_codigo/explorar, entenda o formato exato e AÍ conserte. Se for grande demais pra este modo, diga que precisa escalar (o "ts meta" chama o modelo forte). Repetir a mesma ação de novo encerra a missão.`
|
|
519
|
+
: `LOOP DETECTED: you already did "${name}(${argsShort(name, input)})" ${_nSig}× and the result is NOT changing. STOP repeating. Guessing won't fix it — go to the ROOT CAUSE: READ the authentic source (the source code that emits the error, the real .conf/manual, the full log) with ler_arquivo/buscar_codigo/explorar, learn the exact format, THEN fix. If it's too big for this mode, say you need to escalate ("ts meta" calls the strong model). Repeating the same action again ends the mission.` };
|
|
510
520
|
}
|
|
511
521
|
|
|
512
522
|
// ENFORCEMENT do modo só-leitura PRIMEIRO: bloqueia ferramentas que alteram/rodam ANTES de
|
|
@@ -550,7 +560,9 @@ async function run(task, opts = {}) {
|
|
|
550
560
|
}
|
|
551
561
|
} else if (!yes) {
|
|
552
562
|
// Passa payload ESTRUTURADO → a UI mostra um RESUMO curto (não o heredoc inteiro).
|
|
553
|
-
|
|
563
|
+
// warning: aviso ESPECÍFICO de efeito colateral se o comando muda estado global (ex: ALTER USER root).
|
|
564
|
+
const _cmd = String(input.comando || '');
|
|
565
|
+
const dec = await askApprove({ kind: 'destructive', cmd: _cmd, warning: core.globalStateWarning(_cmd) });
|
|
554
566
|
const d = decideApproval({ autoAll: false, decision: dec });
|
|
555
567
|
approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
|
|
556
568
|
} else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
|
package/lib/core.js
CHANGED
|
@@ -20,9 +20,29 @@
|
|
|
20
20
|
const TS_CONFIG_RE = /[\\\/]\.ts[\\\/](skills|skills-pending|hooks|mcp|config)/i;
|
|
21
21
|
function touchesTsConfig(cmd) { return TS_CONFIG_RE.test(String(cmd || '')); }
|
|
22
22
|
|
|
23
|
+
// Comandos que mudam ESTADO GLOBAL de um serviço/host: o efeito colateral vai ALÉM do que o
|
|
24
|
+
// usuário pediu. Cada um traz um AVISO ESPECÍFICO (não "destrutivo" genérico) pra a aprovação.
|
|
25
|
+
// Nasceu de um ALTER USER root que fez uma app conectar MAS quebrou o `sudo mysql` sem senha.
|
|
26
|
+
const GLOBAL_STATE = [
|
|
27
|
+
[/\bALTER\s+USER\b[^;]*IDENTIFIED\s+BY\b/i, 'muda a senha/auth de um usuário do banco — quebra TUDO que já conecta com ele (backups, sudo mysql, outros serviços). O certo é criar um usuário DEDICADO (CREATE USER + GRANT no schema) em vez de mexer no root.'],
|
|
28
|
+
[/\b(SET\s+PASSWORD|GRANT\s+ALL\s+PRIVILEGES)\b/i, 'muda credencial/privilégio GLOBAL do banco — afeta tudo que usa esse acesso.'],
|
|
29
|
+
[/\bDROP\s+USER\b/i, 'remove um usuário do banco — quebra tudo que autentica com ele.'],
|
|
30
|
+
[/\/etc\/ssh\/sshd_config/i, 'edita a config do SSH do host — pode te TRANCAR pra fora (porta/auth). Faça backup e não reinicie o sshd sem confirmar que ainda entra.'],
|
|
31
|
+
[/\b(ufw|iptables)\b[^\n]*(-F|flush|reset|\bdeny\b|\bDROP\b)/i, 'mexe no firewall do host — pode cortar seu PRÓPRIO acesso SSH.'],
|
|
32
|
+
[/\busermod\b|\/etc\/sudoers|\bvisudo\b/i, 'muda usuários/sudoers do sistema — efeito global de permissão.'],
|
|
33
|
+
[/\bchmod\b[^\n]*\/etc\b|\bchown\b[^\n]*\/etc\b/i, 'muda permissão/dono em /etc — config global do sistema.'],
|
|
34
|
+
];
|
|
35
|
+
// aviso específico do efeito colateral, ou null se o comando não mexe em estado global.
|
|
36
|
+
function globalStateWarning(cmd) {
|
|
37
|
+
const s = String(cmd || '');
|
|
38
|
+
for (const [re, msg] of GLOBAL_STATE) if (re.test(s)) return msg;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
// Padrões de comando destrutivo/irreversível — SEMPRE pedem aprovação humana.
|
|
24
43
|
// (fonte única: antes vivia em tools.js; App e Web tinham cópias divergentes.)
|
|
25
44
|
const DESTRUCTIVE = [
|
|
45
|
+
...GLOBAL_STATE.map(g => g[0]), // mudanças de estado global também passam pela aprovação
|
|
26
46
|
/\brm\s+(-[a-z]*[rf][a-z]*\s+)/i, /\brm\s+.*\*/, /\brmdir\b/i, /\bdel\s+\/[sq]/i, /\brd\s+\/s/i,
|
|
27
47
|
/\bmkfs\b/i, /\bdd\s+if=/i, /(?:^|[\s&;|])format\s+[a-z]:/i, /\bdiskpart\b/i, /> ?\/dev\/sd/i,
|
|
28
48
|
/\bshutdown\b/i, /\breboot\b/i, /\bhalt\b/i, /\bpoweroff\b/i,
|
|
@@ -146,6 +166,7 @@ function classifyToolResult(raw) {
|
|
|
146
166
|
|
|
147
167
|
module.exports = {
|
|
148
168
|
DESTRUCTIVE, isDestructive, touchesTsConfig,
|
|
169
|
+
GLOBAL_STATE, globalStateWarning,
|
|
149
170
|
MODEL_WINDOWS, CTX_WINDOW_DEFAULT, DEFAULT_EXECUTOR, COMPACT_AT, KEEP_TAIL, winFor, estMsgsTok,
|
|
150
171
|
AgentEvents, EVENT_TYPES,
|
|
151
172
|
ERROR_CLASSES, RETRYABLE_CLASSES, classifyError, classifyToolResult,
|
package/lib/ssh.js
CHANGED
|
@@ -6,7 +6,8 @@ const path = require('path');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
|
|
8
8
|
let _conn = null; // cliente ssh2 conectado
|
|
9
|
-
let _info = null; // { host, port, user }
|
|
9
|
+
let _info = null; // { host, port, user, keyPath } — some no 'close'
|
|
10
|
+
let _lastGood = null; // ÚLTIMO login que DEU CERTO nesta sessão { host, port, user, keyPath } — SOBREVIVE à queda
|
|
10
11
|
let Client = null;
|
|
11
12
|
try { Client = require('ssh2').Client; } catch (_) { /* ssh2 ausente → erro amigável no connect */ }
|
|
12
13
|
|
|
@@ -47,17 +48,22 @@ function parseLoginFile(p) {
|
|
|
47
48
|
|
|
48
49
|
function isConnected() { return !!_conn; }
|
|
49
50
|
function info() { return _info; }
|
|
51
|
+
// Último login que funcionou nesta sessão — pra reconectar sem o usuário reinformar host/chave,
|
|
52
|
+
// e pra o agente NUNCA "esquecer" que já entrou (foi o que causou IP errado e scp sem -i).
|
|
53
|
+
function lastGood() { return _lastGood; }
|
|
50
54
|
|
|
51
55
|
function connect(opts = {}) {
|
|
52
56
|
return new Promise((resolve, reject) => {
|
|
53
57
|
if (!Client) return reject(new Error('Biblioteca ssh2 não instalada no CLI.'));
|
|
54
58
|
const prof = loadProfile() || {};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
59
|
+
// ordem de resolução: o que foi passado agora → perfil salvo → ÚLTIMO login que deu certo nesta sessão.
|
|
60
|
+
// assim, se a conexão caiu, um conectar_vps SEM args reusa host+chave que já funcionaram (não pede nada).
|
|
61
|
+
const host = opts.host || prof.host || (_lastGood && _lastGood.host);
|
|
62
|
+
const port = opts.port || prof.port || (_lastGood && _lastGood.port) || 22;
|
|
63
|
+
const user = opts.user || prof.user || (_lastGood && _lastGood.user) || 'root';
|
|
58
64
|
let password = opts.pass || prof.pass;
|
|
59
65
|
let privateKey;
|
|
60
|
-
const keyPath = opts.keyPath || prof.keyPath;
|
|
66
|
+
const keyPath = opts.keyPath || prof.keyPath || (_lastGood && _lastGood.keyPath);
|
|
61
67
|
if (keyPath) { try { privateKey = fs.readFileSync(keyPath); } catch (_) {} }
|
|
62
68
|
if (!host) return reject(new Error('Sem host — configure com "ts vps set" ou passe --host.'));
|
|
63
69
|
if (!password && !privateKey) return reject(new Error('Sem senha nem chave — configure a credencial.'));
|
|
@@ -66,7 +72,12 @@ function connect(opts = {}) {
|
|
|
66
72
|
const c = new Client();
|
|
67
73
|
const cfg = { host, port, username: user, readyTimeout: 20000, keepaliveInterval: 15000 };
|
|
68
74
|
if (privateKey) cfg.privateKey = privateKey; else cfg.password = password;
|
|
69
|
-
c.on('ready', () => {
|
|
75
|
+
c.on('ready', () => {
|
|
76
|
+
_conn = c;
|
|
77
|
+
_info = { host, port, user, keyPath: keyPath || null };
|
|
78
|
+
_lastGood = { host, port, user, keyPath: keyPath || null }; // grava a conexão boa (sobrevive ao close)
|
|
79
|
+
resolve({ host, port, user });
|
|
80
|
+
});
|
|
70
81
|
c.on('error', (e) => { _conn = null; reject(new Error('Falha ao conectar: ' + (e && e.message || e))); });
|
|
71
82
|
c.on('close', () => { if (_conn === c) { _conn = null; _info = null; } });
|
|
72
83
|
try { c.connect(cfg); } catch (e) { reject(e); }
|
|
@@ -90,4 +101,4 @@ function exec(cmd, { timeoutMs = 120000 } = {}) {
|
|
|
90
101
|
|
|
91
102
|
function disconnect() { if (_conn) { try { _conn.end(); } catch (_) {} _conn = null; _info = null; return true; } return false; }
|
|
92
103
|
|
|
93
|
-
module.exports = { connect, exec, disconnect, isConnected, info, loadProfile, saveProfile, parseLoginFile };
|
|
104
|
+
module.exports = { connect, exec, disconnect, isConnected, info, lastGood, loadProfile, saveProfile, parseLoginFile };
|
package/lib/tools.js
CHANGED
|
@@ -368,8 +368,16 @@ async function execute(name, input, opts = {}) {
|
|
|
368
368
|
try {
|
|
369
369
|
const r = await ssh.connect({ host: input.host, user: input.usuario, keyPath: input.chave });
|
|
370
370
|
const who = await ssh.exec('echo "$(whoami)@$(hostname) | $(. /etc/os-release 2>/dev/null; echo $PRETTY_NAME) | pwd=$(pwd)"');
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
// memoriza a conexão boa: nas próximas vezes basta executar_remoto (não reconstrua ssh/scp cru).
|
|
372
|
+
return { conectado: true, servidor: `${r.user}@${r.host}:${r.port}`, info: (who.stdout || '').trim(), memorizado: 'Conexão ativa e memorizada — use executar_remoto/enviar_arquivo (NÃO monte ssh/scp cru) e não repasse host/chave.' };
|
|
373
|
+
} catch (e) {
|
|
374
|
+
// se JÁ conectamos com sucesso nesta sessão, o certo é REUSAR (não pedir credencial ao usuário).
|
|
375
|
+
const lg = ssh.lastGood && ssh.lastGood();
|
|
376
|
+
const dica = lg
|
|
377
|
+
? ` — mas você JÁ conectou nesta sessão em ${lg.user}@${lg.host}${lg.keyPath ? ' (chave ' + require('path').basename(lg.keyPath) + ')' : ''}. Chame conectar_vps SEM argumentos pra reusar essa conexão; NÃO peça senha ao usuário.`
|
|
378
|
+
: ' — configure com "ts vps set" ou passe host/usuario/chave.';
|
|
379
|
+
return { conectado: false, erro: String(e.message).slice(0, 300) + dica };
|
|
380
|
+
}
|
|
373
381
|
}
|
|
374
382
|
case 'executar_remoto': {
|
|
375
383
|
const ssh = require('./ssh');
|
package/lib/verify.js
CHANGED
|
@@ -61,6 +61,35 @@ function vPort(c) {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// Analisa a saída de `ldd`: quais libs NÃO resolvem. Separado pra ser testável sem ldd real.
|
|
65
|
+
// "arquivo está no lugar" ≠ "roda": foi o buraco do deploy que só checou `file` (arquitetura).
|
|
66
|
+
function _analyzeLdd(out) {
|
|
67
|
+
const s = String(out || '');
|
|
68
|
+
if (/not a dynamic executable|não é um executável dinâmico|statically linked/i.test(s)) return { ok: true, missing: [], detail: 'binário estático (sem deps dinâmicas)' };
|
|
69
|
+
const missing = (s.match(/^\s*(\S+)\s*=>\s*not found/gim) || []).map(l => l.trim().split(/\s+/)[0]);
|
|
70
|
+
if (missing.length) return { ok: false, missing, detail: 'libs faltando: ' + missing.join(', ') };
|
|
71
|
+
if (/=>/.test(s)) return { ok: true, missing: [], detail: 'todas as libs resolvem' };
|
|
72
|
+
return { ok: false, missing: [], detail: 'ldd sem saída válida (arquivo existe? é ELF?)' };
|
|
73
|
+
}
|
|
74
|
+
// opts.remoteExec(cmd) → Promise<{stdout,stderr}> é INJETADO por quem chama (bin/ts.js liga ao ssh),
|
|
75
|
+
// pra o verify.js seguir portável (só núcleo+builtins). Sem ele, ldd remoto avisa em vez de importar ssh.
|
|
76
|
+
async function vLdd(c, cwd, opts = {}) {
|
|
77
|
+
const target = c.remote ? String(c.path) : _abs(cwd, c.path);
|
|
78
|
+
const cmd = 'ldd ' + JSON.stringify(target);
|
|
79
|
+
let out = '';
|
|
80
|
+
try {
|
|
81
|
+
if (c.remote) {
|
|
82
|
+
if (typeof opts.remoteExec !== 'function') return { ok: false, detail: 'binário remoto mas sem executor SSH ligado — conecte a VPS antes', errorClass: 'network' };
|
|
83
|
+
const r = await opts.remoteExec(cmd);
|
|
84
|
+
out = (r.stdout || '') + (r.stderr || '');
|
|
85
|
+
} else {
|
|
86
|
+
out = execSync(cmd, { cwd, encoding: 'utf8', timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
87
|
+
}
|
|
88
|
+
} catch (e) { out = String(e.stdout || '') + String(e.stderr || e.message || ''); }
|
|
89
|
+
const a = _analyzeLdd(out);
|
|
90
|
+
return { ok: a.ok, detail: a.detail, errorClass: a.ok ? null : 'missing_dep', evidence: String(out).slice(0, 200).replace(/\s+/g, ' ') };
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
const VERIFIERS = {
|
|
65
94
|
command: (c, cwd) => vCommand(c, cwd),
|
|
66
95
|
file_exists: (c, cwd) => vFileExists(c, cwd),
|
|
@@ -68,6 +97,7 @@ const VERIFIERS = {
|
|
|
68
97
|
file_contains: (c, cwd) => vFileContains(c, cwd),
|
|
69
98
|
http: (c) => vHttp(c),
|
|
70
99
|
port: (c) => vPort(c),
|
|
100
|
+
ldd: (c, cwd, opts) => vLdd(c, cwd, opts),
|
|
71
101
|
};
|
|
72
102
|
const VERIFIER_TYPES = Object.keys(VERIFIERS);
|
|
73
103
|
|
|
@@ -77,7 +107,7 @@ async function runCriterion(c, opts = {}) {
|
|
|
77
107
|
const type = c && c.type;
|
|
78
108
|
const fn = VERIFIERS[type];
|
|
79
109
|
if (!fn) return { ok: false, type: type || '?', detail: 'tipo de critério desconhecido: ' + type, unknown: true };
|
|
80
|
-
try { const r = await fn(c, cwd); return Object.assign({ type, label: c.label || _labelFor(c) }, r); }
|
|
110
|
+
try { const r = await fn(c, cwd, opts); return Object.assign({ type, label: c.label || _labelFor(c) }, r); }
|
|
81
111
|
catch (e) { return { ok: false, type, label: c.label || _labelFor(c), detail: 'erro no verificador: ' + (e.message || e) }; }
|
|
82
112
|
}
|
|
83
113
|
function _labelFor(c) {
|
|
@@ -88,6 +118,7 @@ function _labelFor(c) {
|
|
|
88
118
|
case 'file_contains': return c.path + (c.absent ? ' SEM ' : ' contém ') + '"' + String(c.text).slice(0, 30) + '"';
|
|
89
119
|
case 'http': return (c.method || 'GET') + ' ' + c.url + ' → ' + (c.status || 200);
|
|
90
120
|
case 'port': return 'porta ' + c.port + ' escutando';
|
|
121
|
+
case 'ldd': return 'libs de ' + c.path + ' resolvem' + (c.remote ? ' (VPS)' : '');
|
|
91
122
|
default: return c.type || '?';
|
|
92
123
|
}
|
|
93
124
|
}
|
|
@@ -104,7 +135,7 @@ async function runAll(criteria, opts = {}) {
|
|
|
104
135
|
// ── COMPILADOR de critérios (TaskSpec): o LLM/usuário PROPÕE, o harness VALIDA ────
|
|
105
136
|
// Aceita só tipos conhecidos com os campos obrigatórios preenchidos; descarta o resto
|
|
106
137
|
// (o modelo não impõe critério malformado). Determinístico. Retorna { criteria, dropped }.
|
|
107
|
-
const _REQUIRED = { command: ['cmd'], file_exists: ['path'], file_absent: ['path'], file_contains: ['path', 'text'], http: ['url'], port: ['port'] };
|
|
138
|
+
const _REQUIRED = { command: ['cmd'], file_exists: ['path'], file_absent: ['path'], file_contains: ['path', 'text'], http: ['url'], port: ['port'], ldd: ['path'] };
|
|
108
139
|
function compileCriteria(raw) {
|
|
109
140
|
const list = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.criteria) ? raw.criteria : []);
|
|
110
141
|
const criteria = [], dropped = [];
|
|
@@ -113,7 +144,7 @@ function compileCriteria(raw) {
|
|
|
113
144
|
const missing = _REQUIRED[c.type].some(k => c[k] === undefined || c[k] === null || c[k] === '');
|
|
114
145
|
if (missing) { dropped.push(c); continue; }
|
|
115
146
|
const clean = { type: c.type };
|
|
116
|
-
for (const k of ['cmd', 'exit', 'path', 'text', 'regex', 'flags', 'absent', 'url', 'method', 'status', 'contains', 'port', 'host', 'timeout_s', 'label']) if (c[k] !== undefined) clean[k] = c[k];
|
|
147
|
+
for (const k of ['cmd', 'exit', 'path', 'text', 'regex', 'flags', 'absent', 'url', 'method', 'status', 'contains', 'port', 'host', 'remote', 'timeout_s', 'label']) if (c[k] !== undefined) clean[k] = c[k];
|
|
117
148
|
criteria.push(clean);
|
|
118
149
|
}
|
|
119
150
|
return { criteria, dropped };
|
|
@@ -128,4 +159,4 @@ function deriveFromStack(cwd) {
|
|
|
128
159
|
return out;
|
|
129
160
|
}
|
|
130
161
|
|
|
131
|
-
module.exports = { runCriterion, runAll, compileCriteria, deriveFromStack, VERIFIERS, VERIFIER_TYPES, _labelFor };
|
|
162
|
+
module.exports = { runCriterion, runAll, compileCriteria, deriveFromStack, VERIFIERS, VERIFIER_TYPES, _labelFor, _analyzeLdd };
|