primocode 8.1.0 → 8.1.1
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 +3 -3
- package/lib/browser.js +5 -31
- package/lib/config.js +8 -3
- package/lib/desktop.js +13 -2
- package/lib/modo.js +1 -1
- package/lib/referencia.js +28 -0
- package/lib/tools.js +30 -8
- package/lib/ui.js +1 -68
- package/package.json +1 -1
- package/studio/server.py +24 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode
|
|
1
|
+
# PrimoCode v8.1.1
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code
|
|
12
|
+
▐▛███▜▌ Primo Code v8.1.1
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -263,7 +263,7 @@ primocode -f app.js "onde está o bug?" # com arquivo anexado
|
|
|
263
263
|
| `/model top\|main\|fast` | **Todos grátis.** top = o mais capaz (550B, 1M de contexto), main = agente de código (padrão), fast = o mais rápido. Aceita qualquer ID do OpenRouter |
|
|
264
264
|
| `/attach <arquivo>` | Anexa arquivo ao próximo pedido |
|
|
265
265
|
| `/detach` · `/files` | Remove / lista anexos |
|
|
266
|
-
| `/tudo` | Lista as
|
|
266
|
+
| `/tudo` | Lista as 44 ferramentas que ele sabe usar, por grupo |
|
|
267
267
|
| `/tree [n]` | Árvore do projeto atual |
|
|
268
268
|
| `/read <arquivo>` | Mostra um arquivo do projeto |
|
|
269
269
|
| `/run <comando>` | Executa no shell, dentro da pasta do projeto |
|
package/lib/browser.js
CHANGED
|
@@ -88,8 +88,9 @@ const APPS_MAC = [
|
|
|
88
88
|
*
|
|
89
89
|
* Um app rodando de dentro do `.dmg` funciona, mas é frágil: o disco é
|
|
90
90
|
* somente-leitura, o macOS o executa por AppTranslocation (caminho temporário
|
|
91
|
-
* aleatório) e ele desaparece na hora que o disco for ejetado. Achamos e
|
|
92
|
-
*
|
|
91
|
+
* aleatório) e ele desaparece na hora que o disco for ejetado. Achamos e
|
|
92
|
+
* usamos mesmo assim — sem aviso adicional, o disco ejetado já falha na hora
|
|
93
|
+
* de abrir o navegador, o que já é sinal suficiente do que aconteceu.
|
|
93
94
|
*/
|
|
94
95
|
function procurarEmVolumes() {
|
|
95
96
|
const bases = [];
|
|
@@ -107,24 +108,6 @@ function procurarEmVolumes() {
|
|
|
107
108
|
return null;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
-
* O navegador achado está num lugar problemático? Devolve o aviso, ou null.
|
|
112
|
-
*
|
|
113
|
-
* Não bloqueia nada: o controle funciona de dentro do disco montado. Só avisa,
|
|
114
|
-
* porque quem ejetar o disco vai ver a ferramenta parar de funcionar sem entender
|
|
115
|
-
* o motivo.
|
|
116
|
-
*/
|
|
117
|
-
function avisoDeLocal(caminho) {
|
|
118
|
-
if (!caminho) return null;
|
|
119
|
-
if (caminho.startsWith('/Volumes/')) {
|
|
120
|
-
const app = (caminho.match(/\/Volumes\/[^/]+\/([^/]+\.app)/) || [, 'o navegador'])[1];
|
|
121
|
-
return `${app} está rodando do disco de instalação, não instalado. `
|
|
122
|
-
+ 'Funciona, mas para de funcionar quando você ejetar esse disco — '
|
|
123
|
-
+ 'arraste o app para a pasta Aplicativos.';
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
111
|
function httpGetJson(url, timeoutMs = 3000) {
|
|
129
112
|
return new Promise((resolve, reject) => {
|
|
130
113
|
const req = http.get(url, (res) => {
|
|
@@ -450,8 +433,6 @@ async function open(url) {
|
|
|
450
433
|
return { ok: true, url };
|
|
451
434
|
}
|
|
452
435
|
|
|
453
|
-
async function navigate(url) { return open(url); }
|
|
454
|
-
|
|
455
436
|
async function evaluate(js) {
|
|
456
437
|
await ensure();
|
|
457
438
|
const r = await currentSession.send('Runtime.evaluate', {
|
|
@@ -639,11 +620,6 @@ async function screenshot(saveTo) {
|
|
|
639
620
|
return { ok: true, path: filename, bytes: Buffer.byteLength(r.data, 'base64') };
|
|
640
621
|
}
|
|
641
622
|
|
|
642
|
-
async function currentUrl() {
|
|
643
|
-
await ensure();
|
|
644
|
-
return evaluate('location.href');
|
|
645
|
-
}
|
|
646
|
-
|
|
647
623
|
async function waitFor(selector, timeoutMs = 15000) {
|
|
648
624
|
await ensure();
|
|
649
625
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -669,8 +645,6 @@ function close() {
|
|
|
669
645
|
}
|
|
670
646
|
|
|
671
647
|
module.exports = {
|
|
672
|
-
open,
|
|
673
|
-
key, screenshot, evaluate,
|
|
674
|
-
avisoDeLocal,
|
|
675
|
-
_internal: { findChrome, procurarEmVolumes, PROFILE_DIR, DEBUG_PORT },
|
|
648
|
+
open, click, clickText, clickSelector, type, typeInSelector,
|
|
649
|
+
key, screenshot, evaluate, waitFor, wait, scroll, close,
|
|
676
650
|
};
|
package/lib/config.js
CHANGED
|
@@ -73,10 +73,15 @@ function ensureStructure() {
|
|
|
73
73
|
return { rootDir: ROOT_DIR };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Tira acento (NFD + remove os diacríticos combinantes). Compartilhado com
|
|
77
|
+
// lib/desktop.js — as duas versões faziam a mesma conta separadas.
|
|
78
|
+
function stripAccents(s) {
|
|
79
|
+
return String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
// "Jogo de Carro!" -> "jogo-de-carro"
|
|
77
83
|
function slugify(name) {
|
|
78
|
-
return
|
|
79
|
-
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
84
|
+
return stripAccents(name)
|
|
80
85
|
.toLowerCase()
|
|
81
86
|
.replace(/[^a-z0-9]+/g, '-')
|
|
82
87
|
.replace(/^-+|-+$/g, '')
|
|
@@ -208,7 +213,7 @@ function appendHistory(entry) {
|
|
|
208
213
|
module.exports = {
|
|
209
214
|
loadConfig, saveConfig, appendHistory,
|
|
210
215
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
211
|
-
ensureStructure, slugify,
|
|
216
|
+
ensureStructure, slugify, stripAccents,
|
|
212
217
|
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
|
213
218
|
readGeneralMemory, readProjectMemory, appendProjectMemory, appendGeneralMemory,
|
|
214
219
|
};
|
package/lib/desktop.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
const { execSync } = require('child_process');
|
|
17
17
|
const os = require('os');
|
|
18
|
+
const { stripAccents } = require('./config');
|
|
18
19
|
|
|
19
20
|
const PLATFORM = os.platform();
|
|
20
21
|
|
|
@@ -344,7 +345,7 @@ const APP_ALIASES = {
|
|
|
344
345
|
};
|
|
345
346
|
|
|
346
347
|
function normaliza(s) {
|
|
347
|
-
return
|
|
348
|
+
return stripAccents(s).toLowerCase().trim();
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
function aliasDoApp(nome, platform = PLATFORM) {
|
|
@@ -371,7 +372,17 @@ function openApp(nome) {
|
|
|
371
372
|
} else {
|
|
372
373
|
const bin = app.split(/\s+/)[0];
|
|
373
374
|
if (hasCmd('gtk-launch') && run(`gtk-launch ${sh1(bin)}`).ok) r = { ok: true };
|
|
374
|
-
else if (hasCmd(bin))
|
|
375
|
+
else if (hasCmd(bin)) {
|
|
376
|
+
// sh1() aqui só protegia o script INTEIRO como um argumento pro
|
|
377
|
+
// `bash -c` — dentro do script, bash reparsa o texto, e "app" cru
|
|
378
|
+
// continuava vulnerável a injeção (achado de segurança: um app tipo
|
|
379
|
+
// "a;touch /tmp/pwned;" executava o comando extra). Cada palavra de
|
|
380
|
+
// "app" agora entra entre aspas simples individualmente — preserva
|
|
381
|
+
// "programa --flag" como dois argumentos, mas nenhuma delas deixa
|
|
382
|
+
// metacaractere de shell passar como sintaxe.
|
|
383
|
+
const appEscapado = app.split(/\s+/).map(sh1).join(' ');
|
|
384
|
+
r = run(`bash -c ${sh1(`setsid ${appEscapado} >/dev/null 2>&1 < /dev/null &`)}`);
|
|
385
|
+
}
|
|
375
386
|
else r = { ok: false, error: `Não achei o programa "${pedido}" no PATH.` };
|
|
376
387
|
}
|
|
377
388
|
|
package/lib/modo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Escolha automática de modo.
|
|
3
3
|
*
|
|
4
|
-
* O modo primocode manda o schema das
|
|
4
|
+
* O modo primocode manda o schema das 44 ferramentas em todo pedido — ~4.700
|
|
5
5
|
* tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que uma
|
|
6
6
|
* refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
|
|
7
7
|
* assim que a cota de 100 mil tokens/dia acabou às 15h).
|
package/lib/referencia.js
CHANGED
|
@@ -29,6 +29,33 @@ const net = require('net');
|
|
|
29
29
|
const tls = require('tls');
|
|
30
30
|
const { URL } = require('url');
|
|
31
31
|
|
|
32
|
+
// Achado de segurança: redirect sem checar destino é SSRF — uma fonte de
|
|
33
|
+
// busca (conteúdo de terceiro, não confiável) pode responder com um Location
|
|
34
|
+
// pra localhost ou pro metadado de nuvem (169.254.169.254), e pegar() seguia
|
|
35
|
+
// sem checar host nenhum. Bloqueia loopback/link-local/rede privada antes de
|
|
36
|
+
// abrir a conexão, tanto na URL original quanto em cada redirect seguido.
|
|
37
|
+
function hostBloqueado(hostname) {
|
|
38
|
+
const h = String(hostname || '').toLowerCase();
|
|
39
|
+
if (h === 'localhost' || h.endsWith('.localhost')) return true;
|
|
40
|
+
const versao = net.isIP(h);
|
|
41
|
+
if (versao === 4) {
|
|
42
|
+
const partes = h.split('.').map(Number);
|
|
43
|
+
const [a, b] = partes;
|
|
44
|
+
if (a === 127 || a === 10 || a === 0) return true; // loopback, privada, "esta rede"
|
|
45
|
+
if (a === 169 && b === 254) return true; // link-local — inclui metadado de nuvem
|
|
46
|
+
if (a === 172 && b >= 16 && b <= 31) return true; // privada
|
|
47
|
+
if (a === 192 && b === 168) return true; // privada
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (versao === 6) {
|
|
51
|
+
if (h === '::1' || h === '::') return true; // loopback / unspecified
|
|
52
|
+
if (h.startsWith('fc') || h.startsWith('fd')) return true; // unique local (fc00::/7)
|
|
53
|
+
if (h.startsWith('fe80:') || h.startsWith('fe8') || h.startsWith('fe9') || h.startsWith('fea') || h.startsWith('feb')) return true; // link-local
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return false; // hostname comum (DNS) — não dá pra saber o IP sem resolver
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
const TIMEOUT_MS = 12000;
|
|
33
60
|
const MAX_POR_FONTE = 4;
|
|
34
61
|
const MAX_TEXTO = 4500;
|
|
@@ -72,6 +99,7 @@ function pegar(url, { timeout = TIMEOUT_MS, saltos = 0 } = {}) {
|
|
|
72
99
|
// 5 saltos cobre qualquer doc real; sem teto, um redirect em loop
|
|
73
100
|
// (A→B→A→...) trava essa fonte — e a pesquisa inteira — para sempre.
|
|
74
101
|
if (saltos > 5) return resolve(null);
|
|
102
|
+
if (hostBloqueado(alvo.hostname)) return resolve(null);
|
|
75
103
|
const cliente = alvo.protocol === 'http:' ? http : https;
|
|
76
104
|
|
|
77
105
|
const opcoes = {
|
package/lib/tools.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const os = require('os');
|
|
13
|
-
const { exec } = require('child_process');
|
|
13
|
+
const { exec, execFile } = require('child_process');
|
|
14
14
|
const browser = require('./browser');
|
|
15
15
|
const desktop = require('./desktop');
|
|
16
16
|
const referencia = require('./referencia');
|
|
@@ -165,19 +165,41 @@ async function runShell(command, timeout = 60000, cwd) {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
// `target` vem do modelo (args.target) — que por sua vez pode ter lido de uma
|
|
169
|
+
// página não confiável (prompt injection via pesquisar_referencia/browser_read).
|
|
170
|
+
// execFile com array de argumentos nunca passa por shell nenhum: "target" vira
|
|
171
|
+
// UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
|
|
172
|
+
// `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
|
|
173
|
+
function openInBrowser(target) {
|
|
169
174
|
const p = os.platform();
|
|
170
|
-
const cmd = p === 'darwin' ?
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
|
|
176
|
+
: p === 'win32' ? ['cmd.exe', ['/c', 'start', '', target], 'start']
|
|
177
|
+
: ['xdg-open', [target], 'xdg-open'];
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
execFile(cmd, args, { timeout: 10000 }, (error, stdout, stderr) => {
|
|
180
|
+
resolve(error ? { ok: false, error: (stderr || error.message || '').toString() } : { ok: true, opener });
|
|
181
|
+
});
|
|
182
|
+
});
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
async function runInNewTerminal(command, cwd) {
|
|
176
186
|
const p = os.platform();
|
|
187
|
+
const pastaAlvo = cwd || '.';
|
|
188
|
+
// pastaAlvo normalmente é a pasta do projeto, mas pode vir de input do
|
|
189
|
+
// modelo — sem checar, um valor malicioso quebra pra fora do "cd" embutido
|
|
190
|
+
// no comando que abre o terminal (AppleScript/cmd/x-terminal-emulator),
|
|
191
|
+
// que reinterpreta a string inteira como shell (achado de segurança).
|
|
192
|
+
// Recusa em vez de tentar escapar certo em três dialetos de shell ao
|
|
193
|
+
// mesmo tempo (osascript, cmd.exe, sh) — "command" continua livre de
|
|
194
|
+
// propósito, é o próprio contrato desta tool (abrir terminal com um
|
|
195
|
+
// comando de shell de verdade).
|
|
196
|
+
if (/["'`;&|$\\]/.test(pastaAlvo)) {
|
|
197
|
+
return { ok: false, error: `Pasta com caractere não permitido: ${pastaAlvo}` };
|
|
198
|
+
}
|
|
177
199
|
let cmd;
|
|
178
|
-
if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${
|
|
179
|
-
else if (p === 'win32') cmd = `start cmd /k "cd /d ${
|
|
180
|
-
else cmd = `x-terminal-emulator -e "bash -c 'cd ${
|
|
200
|
+
if (p === 'darwin') cmd = `osascript -e 'tell application "Terminal" to do script "cd ${pastaAlvo} && ${command}"'`;
|
|
201
|
+
else if (p === 'win32') cmd = `start cmd /k "cd /d ${pastaAlvo} && ${command}"`;
|
|
202
|
+
else cmd = `x-terminal-emulator -e "bash -c 'cd ${pastaAlvo} && ${command}; exec bash'"`;
|
|
181
203
|
const r = await runShell(cmd);
|
|
182
204
|
return r.ok ? { ok: true, opener: cmd.split(' ')[0] } : { ok: false, error: r.stderr || 'falhou' };
|
|
183
205
|
}
|
package/lib/ui.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
-
const readline = require('readline');
|
|
8
7
|
|
|
9
8
|
// ── Paleta ───────────────────────────────────────────────────────────────
|
|
10
9
|
// Vem do design system do produto (product-plan/design-system): primária cyan,
|
|
@@ -264,39 +263,6 @@ class Spinner {
|
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
265
|
|
|
267
|
-
// Barra de progresso estilo Claude Code — silenciosa, elegante
|
|
268
|
-
class ProgressBar {
|
|
269
|
-
constructor(label = 'Instalando') {
|
|
270
|
-
this.label = label;
|
|
271
|
-
this.width = 30;
|
|
272
|
-
this.progress = 0;
|
|
273
|
-
this.timer = null;
|
|
274
|
-
this.done = false;
|
|
275
|
-
}
|
|
276
|
-
start() {
|
|
277
|
-
process.stdout.write(` ${c.muted(this.label)} [`);
|
|
278
|
-
this.timer = setInterval(() => {
|
|
279
|
-
if (this.done) return;
|
|
280
|
-
this.progress = Math.min(this.progress + 1, this.width);
|
|
281
|
-
const filled = '█'.repeat(this.progress);
|
|
282
|
-
const empty = '░'.repeat(this.width - this.progress);
|
|
283
|
-
process.stdout.write(`\r ${c.muted(this.label)} [${c.brand(filled)}${c.dim(empty)}] ${c.muted(Math.round((this.progress/this.width)*100) + '%')}`);
|
|
284
|
-
}, 200);
|
|
285
|
-
return this;
|
|
286
|
-
}
|
|
287
|
-
tick() {
|
|
288
|
-
this.progress = Math.min(this.progress + 2, this.width);
|
|
289
|
-
const filled = '█'.repeat(this.progress);
|
|
290
|
-
const empty = '░'.repeat(this.width - this.progress);
|
|
291
|
-
process.stdout.write(`\r ${c.muted(this.label)} [${c.brand(filled)}${c.dim(empty)}] ${c.muted(Math.round((this.progress/this.width)*100) + '%')}`);
|
|
292
|
-
}
|
|
293
|
-
stop() {
|
|
294
|
-
this.done = true;
|
|
295
|
-
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
|
296
|
-
process.stdout.write(`\r ${c.muted(this.label)} [${c.ok('██████████ DONE ██████████')}] ${c.ok('100%')}\n`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
266
|
function actionHeader(name, args) {
|
|
301
267
|
const nice = {
|
|
302
268
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
@@ -422,38 +388,5 @@ function friendlyError(raw) {
|
|
|
422
388
|
return clean.length > 200 ? clean.slice(0, 200) + '…' : clean || 'Algo deu errado.';
|
|
423
389
|
}
|
|
424
390
|
|
|
425
|
-
function toolLine(text) { return ` ${c.dim('⎿')} ${c.muted(text)}`; }
|
|
426
|
-
|
|
427
|
-
function toolResult(result) {
|
|
428
|
-
if (!result) return '';
|
|
429
|
-
if (result.ok === false) return c.err(` ✗ ${result.error || 'failed'}`);
|
|
430
|
-
if (result.created !== undefined) return c.ok(` ${result.created?'created':'updated'} · ${fmtBytes(result.bytes||0)}`);
|
|
431
|
-
return '';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function fmtBytes(n) {
|
|
435
|
-
if (n < 1024) return `${n} B`;
|
|
436
|
-
if (n < 1024*1024) return `${(n/1024).toFixed(1)} KB`;
|
|
437
|
-
return `${(n/(1024*1024)).toFixed(1)} MB`;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function renderMarkdown(text) {
|
|
441
|
-
return text.replace(/\*\*([^*]+)\*\*/g,'$1').replace(/__([^_]+)__/g,'$1').replace(/`([^`]+)`/g,'$1').replace(/^#{1,6}\s+/gm,'');
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Pergunta Yes/No — aguarda resposta real do usuário
|
|
445
|
-
function askYesNo(question) {
|
|
446
|
-
return new Promise((resolve) => {
|
|
447
|
-
const rl = readline.createInterface({
|
|
448
|
-
input: process.stdin,
|
|
449
|
-
output: process.stdout,
|
|
450
|
-
});
|
|
451
|
-
rl.question(`${c.brand('?')} ${question} ${c.muted('(Y/n)')} `, (answer) => {
|
|
452
|
-
rl.close();
|
|
453
|
-
const a = (answer || '').trim().toLowerCase();
|
|
454
|
-
resolve(a === 'y' || a === 'yes' || a === '');
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
391
|
|
|
459
|
-
module.exports = { c, mascot, welcomeBox, Spinner,
|
|
392
|
+
module.exports = { c, mascot, welcomeBox, Spinner, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.1",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. 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": {
|
package/studio/server.py
CHANGED
|
@@ -395,6 +395,24 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
395
395
|
except Exception:
|
|
396
396
|
return {}
|
|
397
397
|
|
|
398
|
+
# Achado de segurança: POST/PUT/DELETE sem checagem de Origin é CSRF — uma
|
|
399
|
+
# página de terceiro (aberta no MESMO navegador, sem relação nenhuma com o
|
|
400
|
+
# Studio) consegue disparar um fetch() com Content-Type: text/plain (que
|
|
401
|
+
# não exige preflight de CORS) contra http://127.0.0.1:7777 e o pedido
|
|
402
|
+
# chega igual a um legítimo. Bloqueia quando o Origin existe e não é o
|
|
403
|
+
# próprio servidor — sem Origin nenhum (cliente não-navegador, como o CLI
|
|
404
|
+
# do PrimoCode via http nativo do Node) continua liberado, já que isso
|
|
405
|
+
# nunca é o vetor de um navegador sendo usado contra o usuário.
|
|
406
|
+
def origem_ok(self):
|
|
407
|
+
origem = self.headers.get("Origin")
|
|
408
|
+
if not origem:
|
|
409
|
+
return True
|
|
410
|
+
esperados = {
|
|
411
|
+
"http://127.0.0.1:%d" % PORTA,
|
|
412
|
+
"http://localhost:%d" % PORTA,
|
|
413
|
+
}
|
|
414
|
+
return origem in esperados
|
|
415
|
+
|
|
398
416
|
# -------------------------------------------------------------- rotas --
|
|
399
417
|
|
|
400
418
|
@seguro
|
|
@@ -502,6 +520,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
502
520
|
|
|
503
521
|
@seguro
|
|
504
522
|
def do_POST(self):
|
|
523
|
+
if not self.origem_ok():
|
|
524
|
+
return self.js({"erro": "origem não permitida"}, 403)
|
|
505
525
|
rota = urlparse(self.path).path
|
|
506
526
|
if rota == "/api/midia":
|
|
507
527
|
corpo, status = guardar_midia(self.corpo())
|
|
@@ -527,6 +547,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
527
547
|
|
|
528
548
|
@seguro
|
|
529
549
|
def do_PUT(self):
|
|
550
|
+
if not self.origem_ok():
|
|
551
|
+
return self.js({"erro": "origem não permitida"}, 403)
|
|
530
552
|
m = re.fullmatch(r"/api/arquivos/([^/]+)", urlparse(self.path).path)
|
|
531
553
|
if not m:
|
|
532
554
|
return self.js({"erro": "não encontrado"}, 404)
|
|
@@ -547,6 +569,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
547
569
|
|
|
548
570
|
@seguro
|
|
549
571
|
def do_DELETE(self):
|
|
572
|
+
if not self.origem_ok():
|
|
573
|
+
return self.js({"erro": "origem não permitida"}, 403)
|
|
550
574
|
rota = urlparse(self.path).path
|
|
551
575
|
m = re.fullmatch(r"/api/midia/([^/]+)", rota)
|
|
552
576
|
if m:
|