primocode 10.15.1 → 10.16.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 +2 -2
- package/bin/primocode.js +73 -10
- package/lib/act.js +2 -0
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +3 -1
- package/lib/config.js +1 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +264 -0
- package/lib/estudio/index.js +9 -1
- package/lib/estudio/maquina-diretor.js +9 -5
- package/lib/estudio/maquina.js +5 -1
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +93 -1
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +22 -1
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/tools.js +20 -4
- package/lib/ui.js +1 -1
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +119 -46
- package/voz/primo_voz/app.py +582 -24
- package/voz/primo_voz/appmac.py +2 -1
- package/voz/primo_voz/chat.html +1762 -0
- package/voz/primo_voz/conversa.py +17 -32
- package/voz/primo_voz/escuta.py +15 -11
- package/voz/primo_voz/fala_pt.py +151 -0
- package/voz/primo_voz/hud.html +10 -4
- package/voz/primo_voz/hud.py +152 -7
- package/voz/primo_voz/identidade.py +1 -1
- package/voz/primo_voz/transcrever.py +9 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memoria-usuario.js — o PrimoCode lembra de quem a pessoa é.
|
|
3
|
+
*
|
|
4
|
+
* A memória que vale entre sessões, do jeito do Claude Code: um fato por
|
|
5
|
+
* arquivo .md em ~/.primocode/memoria/, e um índice MEMORIA.md com uma linha
|
|
6
|
+
* por fato. O índice entra no contexto de TODA sessão; o corpo de cada fato
|
|
7
|
+
* o modelo lê quando precisa (read_file no caminho).
|
|
8
|
+
*
|
|
9
|
+
* ~/.primocode/memoria/MEMORIA.md - [Título](arquivo.md) — gancho
|
|
10
|
+
* ~/.primocode/memoria/<nome-em-kebab>.md frontmatter (nome, descricao, tipo) + corpo
|
|
11
|
+
*
|
|
12
|
+
* Difere do memory-geral.md (uma lista corrida de frases) e do AGENTS.md da
|
|
13
|
+
* pasta (o resumo da conversa): aqui cada fato tem nome, tipo e pode ser
|
|
14
|
+
* atualizado ou apagado sem duplicar.
|
|
15
|
+
*
|
|
16
|
+
* HOME é injetável, como em lib/estudio/gosto.js: o teste usa uma casa temporária.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
const TIPOS = ['usuario', 'feedback', 'projeto', 'referencia'];
|
|
26
|
+
const LINHAS_DO_INDICE = 200;
|
|
27
|
+
const LIMITE_TEXTO = 8000;
|
|
28
|
+
const INDICE = 'MEMORIA.md';
|
|
29
|
+
|
|
30
|
+
// Cara de segredo: o fato fica em disco e volta em todo prompt, então chave não entra.
|
|
31
|
+
const SEGREDOS = [
|
|
32
|
+
/\bsk-[A-Za-z0-9_-]{8,}/,
|
|
33
|
+
/\bAIza[0-9A-Za-z_-]{10,}/,
|
|
34
|
+
/\bgh[pousr]_[A-Za-z0-9]{10,}/,
|
|
35
|
+
/-----BEGIN [A-Z ]*(?:PRIVATE KEY|CERTIFICATE)?-----/,
|
|
36
|
+
/\bxox[abprs]-[A-Za-z0-9-]{8,}/,
|
|
37
|
+
/\bAKIA[0-9A-Z]{12,}/,
|
|
38
|
+
/\b(?:senha|password|passwd|token|api[_-]?key|secret)\s*[:=]\s*\S+/i,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function pasta(home = os.homedir()) { return path.join(home, '.primocode', 'memoria'); }
|
|
42
|
+
function arquivoIndice(home) { return path.join(pasta(home), INDICE); }
|
|
43
|
+
|
|
44
|
+
/** "Prefere Respostas Curtas!" → "prefere-respostas-curtas". '' se não sobrar nada. */
|
|
45
|
+
function kebab(s) {
|
|
46
|
+
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function umaLinha(s) { return String(s || '').replace(/\s+/g, ' ').trim(); }
|
|
51
|
+
|
|
52
|
+
/** O texto parece ter segredo? Devolve true/false. Puro. */
|
|
53
|
+
function temSegredo(...textos) {
|
|
54
|
+
return textos.some((t) => SEGREDOS.some((re) => re.test(String(t || ''))));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** "usuario-prefere-curto" → "Usuario prefere curto": o título que aparece no índice. */
|
|
58
|
+
function titulo(nome) {
|
|
59
|
+
const t = String(nome || '').replace(/-/g, ' ');
|
|
60
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function montar({ nome, descricao, tipo, texto }) {
|
|
64
|
+
return `---\nnome: ${nome}\ndescricao: ${descricao}\ntipo: ${tipo}\n---\n\n${String(texto).trim()}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Lê o frontmatter simples (chave: valor) e o corpo. null se não tem. */
|
|
68
|
+
function analisar(conteudo) {
|
|
69
|
+
const m = String(conteudo || '').match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
70
|
+
if (!m) return null;
|
|
71
|
+
const meta = {};
|
|
72
|
+
for (const l of m[1].split('\n')) {
|
|
73
|
+
const i = l.indexOf(':');
|
|
74
|
+
if (i > 0) meta[l.slice(0, i).trim()] = l.slice(i + 1).trim();
|
|
75
|
+
}
|
|
76
|
+
return { ...meta, corpo: m[2].trim() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Todas as memórias, em ordem alfabética do arquivo. */
|
|
80
|
+
function listar(home) {
|
|
81
|
+
let nomes = [];
|
|
82
|
+
try { nomes = fs.readdirSync(pasta(home)); } catch { return []; }
|
|
83
|
+
const saida = [];
|
|
84
|
+
for (const f of nomes.sort()) {
|
|
85
|
+
if (!f.endsWith('.md') || f === INDICE) continue;
|
|
86
|
+
let a = null;
|
|
87
|
+
try { a = analisar(fs.readFileSync(path.join(pasta(home), f), 'utf-8')); } catch { /* ilegível: pula */ }
|
|
88
|
+
if (!a) continue;
|
|
89
|
+
saida.push({ nome: a.nome || f.slice(0, -3), descricao: a.descricao || '', tipo: a.tipo || '', arquivo: f, corpo: a.corpo });
|
|
90
|
+
}
|
|
91
|
+
return saida;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Refaz o MEMORIA.md a partir dos arquivos: o índice nunca desanda do que existe. */
|
|
95
|
+
function reindexar(home) {
|
|
96
|
+
const linhas = listar(home).map((m) => `- [${titulo(m.nome)}](${m.arquivo}) — ${m.descricao}`);
|
|
97
|
+
fs.mkdirSync(pasta(home), { recursive: true });
|
|
98
|
+
fs.writeFileSync(arquivoIndice(home), linhas.length ? linhas.join('\n') + '\n' : '', 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Salva ou atualiza (mesmo nome = mesmo arquivo, sem duplicar).
|
|
103
|
+
* Devolve { ok, nome, arquivo, criado } ou { ok:false, error }.
|
|
104
|
+
*/
|
|
105
|
+
function salvar({ nome, descricao, tipo, texto } = {}, home) {
|
|
106
|
+
const slug = kebab(nome);
|
|
107
|
+
if (!slug) return { ok: false, error: 'Dê um nome curto pra memória (ex.: "prefere-respostas-curtas").' };
|
|
108
|
+
const desc = umaLinha(descricao);
|
|
109
|
+
if (!desc) return { ok: false, error: 'Falta a descrição: uma linha dizendo do que se trata.' };
|
|
110
|
+
const t = String(tipo || '').toLowerCase();
|
|
111
|
+
if (!TIPOS.includes(t)) return { ok: false, error: `Tipo inválido. Use um destes: ${TIPOS.join(', ')}.` };
|
|
112
|
+
const corpo = String(texto || '').trim();
|
|
113
|
+
if (!corpo) return { ok: false, error: 'Falta o texto da memória.' };
|
|
114
|
+
if (corpo.length > LIMITE_TEXTO) return { ok: false, error: `Texto grande demais (máx. ${LIMITE_TEXTO} letras). Resuma.` };
|
|
115
|
+
if (temSegredo(nome, desc, corpo)) {
|
|
116
|
+
return { ok: false, error: 'Isso tem cara de segredo (chave, token ou senha). Não guardo isso na memória. Tire o segredo e tente de novo.' };
|
|
117
|
+
}
|
|
118
|
+
const arquivo = `${slug}.md`;
|
|
119
|
+
const caminho = path.join(pasta(home), arquivo);
|
|
120
|
+
const criado = !fs.existsSync(caminho);
|
|
121
|
+
try {
|
|
122
|
+
fs.mkdirSync(pasta(home), { recursive: true });
|
|
123
|
+
fs.writeFileSync(caminho, montar({ nome: slug, descricao: desc, tipo: t, texto: corpo }), 'utf-8');
|
|
124
|
+
reindexar(home);
|
|
125
|
+
} catch (e) { return { ok: false, error: `Não consegui gravar: ${e.message}` }; }
|
|
126
|
+
return { ok: true, nome: slug, arquivo, criado };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Apaga pelo nome. Devolve { ok } ou { ok:false, error }. */
|
|
130
|
+
function apagar(nome, home) {
|
|
131
|
+
const slug = kebab(nome);
|
|
132
|
+
const caminho = path.join(pasta(home), `${slug}.md`);
|
|
133
|
+
if (!slug || !fs.existsSync(caminho)) return { ok: false, error: `Não achei a memória "${nome}".` };
|
|
134
|
+
try { fs.unlinkSync(caminho); reindexar(home); } catch (e) { return { ok: false, error: `Não consegui apagar: ${e.message}` }; }
|
|
135
|
+
return { ok: true, nome: slug };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** O índice, cortado em `max` linhas (a cabeça fica). '' se não há memória. */
|
|
139
|
+
function carregarIndice(home, max = LINHAS_DO_INDICE) {
|
|
140
|
+
let t = '';
|
|
141
|
+
try { t = fs.readFileSync(arquivoIndice(home), 'utf-8'); } catch { return ''; }
|
|
142
|
+
const linhas = t.split('\n').filter((l) => l.trim());
|
|
143
|
+
if (linhas.length <= max) return linhas.join('\n');
|
|
144
|
+
return linhas.slice(0, max).join('\n') + `\n(… cortado: mais ${linhas.length - max} memórias, use /memoria)`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** As regras de quando salvar. Entram no contexto junto com o índice. */
|
|
148
|
+
const INSTRUCAO = '[memória]\n'
|
|
149
|
+
+ 'Você tem memória que dura entre sessões, em ~/.primocode/memoria/ (ferramenta `lembrar`: ação salvar ou apagar). '
|
|
150
|
+
+ 'O índice está abaixo; para ler uma memória, read_file no arquivo dela.\n'
|
|
151
|
+
+ '- SALVE quando a pessoa corrige um jeito de trabalhar, diz uma preferência, conta quem é, ou dá um fato de projeto que não está no código. '
|
|
152
|
+
+ 'tipo: usuario (quem é, como trabalha), feedback (correção ou preferência), projeto (fato do projeto), referencia (onde achar algo).\n'
|
|
153
|
+
+ '- NÃO salve o que o código ou o git já mostram, nem o que só vale para esta conversa. Nunca salve chave, token ou senha.\n'
|
|
154
|
+
+ '- ANTES de salvar, confira o índice: se já existe uma que cobre, salve com o MESMO nome para atualizar, em vez de duplicar.';
|
|
155
|
+
|
|
156
|
+
/** O bloco que entra no contexto da sessão: instrução + índice. */
|
|
157
|
+
/* No servidor (plano grátis) o contexto fixo é curto: o índice entra só com as primeiras `linhas`, e o resto
|
|
158
|
+
vira o aviso de corte. Com provedor próprio (/api) entra até LINHAS_DO_INDICE. */
|
|
159
|
+
function blocoDoContexto(home, { linhas = LINHAS_DO_INDICE } = {}) {
|
|
160
|
+
const indice = carregarIndice(home, linhas);
|
|
161
|
+
return INSTRUCAO + (indice ? `\n\n[MEMORIA.md — o que você já sabe sobre esta pessoa]\n${indice}` : '\n\n(Ainda não há memórias salvas.)');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { TIPOS, INDICE, LINHAS_DO_INDICE, pasta, arquivoIndice, kebab, temSegredo, listar, salvar, apagar, reindexar, carregarIndice, blocoDoContexto, INSTRUCAO };
|
package/lib/nuvem.js
CHANGED
|
@@ -300,6 +300,27 @@ async function lerConversa(id, { limite = 200 } = {}) {
|
|
|
300
300
|
} catch (e) { return { ok: false, error: e.message }; }
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Apaga uma conversa. O Firestore não apaga a subcoleção junto com o
|
|
305
|
+
* documento, então as mensagens vão antes, em lotes, e a conversa por último.
|
|
306
|
+
*/
|
|
307
|
+
async function apagarConversa(id) {
|
|
308
|
+
const s = await abrir();
|
|
309
|
+
if (!s) return { ok: false, error: 'não conectado' };
|
|
310
|
+
const base = `${BASE(s.projeto)}/users/${s.uid}/${COLECAO}/${id}`;
|
|
311
|
+
const auth = { Authorization: `Bearer ${s.idToken}` };
|
|
312
|
+
try {
|
|
313
|
+
for (let volta = 0; volta < 50; volta++) {
|
|
314
|
+
const r = await httpsJson(`${base}/messages?pageSize=300`, 'GET', null, auth);
|
|
315
|
+
const docs = r.documents || [];
|
|
316
|
+
if (!docs.length) break;
|
|
317
|
+
for (const d of docs) await httpsJson(`${FIRESTORE}/v1/${d.name}`, 'DELETE', null, auth);
|
|
318
|
+
}
|
|
319
|
+
await httpsJson(base, 'DELETE', null, auth);
|
|
320
|
+
return { ok: true };
|
|
321
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
322
|
+
}
|
|
323
|
+
|
|
303
324
|
// ── o gravador da sessão ─────────────────────────────────────────────────
|
|
304
325
|
|
|
305
326
|
/** Título a partir da primeira frase: uma linha, curta, sem cortar no meio de
|
|
@@ -388,7 +409,7 @@ module.exports = {
|
|
|
388
409
|
cifrar, decifrar, cifrarJson, decifrarJson,
|
|
389
410
|
gravador, tituloDe,
|
|
390
411
|
abrir, esquecer,
|
|
391
|
-
salvarConversa, salvarMensagem, listarConversas, lerConversa,
|
|
412
|
+
salvarConversa, salvarMensagem, listarConversas, lerConversa, apagarConversa,
|
|
392
413
|
COLECAO, VERSAO_CIFRA, ITERACOES, TAM_IV,
|
|
393
414
|
// A tradução para o formato do Firestore sai daqui porque é ela que decide
|
|
394
415
|
// o que vira `stringValue` em claro e o que vira `mapValue` cifrado.
|
package/lib/repeticao.js
CHANGED
|
@@ -100,7 +100,7 @@ function consultaDe(nome, args) {
|
|
|
100
100
|
* ficam fora da época. */
|
|
101
101
|
const OBSERVAM = new Set(['read_file', 'list_dir', 'glob', 'grep', 'browser_read', 'browser_screenshot',
|
|
102
102
|
'desktop_screenshot', 'desktop_size', 'desktop_windows', 'desktop_elements']);
|
|
103
|
-
const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'pesquisar_referencia', ...REFERENCIAS]);
|
|
103
|
+
const NAO_MUDAM = new Set(['write_todos', 'remember_general', 'lembrar', 'pesquisar_referencia', ...REFERENCIAS]);
|
|
104
104
|
const SHELL_SO_LEITURA = /^\s*(ls|cat|head|tail|pwd|echo|grep|rg|find|wc|tree|stat|file|which|type|du|df|env|printenv|date|git\s+(status|log|diff|show|branch|remote))\b[^;&|>]*$/;
|
|
105
105
|
|
|
106
106
|
function comandoDe(args) {
|
package/lib/sandbox.js
CHANGED
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
* hard_requirement (recusa)
|
|
9
9
|
*
|
|
10
10
|
* O que o comando pode: escrever na pasta do projeto, em /tmp e em
|
|
11
|
-
* ~/.primocode/cache; ler quase tudo, MENOS
|
|
12
|
-
*
|
|
11
|
+
* ~/.primocode/cache; ler quase tudo, MENOS os arquivos de credencial da
|
|
12
|
+
* LEITURA_NEGADA_HOME (chaves, tokens, a conta do PrimoCode, o Keychain) e
|
|
13
|
+
* .env*. Rede ligada (o `npm install` precisa), com opção de cortar.
|
|
14
|
+
* No macOS ainda fica de fora: osascript, o comando `security` (Keychain) e
|
|
15
|
+
* escrever hook em .git/hooks (rodaria FORA da cerca no próximo commit).
|
|
16
|
+
* O ssh-agent da pessoa fica fora por padrão (review do Arthur, #162): sem
|
|
17
|
+
* ele, "não lê ~/.ssh" vale de verdade, e não só para o arquivo.
|
|
13
18
|
* Quando bloqueia, a mensagem diz POR QUE, para o modelo aprender.
|
|
14
19
|
*
|
|
15
20
|
* Config em ~/.primocode/config.json:
|
|
16
21
|
* "sandbox": { "ligado": true, "modo": "best_effort"|"hard_requirement",
|
|
17
|
-
* "rede": true, "env_permitidas": ["MINHA_KEY"],
|
|
22
|
+
* "rede": true, "ssh_agent": false, "env_permitidas": ["MINHA_KEY"],
|
|
18
23
|
* "escrita_extra": ["~/.npm"] }
|
|
19
24
|
*/
|
|
20
25
|
'use strict';
|
|
@@ -25,10 +30,14 @@ const path = require('path');
|
|
|
25
30
|
const { execFile, spawnSync } = require('child_process');
|
|
26
31
|
|
|
27
32
|
const MODOS = ['best_effort', 'hard_requirement'];
|
|
28
|
-
const LEITURA_NEGADA_HOME = ['.ssh', '.aws', '.config/gcloud', '.npmrc'
|
|
33
|
+
const LEITURA_NEGADA_HOME = ['.ssh', '.aws', '.config/gcloud', '.npmrc',
|
|
34
|
+
'.primocode/conta.json', '.netrc', '.git-credentials', '.config/gh', '.docker', '.kube', 'Library/Keychains'];
|
|
35
|
+
// Socket do ssh-agent do macOS (launchd). O caminho exato vem do SSH_AUTH_SOCK; a regex pega os outros.
|
|
36
|
+
const SOCKET_SSH_MAC = '^/private/(tmp|var/run)/com\\.apple\\.launchd\\.[^/]+/Listeners$';
|
|
29
37
|
const SECRETA = /(_KEY|_TOKEN|SECRET)/i;
|
|
30
38
|
|
|
31
39
|
let _avisou = false;
|
|
40
|
+
let _avisouDesligado = false;
|
|
32
41
|
let _forcaMecanismo; // teste: undefined = detectar; null = ausente; 'seatbelt'|'bwrap'
|
|
33
42
|
|
|
34
43
|
function arquivoConfig(home) { return path.join(home || os.homedir(), '.primocode', 'config.json'); }
|
|
@@ -43,6 +52,7 @@ function config(home) {
|
|
|
43
52
|
ligado: typeof s.ligado === 'boolean' ? s.ligado : process.platform === 'darwin',
|
|
44
53
|
modo: MODOS.includes(s.modo) ? s.modo : 'best_effort',
|
|
45
54
|
rede: s.rede !== false,
|
|
55
|
+
ssh_agent: s.ssh_agent === true,
|
|
46
56
|
env_permitidas: Array.isArray(s.env_permitidas) ? s.env_permitidas.map(String) : [],
|
|
47
57
|
escrita_extra: Array.isArray(s.escrita_extra) ? s.escrita_extra.map(String) : [],
|
|
48
58
|
};
|
|
@@ -68,6 +78,17 @@ function mecanismo() {
|
|
|
68
78
|
return null;
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
/** Linha para a PESSOA ver no terminal (o stderr do comando só o modelo lê). Vai pro
|
|
82
|
+
* stderr do processo para não sujar a saída do `-p`. */
|
|
83
|
+
function avisarPessoa(msg) {
|
|
84
|
+
try { console.error(require('./ui.js').c.warn(` ⎿ ${msg}`)); } catch { /* sem tela */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function socketSsh() {
|
|
88
|
+
const s = process.env.SSH_AUTH_SOCK;
|
|
89
|
+
try { return s && fs.statSync(s).isSocket() ? real(s) : null; } catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function real(p) { try { return fs.realpathSync(p); } catch { return path.resolve(p); } }
|
|
72
93
|
function expandir(p, home) { return p.startsWith('~') ? path.join(home, p.slice(1)) : p; }
|
|
73
94
|
const sbq = (s) => '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
@@ -86,8 +107,20 @@ function perfilSeatbelt(projectDir, cfg = config(), home = os.homedir()) {
|
|
|
86
107
|
const l = ['(version 1)', '(allow default)', '(deny file-write*)'];
|
|
87
108
|
for (const p of escrita) l.push(`(allow file-write* (subpath ${sbq(p)}))`);
|
|
88
109
|
l.push('(allow file-write* (literal "/dev/null") (literal "/dev/tty") (literal "/dev/dtracehelper") (regex #"^/dev/fd/"))');
|
|
110
|
+
// Hook em .git/hooks roda fora da cerca no próximo `git commit` da pessoa. O .sample do `git init` passa.
|
|
111
|
+
l.push('(deny file-write* (regex #"/\\.git/hooks/[^/]+$"))');
|
|
112
|
+
l.push('(allow file-write* (regex #"/\\.git/hooks/[^/]+\\.sample$"))');
|
|
89
113
|
for (const p of negadosLeitura) l.push(`(deny file-read* (subpath ${sbq(real(p))}) (subpath ${sbq(p)}))`);
|
|
90
114
|
l.push('(deny file-read* (regex #"/\\.env($|\\.[^/]*$)"))');
|
|
115
|
+
// osascript manda o Terminal/Finder fazer o que a cerca não deixa; `security` lê o Keychain.
|
|
116
|
+
l.push('(deny process-exec (literal "/usr/bin/osascript") (literal "/usr/bin/security"))');
|
|
117
|
+
l.push('(deny appleevent-send)');
|
|
118
|
+
l.push('(deny mach-lookup (global-name "com.apple.SecurityServer") (global-name "com.apple.securityd"))');
|
|
119
|
+
if (!cfg.ssh_agent) {
|
|
120
|
+
const sock = socketSsh();
|
|
121
|
+
l.push(`(deny network-outbound (remote unix-socket (path-regex #"${SOCKET_SSH_MAC}")))`);
|
|
122
|
+
if (sock) l.push(`(deny network-outbound (remote unix-socket (path-literal ${sbq(sock)})))`);
|
|
123
|
+
}
|
|
91
124
|
if (!cfg.rede) l.push('(deny network*)');
|
|
92
125
|
return l.join('\n');
|
|
93
126
|
}
|
|
@@ -106,6 +139,11 @@ function argsBwrap(projectDir, cfg = config(), home = os.homedir()) {
|
|
|
106
139
|
if (/^\.env($|\.)/.test(f)) a.push('--ro-bind', '/dev/null', path.join(real(projectDir), f));
|
|
107
140
|
}
|
|
108
141
|
} catch { /* pasta sumiu */ }
|
|
142
|
+
// Montado DEPOIS do --bind do projeto, para valer por cima dele. [VERIFICAR] no Linux.
|
|
143
|
+
const hooks = path.join(real(projectDir), '.git', 'hooks');
|
|
144
|
+
if (fs.existsSync(hooks)) a.push('--ro-bind', hooks, hooks);
|
|
145
|
+
const sock = cfg.ssh_agent ? null : socketSsh();
|
|
146
|
+
if (sock) a.push('--ro-bind', '/dev/null', sock);
|
|
109
147
|
if (!cfg.rede) a.push('--unshare-net');
|
|
110
148
|
a.push('--die-with-parent', '--chdir', real(projectDir));
|
|
111
149
|
return a;
|
|
@@ -115,7 +153,16 @@ function argsBwrap(projectDir, cfg = config(), home = os.homedir()) {
|
|
|
115
153
|
function envolver(command, projectDir, opts = {}) {
|
|
116
154
|
const home = opts.home || os.homedir();
|
|
117
155
|
const cfg = opts.cfg || config(home);
|
|
118
|
-
if (!cfg.ligado)
|
|
156
|
+
if (!cfg.ligado) {
|
|
157
|
+
let aviso = null;
|
|
158
|
+
if (!_avisouDesligado) {
|
|
159
|
+
_avisouDesligado = true;
|
|
160
|
+
aviso = process.platform === 'darwin'
|
|
161
|
+
? 'aviso: o sandbox está desligado; os comandos do agente rodam sem cerca. /sandbox on liga.'
|
|
162
|
+
: 'aviso: o sandbox vem desligado fora do macOS; os comandos do agente rodam sem cerca. /sandbox on liga (no Linux precisa do bwrap).';
|
|
163
|
+
}
|
|
164
|
+
return { semCerca: true, motivo: 'desligado', aviso };
|
|
165
|
+
}
|
|
119
166
|
const mec = mecanismo();
|
|
120
167
|
if (!mec) {
|
|
121
168
|
if (cfg.modo === 'hard_requirement') {
|
|
@@ -132,14 +179,15 @@ function envolver(command, projectDir, opts = {}) {
|
|
|
132
179
|
/** Se o comando falhou por causa da cerca, diz por quê (e qual caminho). */
|
|
133
180
|
function explicar(stderr, projectDir, cfg, home) {
|
|
134
181
|
const txt = String(stderr || '');
|
|
135
|
-
const m = /(?:^|\n)(?:[^\n:]*:\s*)?(\/[^\n:]+?|\.{1,2}\/[^\n:]+?|[^\s:'"]+?):?\s*(?:Operation not permitted|Permission denied|Read-only file system)/.exec(txt)
|
|
182
|
+
const m = /(?:^|\n)(?:[^\n:]*:\s*)?(\/[^\n:]+?|\.{1,2}\/[^\n:]+?|[^\s:'"]+?):?\s*(?:(?:open|read|stat|lstat|opendir|write):\s*)?(?:Operation not permitted|Permission denied|Read-only file system)/.exec(txt)
|
|
136
183
|
|| /(?:Operation not permitted|Permission denied|Read-only file system)[^\n]*?['"]([^'"\n]+)['"]/.exec(txt);
|
|
137
184
|
if (!m) return null;
|
|
138
185
|
const alvo = path.resolve(projectDir, m[1].trim());
|
|
139
186
|
const { negadosLeitura } = caminhos(projectDir, cfg, home);
|
|
140
187
|
const secreto = negadosLeitura.some((p) => alvo === p || alvo.startsWith(p + path.sep) || alvo.startsWith(real(p) + path.sep))
|
|
141
188
|
|| /(^|\/)\.env($|\.)/.test(alvo);
|
|
142
|
-
if (secreto) return `o sandbox bloqueou leitura de segredo: ${alvo} (
|
|
189
|
+
if (secreto) return `o sandbox bloqueou leitura de segredo: ${alvo} (${LEITURA_NEGADA_HOME.map((p) => '~/' + p).join(', ')} e .env* ficam fora do alcance). Não tente ler isso; peça ao usuário o que precisa.`;
|
|
190
|
+
if (/\/\.git\/hooks\//.test(alvo)) return `o sandbox bloqueou escrita em hook do git: ${alvo}. Hook roda fora da cerca; se o usuário quiser um, ele instala.`;
|
|
143
191
|
if (/Read-only|Operation not permitted|Permission denied/.test(txt)) {
|
|
144
192
|
return `o sandbox bloqueou escrita fora do projeto: ${alvo}. Escreva só dentro de ${projectDir}, em /tmp ou em ~/.primocode/cache.`;
|
|
145
193
|
}
|
|
@@ -168,7 +216,7 @@ function executar(command, { projectDir, timeout = 60000, env = process.env, hom
|
|
|
168
216
|
const termina = (error, stdout, stderr) => {
|
|
169
217
|
let err = stderr || '';
|
|
170
218
|
const info = { mecanismo: e.mec || null };
|
|
171
|
-
if (e.aviso) err = e.aviso + '\n' + err;
|
|
219
|
+
if (e.aviso) { err = e.aviso + '\n' + err; avisarPessoa(e.aviso); }
|
|
172
220
|
if (error && e.mec) {
|
|
173
221
|
const why = explicar(err, cwd, cfg, e.home);
|
|
174
222
|
if (why) { err = err + '\n[sandbox] ' + why; info.bloqueou = why; }
|
|
@@ -191,7 +239,7 @@ function estado(home) {
|
|
|
191
239
|
|
|
192
240
|
function _teste({ mecanismo: m, avisou } = {}) {
|
|
193
241
|
if (m !== undefined) _forcaMecanismo = m === 'auto' ? undefined : m;
|
|
194
|
-
if (avisou !== undefined) _avisou = avisou;
|
|
242
|
+
if (avisou !== undefined) { _avisou = avisou; _avisouDesligado = avisou; }
|
|
195
243
|
}
|
|
196
244
|
|
|
197
245
|
module.exports = { config, salvar, estado, mecanismo, envolver, executar, explicar, filtrarEnv, perfilSeatbelt, argsBwrap, MODOS, _teste };
|
package/lib/tools.js
CHANGED
|
@@ -274,10 +274,13 @@ function grepFiles({ pattern, glob, ignoreCase, contexto = 0, maxResultados = 10
|
|
|
274
274
|
USER, NODE_*, nvm etc. continuam — sem eles `npm`, `node` e `git` quebram.
|
|
275
275
|
Na dúvida o nome sai: um token a mais escondido custa pouco, um a menos
|
|
276
276
|
vaza. Só o caminho do agente (execute → run_shell) é filtrado; os usos
|
|
277
|
-
internos (abrir terminal, desinstalar) seguem com o ambiente inteiro.
|
|
277
|
+
internos (abrir terminal, desinstalar) seguem com o ambiente inteiro.
|
|
278
|
+
O SSH_AUTH_SOCK também sai (review do Arthur, #162): com ele o agente usa o
|
|
279
|
+
ssh-agent da pessoa (`git push`, `ssh host`) sem ler ~/.ssh. Volta só com
|
|
280
|
+
`/sandbox ssh on`. */
|
|
278
281
|
const NOME_SECRETO = /(TOKEN|SECRET|KEY|PASSWORD|PASSWD|CREDENTIAL|AUTH|COOKIE|SESSION)/i;
|
|
279
282
|
const PREFIXO_SECRETO = /^(AWS_|OPENAI_|ANTHROPIC_|GEMINI_|GOOGLE_API|GROQ_|OPENROUTER_|AZURE_|HF_|HUGGING)/i;
|
|
280
|
-
const SEMPRE_PASSA = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'PWD'
|
|
283
|
+
const SEMPRE_PASSA = new Set(['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'TMPDIR', 'PWD']);
|
|
281
284
|
|
|
282
285
|
function ambienteLimpo(env = process.env, permitidas = []) {
|
|
283
286
|
const limpo = {};
|
|
@@ -289,6 +292,10 @@ function ambienteLimpo(env = process.env, permitidas = []) {
|
|
|
289
292
|
return limpo;
|
|
290
293
|
}
|
|
291
294
|
|
|
295
|
+
function permitidasNoShell(cfg = sandbox.config()) {
|
|
296
|
+
return cfg.ssh_agent ? [...cfg.env_permitidas, 'SSH_AUTH_SOCK'] : cfg.env_permitidas;
|
|
297
|
+
}
|
|
298
|
+
|
|
292
299
|
async function runShell(command, timeout = 60000, cwd, env) {
|
|
293
300
|
return new Promise((resolve) => {
|
|
294
301
|
exec(command, { timeout, cwd: cwd || process.cwd(), maxBuffer: 10 * 1024 * 1024, env: env || process.env }, (error, stdout, stderr) => {
|
|
@@ -668,7 +675,7 @@ async function despachar(name, args, ctx) {
|
|
|
668
675
|
case 'run_shell':
|
|
669
676
|
// Dentro da cerca (lib/sandbox.js); desligada ou sem mecanismo cai no exec de sempre.
|
|
670
677
|
return sandbox.executar(args.command, { projectDir, timeout: args.timeout_ms || 60000,
|
|
671
|
-
env: ambienteLimpo(process.env,
|
|
678
|
+
env: ambienteLimpo(process.env, permitidasNoShell()) });
|
|
672
679
|
case 'run_in_new_terminal':
|
|
673
680
|
return runInNewTerminal(args.command, args.cwd || projectDir);
|
|
674
681
|
case 'open_in_browser': {
|
|
@@ -712,6 +719,15 @@ async function despachar(name, args, ctx) {
|
|
|
712
719
|
return { ok: true, gravado: entry };
|
|
713
720
|
}
|
|
714
721
|
|
|
722
|
+
// A memória que dura entre sessões (lib/memoria-usuario.js).
|
|
723
|
+
case 'lembrar': {
|
|
724
|
+
const mem = require('./memoria-usuario.js');
|
|
725
|
+
const acao = String(args.acao || 'salvar').toLowerCase();
|
|
726
|
+
if (acao === 'apagar') return mem.apagar(args.nome);
|
|
727
|
+
if (acao === 'salvar') return mem.salvar({ nome: args.nome, descricao: args.descricao, tipo: args.tipo, texto: args.texto });
|
|
728
|
+
return { ok: false, error: 'Ação inválida. Use "salvar" ou "apagar".' };
|
|
729
|
+
}
|
|
730
|
+
|
|
715
731
|
// ── MCP: o serviço pela API dele, antes do mouse (lib/mcp.js) ──
|
|
716
732
|
case 'mcp_procurar':
|
|
717
733
|
return require('./mcp.js').procurar(args.busca || args.query || args.servico || '');
|
|
@@ -978,7 +994,7 @@ async function uninstall() {
|
|
|
978
994
|
}
|
|
979
995
|
|
|
980
996
|
module.exports = {
|
|
981
|
-
fmtBytes, treeDir, runShell, ambienteLimpo, openInBrowser, runInNewTerminal,
|
|
997
|
+
fmtBytes, treeDir, runShell, ambienteLimpo, permitidasNoShell, openInBrowser, runInNewTerminal,
|
|
982
998
|
execute, uninstall, resolveInProject, globToRegex, globFiles, grepFiles, editFile,
|
|
983
999
|
multiEdit, writeTodos, getTodos,
|
|
984
1000
|
liberarLeitura, pastasDeConsulta, esquecerConsultas, resolverParaLer,
|
package/lib/ui.js
CHANGED
|
@@ -557,7 +557,7 @@ const ROTULOS = {
|
|
|
557
557
|
desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
|
|
558
558
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
559
559
|
estudio_midia:'Estúdio·mídia', estudio_ae:'Estúdio·After Effects', estudio_motion:'Estúdio·Motion', estudio_editar:'Estúdio·Editar', construir_app:'Construir',
|
|
560
|
-
remember_general:'Lembrar',
|
|
560
|
+
remember_general:'Lembrar', lembrar:'Memória',
|
|
561
561
|
mcp_procurar:'MCP·procurar', mcp_conectar:'MCP·conectar', mcp_usar:'MCP',
|
|
562
562
|
};
|
|
563
563
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.16.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
11
|
"postinstall": "node lib/preparar.js || true",
|
|
12
12
|
"test:server": "cd server && npm test",
|
|
13
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/primo-voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js"
|
|
13
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/primo-voz.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/casar.test.js && node test/recado.test.js && node test/releitura.test.js && node test/snapshot.test.js && node test/video.test.js && node test/limpeza.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/conta.test.mjs && node test/planos-tela.test.js && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js && node test/cinema.test.js && node test/producao.test.js && node test/ende.test.js && node test/capa.test.js && node test/broll.test.js && node test/morfar.test.js && node test/motion.test.js && node test/motion-roteiro.test.js && node test/kit-motion.test.js && node test/gerativo.test.js && node test/gerativo-identidade.test.js && node test/gerativo-moldura.test.js && node test/gerativo-heroi.test.js && node test/gerativo-ambiente.test.js && node test/gerativo-variedade.test.js && node test/hibrido.test.js && node test/batida.test.js && node test/estudio-acervo.test.js && node test/estudio-diretor.test.js && node test/estudio-marca.test.js && node test/estudio-novo-projeto.test.js && node test/estudio-plano.test.js && node test/estudio-slides.test.js && node test/estudio-video.test.js && node test/revisor.test.js && node test/conferencia.test.js && node test/cor.test.js && node test/assinatura.test.js && node test/biblioteca.test.js && node test/mcp.test.js && node test/mcp-http.test.js && node test/personas.test.js && node test/efeitos.test.js && node test/auto.test.js && node test/colar.test.js && node test/rolar.test.js && node test/orquestra.test.js && node test/shell-limpo.test.js && node test/editar-remotion.test.js && node test/variacao3d.test.js && node test/portao-skill.test.js && node test/clones.test.js && node test/kokoro.test.js && node test/fluido.test.js && node test/zoom.test.js && node test/personagem.test.js && node test/tela.test.js && node test/margem.test.js && node test/travado.test.js && node test/leitura.test.js && node test/producao-provedores.test.js && node test/broll-frase.test.js && node test/vazamento.test.js && node test/colecao.test.js && node test/mundo.test.js && node test/mcp-procurar.test.js && node test/selecao.test.js && node test/nome-curto.test.js && node test/agents-md.test.js && node test/outra-pasta.test.js && node test/estudio-sem-id.test.js && node test/estudio-objeto-do-site.test.js && node test/cenarios.test.js && node test/movimentos.test.js && node test/gerativo-so-fala.test.js && node test/estilos.test.js && node test/proporcao.test.js && node test/cena.test.js && node test/sem-narracao.test.js && node test/camadas.test.js && node test/gerativo-referencias.test.js && node test/voicestudio.test.js && node test/effectcraft.test.js && node test/projetos.test.js && node test/maquina.test.js && node test/maquina-som.test.js && node test/maquina-voz.test.js && node test/prova-fresca.test.js && node test/cota-video.test.js && node test/hooks.test.js && node test/modo-plano.test.js && node test/headless.test.js && node test/retomar.test.js && node test/sandbox.test.js && node test/exportar.test.js && node test/editor-video.test.js && node test/marca-kit.test.js && node test/preparar.test.js && node test/landing-planos.test.js && node test/limites.test.js && node test/app-chat-cli.test.js && node test/app-chat-ponte.test.js && node test/gosto-motion.test.js && node test/desktop-vivo.test.js && node test/memoria-usuario.test.js && node test/landing-instalar.test.js"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18.17.0"
|