beast-agent 2.30.6 → 2.31.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.30.6",
4
+ "version": "2.31.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /* Dışa gönderim güvenlik kapısı: ajanın send_file aracı sır/anahtar
4
+ dosyalarını asla WhatsApp/sohbete göndermesin. Saf fonksiyon —
5
+ electron'a bağımlı değildir, test edilebilir. */
6
+
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ function norm(p) {
11
+ return String(p || '').replace(/\//g, '\\').toLowerCase();
12
+ }
13
+
14
+ /* true → bu dosya hassas, dışa gönderilmez */
15
+ function isSensitiveSendPath(abs, opts = {}) {
16
+ const appData = opts.appData || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
17
+ const home = opts.home || os.homedir();
18
+ let full;
19
+ try {
20
+ full = norm(path.resolve(String(abs || '')));
21
+ } catch {
22
+ return false;
23
+ }
24
+ if (!full) return false;
25
+
26
+ /* Beast veri dizini: settings.json (API key'leri), wa-auth, mcp.json,
27
+ sessions, botlar, tools — tek komutla sızamaz */
28
+ const beastDir = norm(path.join(appData, 'beast'));
29
+ if (full === beastDir || full.startsWith(beastDir + '\\')) return true;
30
+
31
+ /* Kimlik/anahtar klasörleri */
32
+ for (const d of ['.ssh', '.aws', '.gnupg', '.docker', '.kube', '.config']) {
33
+ const p = norm(path.join(home, d));
34
+ if (full === p || full.startsWith(p + '\\')) return true;
35
+ }
36
+
37
+ const base = path.basename(full);
38
+ if (/^(id_rsa|id_ed25519|id_ecdsa|id_dsa|authorized_keys)(\.pub)?$/.test(base)) return true;
39
+ if (/^\.env(\.|$)/.test(base)) return true;
40
+ if (/\.(pem|key|pfx|p12|keystore|jks|ppk)$/.test(base)) return true;
41
+ if (/^(credentials|creds|secrets?)\.(json|ya?ml|txt)$/.test(base)) return true;
42
+ if (/^(\.npmrc|\.netrc|\.git-credentials|\.htpasswd)$/.test(base)) return true;
43
+ return false;
44
+ }
45
+
46
+ module.exports = { isSensitiveSendPath };
package/src/agent/mcp.js CHANGED
@@ -42,6 +42,23 @@ function configPath() {
42
42
 
43
43
  let cfgCache = { mtime: -1, cfg: { servers: {} } };
44
44
 
45
+ /* Sunucu kaydını normalize eder + token güvenliğini doğrular.
46
+ Komut yoksa null döner; satır sonu/NUL varsa fırlatır (kayıt atlanır/reddedilir). */
47
+ function normalizeServer(s) {
48
+ if (!s || typeof s !== 'object' || !s.command) return null;
49
+ const command = String(s.command);
50
+ assertSafeToken(command, 'MCP komutu');
51
+ const args = (Array.isArray(s.args) ? s.args : []).map((a) => assertSafeToken(a, 'MCP argümanı'));
52
+ return {
53
+ command,
54
+ args,
55
+ env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
56
+ enabled: s.enabled !== false,
57
+ tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
58
+ timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
59
+ };
60
+ }
61
+
45
62
  function readConfig(force) {
46
63
  try {
47
64
  const p = configPath();
@@ -51,15 +68,12 @@ function readConfig(force) {
51
68
  const cfg = { servers: {} };
52
69
  if (parsed && typeof parsed.servers === 'object') {
53
70
  for (const [name, s] of Object.entries(parsed.servers)) {
54
- if (!s || typeof s !== 'object' || !s.command) continue;
55
- cfg.servers[sanitizeName(name)] = {
56
- command: String(s.command),
57
- args: Array.isArray(s.args) ? s.args.map(String) : [],
58
- env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
59
- enabled: s.enabled !== false,
60
- tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
61
- timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
62
- };
71
+ try {
72
+ const srv = normalizeServer(s);
73
+ if (srv) cfg.servers[sanitizeName(name)] = srv;
74
+ } catch (e) {
75
+ log.warn(`[mcp] '${name}' geçersiz, atlandı: ${String((e && e.message) || e)}`);
76
+ }
63
77
  }
64
78
  }
65
79
  cfgCache = { mtime: st.mtimeMs, cfg };
@@ -76,9 +90,15 @@ function readConfig(force) {
76
90
 
77
91
  function saveConfig(cfg) {
78
92
  const p = configPath();
93
+ /* DİSKE YAZMADAN doğrula: güvensiz/yapıbozuk kayıt asla mcp.json'a düşmez */
94
+ const clean = { servers: {} };
95
+ for (const [name, s] of Object.entries((cfg && cfg.servers) || {})) {
96
+ const srv = normalizeServer(s);
97
+ if (srv) clean.servers[sanitizeName(name)] = srv;
98
+ }
79
99
  fs.mkdirSync(path.dirname(p), { recursive: true });
80
- fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
81
- cfgCache = { mtime: -1, cfg }; /* sonraki okuma diskten gelsin */
100
+ fs.writeFileSync(p, JSON.stringify(clean, null, 2) + '\n');
101
+ cfgCache = { mtime: -1, cfg: clean }; /* sonraki okuma diskten gelsin */
82
102
  }
83
103
 
84
104
  function sanitizeName(name) {
@@ -89,20 +109,77 @@ function sanitizeName(name) {
89
109
  .slice(0, 32) || 'server';
90
110
  }
91
111
 
112
+ /* UI'a maskeli ('***') giden env sırlarını diskteki mevcut değerlerle geri koyar.
113
+ Böylece kullanıcı formu kaydedince sırlar '***' ile ezilmez. */
114
+ function restoreMaskedSecrets(cfg) {
115
+ let cur;
116
+ try { cur = readConfig(true); } catch { return cfg; }
117
+ for (const [name, s] of Object.entries((cfg && cfg.servers) || {})) {
118
+ if (!s || typeof s !== 'object' || !s.env || typeof s.env !== 'object') continue;
119
+ const old = cur.servers[sanitizeName(name)];
120
+ if (!old || !old.env) continue;
121
+ for (const [k, v] of Object.entries(s.env)) {
122
+ if (String(v) === '***' && old.env[k] !== undefined) s.env[k] = old.env[k];
123
+ }
124
+ }
125
+ return cfg;
126
+ }
127
+
92
128
  /* ---------- süreç yaşam döngüsü ---------- */
93
129
 
130
+ /* Token doğrulama: satır sonu/NUL her platformda enjeksiyon vektörüdür */
131
+ function assertSafeToken(t, what) {
132
+ const s = String(t == null ? '' : t);
133
+ if (/[\0\r\n]/.test(s)) throw new Error(what + ' içinde satır sonu/NUL karakteri olamaz');
134
+ return s;
135
+ }
136
+
137
+ const CMD_META_RE = /[\s"&|<>^()]/;
138
+
139
+ /* KOMUT token'ı: yalnız gerekiyorsa tırnaklanır — aksi halde npm/npx gibi
140
+ .cmd sarmalayıcılarının %~dp0 çözümü bozulur. %/! reddedilir. */
141
+ function quoteCmdCommand(t) {
142
+ const s = assertSafeToken(t, 'MCP komutu');
143
+ if (/[%!]/.test(s)) throw new Error('MCP komutunda % veya ! kullanılamaz (güvenlik)');
144
+ if (!CMD_META_RE.test(s)) return s;
145
+ return '"' + s.replace(/"/g, '""') + '"';
146
+ }
147
+
148
+ /* ARGÜMAN token'ı: HER ZAMAN tırnak içinde → & | < > gibi metakarakterler
149
+ komut ayıramaz. %/! genişletilebildiği için reddedilir; kapanış tırnağı
150
+ öncesi ters bölü CRT kuralı gereği çiftlenir. */
151
+ function quoteCmdArg(t) {
152
+ const s = assertSafeToken(t, 'MCP argümanı');
153
+ if (/[%!]/.test(s)) throw new Error('MCP argümanında % veya ! kullanılamaz (güvenlik)');
154
+ const out = s.replace(/"/g, '""').replace(/(\\+)$/, '$1$1');
155
+ return '"' + out + '"';
156
+ }
157
+
94
158
  function spawnServer(cfg) {
95
- /* Windows: npx/uvx gibi .cmd sarmalayıcıları düz spawn ile başlamaz —
96
- cmd /d /s /c üzerinden, argümanlar elle tırnaklanarak geç. */
97
- const quote = (t) => (/[\s"]/u.test(t) ? '"' + t.replace(/"/g, '\\"') + '"' : t);
98
- const line = [cfg.command, ...cfg.args].map(quote).join(' ');
99
- const child = spawn(line, {
100
- shell: true,
159
+ const cmd = assertSafeToken(cfg.command, 'MCP komutu');
160
+ if (!cmd.trim()) throw new Error('MCP komutu boş');
161
+ const args = (Array.isArray(cfg.args) ? cfg.args : []).map((a) => assertSafeToken(a, 'MCP argümanı'));
162
+ const opts = {
101
163
  windowsHide: true,
102
164
  stdio: ['pipe', 'pipe', 'pipe'],
103
165
  env: { ...process.env, ...(cfg.env || {}) },
166
+ };
167
+ if (process.platform !== 'win32') {
168
+ /* POSIX: shell YOK — & | ; $() gibi metakarakterler enjekte edilemez */
169
+ return spawn(cmd, args, { ...opts, shell: false });
170
+ }
171
+ /* Windows: npx/uvx .cmd sarmalayıcıları düz spawn edilemez — cmd.exe'ye
172
+ tırnaklı/tırnaksız DOĞRU komut satırı elle kurulur; shell KAPALI,
173
+ windowsVerbatimArguments ile libuv müdahalesi kapatılır. */
174
+ const first = quoteCmdCommand(cmd);
175
+ const line = [first, ...args.map(quoteCmdArg)].join(' ');
176
+ /* İlk token tırnaklıysa cmd /s ilk ve son tırnağı KIRPAR — dış sarmal şart */
177
+ const full = first.startsWith('"') ? '"' + line + '"' : line;
178
+ return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', full], {
179
+ ...opts,
180
+ shell: false,
181
+ windowsVerbatimArguments: true,
104
182
  });
105
- return child;
106
183
  }
107
184
 
108
185
  function connFor(name) {
@@ -414,6 +491,9 @@ module.exports = {
414
491
  configPath,
415
492
  readConfig,
416
493
  saveConfig,
494
+ restoreMaskedSecrets,
495
+ normalizeServer,
496
+ _spawnServer: spawnServer,
417
497
  mergeTools,
418
498
  toolSchemas,
419
499
  call,
@@ -4,6 +4,8 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const https = require('https');
7
+ const dns = require('dns');
8
+ const net = require('net');
7
9
  const { execFile } = require('child_process');
8
10
  const { spawn } = require('child_process');
9
11
  const research = require('./research');
@@ -877,6 +879,111 @@ function assertPublicHttpUrl(raw) {
877
879
  return u.toString();
878
880
  }
879
881
 
882
+ /* Özel/ayrılmış IP aralığı mı? (IPv4 + IPv6 + v4-mapped) */
883
+ function isPrivateIp(ip) {
884
+ const v = String(ip || '').toLowerCase().replace(/^\[|\]$/g, '');
885
+ if (v.includes(':')) {
886
+ if (v === '::1' || v === '::') return true;
887
+ if (/^f[cd]/.test(v)) return true; /* fc00::/7 unique-local */
888
+ if (/^fe[89ab]/.test(v)) return true; /* fe80::/10 link-local */
889
+ if (/^ff/.test(v)) return true; /* multicast */
890
+ if (v.startsWith('2001:db8')) return true; /* dokümantasyon */
891
+ const m = v.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); /* v4-mapped */
892
+ if (m) return isPrivateIp(m[1]);
893
+ return false;
894
+ }
895
+ const p = v.split('.').map(Number);
896
+ if (p.length !== 4 || p.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return true; /* çözümlenemeyen → güvensiz */
897
+ if (p[0] === 0 || p[0] === 10 || p[0] === 127 || p[0] >= 224) return true;
898
+ if (p[0] === 100 && p[1] >= 64 && p[1] <= 127) return true; /* CGNAT */
899
+ if (p[0] === 169 && p[1] === 254) return true; /* link-local */
900
+ if (p[0] === 172 && p[1] >= 16 && p[1] <= 31) return true;
901
+ if (p[0] === 192 && (p[1] === 168 || (p[1] === 0 && (p[2] === 0 || p[2] === 2)) || (p[1] === 88 && p[2] === 99))) return true;
902
+ if (p[0] === 198 && (p[1] === 18 || p[1] === 19 || (p[1] === 51 && p[2] === 100))) return true;
903
+ if (p[0] === 203 && p[1] === 0 && p[2] === 113) return true;
904
+ return false;
905
+ }
906
+
907
+ /* DNS çözümlemesi: alan adı ÖZEL bir IP'ye çözülüyorsa (localtest.me vb.) engelle.
908
+ TOCTOU/DNS-rebinding için tam çözüm connection pinning gerektirir; bu katman
909
+ metin kontrolü + gerçek çözümleme + redirect doğrulamasıyla açığı kapatır. */
910
+ async function assertPublicHostResolved(hostname) {
911
+ const h = String(hostname || '').replace(/^\[|\]$/g, '');
912
+ if (!h) throw new Error('geçersiz adres');
913
+ if (net.isIP(h)) {
914
+ if (isPrivateIp(h)) throw new Error('yerel/ağ içi adreslere erişim engellendi');
915
+ return;
916
+ }
917
+ let addrs;
918
+ try {
919
+ addrs = await dns.promises.lookup(h, { all: true, verbatim: true });
920
+ } catch {
921
+ throw new Error('alan adı çözümlenemedi: ' + h);
922
+ }
923
+ if (!addrs || !addrs.length) throw new Error('alan adı çözümlenemedi: ' + h);
924
+ for (const a of addrs) {
925
+ if (isPrivateIp(a.address)) throw new Error('yerel/ağ içi adreslere erişim engellendi (' + h + ')');
926
+ }
927
+ }
928
+
929
+ /* http_fetch için: yönlendirmeleri ELLE takip eder ve HER adımı doğrular
930
+ (302 → 127.0.0.1 gibi bypass'ları kapatır). Döner: { res, url } */
931
+ async function fetchPublicFollow(rawUrl, opts = {}, timeoutMs = 30000, signal) {
932
+ let current = assertPublicHttpUrl(rawUrl);
933
+ for (let hop = 0; hop <= 5; hop++) {
934
+ const u = new URL(current);
935
+ await assertPublicHostResolved(u.hostname);
936
+ const ctrl = new AbortController();
937
+ const timer = setTimeout(() => ctrl.abort(new Error('zaman aşımı')), timeoutMs);
938
+ const onAbort = () => ctrl.abort(new Error('iptal'));
939
+ if (signal) {
940
+ if (signal.aborted) throw new Error('iptal');
941
+ signal.addEventListener('abort', onAbort, { once: true });
942
+ }
943
+ let res;
944
+ try {
945
+ res = await fetch(current, { ...opts, signal: ctrl.signal, redirect: 'manual' });
946
+ } finally {
947
+ clearTimeout(timer);
948
+ if (signal) signal.removeEventListener('abort', onAbort);
949
+ }
950
+ const loc = res.headers.get('location');
951
+ if (res.status >= 300 && res.status < 400 && loc) {
952
+ if (hop === 5) throw new Error('çok fazla yönlendirme');
953
+ current = assertPublicHttpUrl(new URL(loc, current).toString());
954
+ continue;
955
+ }
956
+ return { res, url: current };
957
+ }
958
+ throw new Error('çok fazla yönlendirme');
959
+ }
960
+
961
+ /* Gövdeyi SINIRLI oku — kötü niyetli sunucu sınırsız veri akıtıp belleği şişirmesin */
962
+ async function readTextCapped(res, byteCap = 1024 * 1024) {
963
+ if (!res.body || typeof res.body.getReader !== 'function') {
964
+ const t = await res.text();
965
+ return t.length > byteCap ? t.slice(0, byteCap) : t;
966
+ }
967
+ const reader = res.body.getReader();
968
+ const chunks = [];
969
+ let size = 0;
970
+ try {
971
+ while (true) {
972
+ const { done, value } = await reader.read();
973
+ if (done) break;
974
+ if (!value || !value.length) continue;
975
+ const room = byteCap - size;
976
+ if (room <= 0) break;
977
+ chunks.push(value.length > room ? value.slice(0, room) : value);
978
+ size += Math.min(value.length, room);
979
+ if (size >= byteCap) break;
980
+ }
981
+ } finally {
982
+ try { await reader.cancel(); } catch {}
983
+ }
984
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');
985
+ }
986
+
880
987
  const ENTITIES = {
881
988
  amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', mdash: '—', ndash: '–',
882
989
  hellip: '…', rsquo: '\u2019', lsquo: '\u2018', ldquo: '\u201c', rdquo: '\u201d',
@@ -1004,10 +1111,10 @@ async function webSearch(query, { maxResults = 8, signal } = {}) {
1004
1111
  const MAX_FETCH_CHARS = 9000;
1005
1112
 
1006
1113
  async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', timeoutMs = 30000, signal } = {}) {
1007
- const safe = assertPublicHttpUrl(url);
1008
1114
  const t = Math.min(Math.max(Number(timeoutMs) || 30000, 1000), 120000); // opencode: default 30s, max 120s
1009
- const res = await fetchWithTimeout(
1010
- safe,
1115
+ /* SSRF: metin + DNS çözümlemesi + her redirect adımı ayrı doğrulanır */
1116
+ const { res, url: safe } = await fetchPublicFollow(
1117
+ url,
1011
1118
  { headers: { 'User-Agent': UA, Accept: 'text/html,text/plain,application/json;q=0.9,*/*;q=0.5' } },
1012
1119
  t,
1013
1120
  signal
@@ -1020,7 +1127,7 @@ async function httpFetch(url, { maxChars = MAX_FETCH_CHARS, format = 'text', tim
1020
1127
  return { ok: true, url: safe, status: res.status, contentType: ctype, note: 'ikili/metin olmayan içerik indirilmedi' };
1021
1128
  }
1022
1129
  const cap = 400000;
1023
- let body = await res.text();
1130
+ let body = await readTextCapped(res, 1024 * 1024);
1024
1131
  if (body.length > cap) body = body.slice(0, cap);
1025
1132
  let content;
1026
1133
  if (/html/i.test(ctype)) {
@@ -2357,6 +2464,9 @@ module.exports = {
2357
2464
  globToRegExp,
2358
2465
  walkFiles,
2359
2466
  assertPublicHttpUrl,
2467
+ isPrivateIp,
2468
+ assertPublicHostResolved,
2469
+ fetchPublicFollow,
2360
2470
  parseDdgResults,
2361
2471
  htmlToText,
2362
2472
  webSearch,
package/src/main.js CHANGED
@@ -26,10 +26,15 @@ const watchers = require('./agent/watchers');
26
26
  const usageMod = require('./agent/usage');
27
27
  const bus = require('./agent/bus');
28
28
  const computeruse = require('./agent/computeruse');
29
+ const fsguard = require('./agent/fsguard');
29
30
  const log = require('./agent/logger');
30
31
  const headroom = require('./agent/headroom');
31
32
  const QRCode = require('qrcode'); /* Expo Go QR (bc-expurl) — whatsapp ile aynı paket */
32
33
 
34
+ /* Renderer'a sır gönderirken kullanılan maske; kaydederken aynen geri gelirse
35
+ gerçek değer korunur (anahtarlar UI'da düz metin dolaşmaz). */
36
+ const SECRET_MASK = '***';
37
+
33
38
  /* #3 otomatik updater: sessiz — indirir, kapanışta kurar, kullanıcıya soru sormaz.
34
39
  Paketlenmemiş (npm start) modda devre dışı; Update sekmesi ve /update komutu kontrol eder. */
35
40
  let autoUpdater = null;
@@ -2337,20 +2342,34 @@ async function handleWaIncoming(jid, payload, senderNum) {
2337
2342
  if (!engine) return;
2338
2343
  if (typeof payload === 'string') payload = { text: payload };
2339
2344
  const isGroup = !!payload.isGroup || jid.endsWith('@g.us');
2345
+ /* İzin listesi kaydı — grup kapıları ve doğal dil komutları bunu kullanır:
2346
+ izinli değilse yalnız sohbet; agentic iş yaptıramaz. */
2347
+ const senderHit = waFind(senderNum);
2340
2348
 
2341
- /* slash komutları: DM'de her zaman; grupta sadece bot mention edildiyse */
2349
+ /* slash komutları: DM'de her zaman; grupta sadece bot mention edildiyse.
2350
+ GRUP KAPISI: izin listesinde olmayan üye slash (agentic) komut kullanamaz —
2351
+ herkes sohbet eder ama işi yalnız izinli kişiler yaptırır. */
2342
2352
  const txt0 = String(payload.text || '').trim();
2343
2353
  if (txt0.startsWith('/') && !txt0.includes('\n') && (!isGroup || payload.mentioned)) {
2354
+ if (isGroup && !senderHit && !/^\/(help|version)\b/i.test(txt0)) {
2355
+ await sendWaSafe(
2356
+ jid,
2357
+ 'Bu komutu yalnız izinli kişiler kullanabilir. Sohbet için beni @mention ile çağırabilirsin.'
2358
+ ).catch(() => {});
2359
+ return;
2360
+ }
2344
2361
  if (await tryWaSlash(jid, txt0, senderNum, payload)) return;
2345
2362
  }
2346
2363
 
2347
2364
  /* "hepsini sil" / "tüm cron" / "cronları temizle" gibi doğal dil → cron temizle.
2365
+ Yalnız İZİNLİ gönderen: misafir grup üyesi cron silemez.
2348
2366
  clearAll() bellekteki job'ları VE diski boşaltır, zamanlayıcıyı durdurur
2349
2367
  (sadece dosya silmek yetmezdi: çalışan proses belleği geri yazıyordu). */
2350
2368
  const lc = txt0.toLowerCase();
2351
2369
  if (
2352
- (lc.includes('cron') && /(sil|temizle|kaldır|hepsi|tüm|sıfırla)/.test(lc)) ||
2353
- /^(hepsini|hepsını|tümünü|tümünü|hepsnı|hepsnı)\b.*(sil|sıl|temizle|kaldır)/.test(lc)
2370
+ senderHit &&
2371
+ ((lc.includes('cron') && /(sil|temizle|kaldır|hepsi|tüm|sıfırla)/.test(lc)) ||
2372
+ /^(hepsini|hepsını|tümünü|tümünü|hepsnı|hepsnı)\b.*(sil|sıl|temizle|kaldır)/.test(lc))
2354
2373
  ) {
2355
2374
  const r = cron.clearAll();
2356
2375
  cronEmit();
@@ -2364,8 +2383,10 @@ async function handleWaIncoming(jid, payload, senderNum) {
2364
2383
  }
2365
2384
 
2366
2385
  /* "hatırlatmaları sil" / "alarm temizle" / "reminders kaldır" → yalnızca
2367
- otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz). */
2386
+ otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz).
2387
+ Yalnız İZİNLİ gönderen tetikleyebilir. */
2368
2388
  if (
2389
+ senderHit &&
2369
2390
  /hat[iı]rlat|alarm|remind/.test(lc) &&
2370
2391
  /(sil|sıl|temizle|kaldır|iptal|sıfırla|kapat)/.test(lc)
2371
2392
  ) {
@@ -2505,7 +2526,10 @@ async function mqueueTick() {
2505
2526
 
2506
2527
  async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2507
2528
  const isGroup = !!payload.isGroup;
2508
- const hit = isGroup ? null : waFind(senderNum);
2529
+ /* GRUP: gönderen izin listesinde mi? İzinli → kendi yetki seviyesi;
2530
+ değilse 'chat' (sohbet serbest, araç YOK — agentic işi yalnız izinliler
2531
+ yaptırabilir). DM'de izinsiz gönderen buraya gelemez. */
2532
+ const hit = waFind(senderNum);
2509
2533
  if (!isGroup && !hit) {
2510
2534
  waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
2511
2535
  return;
@@ -2522,8 +2546,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2522
2546
  waLastActiveJid = String(jid); // cevap/dosya hedefi: en son yazan sohbet
2523
2547
  // Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
2524
2548
  wa.setComposing(jid, true);
2525
- // Kişi bazlı granül izin: all/web/read/chat
2526
- let perm = isGroup ? 'all' : hit.perm || (hit.lockdown ? 'chat' : 'all');
2549
+ // Kişi bazlı granül izin: all/web/read/chat — grup misafiri varsayılan 'chat'
2550
+ let perm = hit ? hit.perm || (hit.lockdown ? 'chat' : 'all') : 'chat';
2527
2551
  engine.setSessionPerm(sid, perm);
2528
2552
 
2529
2553
  /* BOT SİSTEMİ: numara → bot eşleştirme (izinli kayıtta bot_id yoksa beast'e düşer) */
@@ -6111,13 +6135,36 @@ ipcMain.handle('wa:groups:set', (_e, cfg) => {
6111
6135
  ipcMain.handle('settings:get', () => {
6112
6136
  const out = JSON.parse(JSON.stringify(settings));
6113
6137
  /* Sırların renderera düz metin gitmesini engelle */
6114
- if (out.email && out.email.pass) out.email.pass = '***';
6115
- if (out.waTts && out.waTts.key) out.waTts.key = '***';
6138
+ if (out.email && out.email.pass) out.email.pass = SECRET_MASK;
6139
+ if (out.waTts && out.waTts.key) out.waTts.key = SECRET_MASK;
6140
+ if (out.supermemory && out.supermemory.apiKey) out.supermemory.apiKey = SECRET_MASK;
6141
+ if (out.fallout && Array.isArray(out.fallout.slots)) {
6142
+ for (const s of out.fallout.slots) if (s && s.key) s.key = SECRET_MASK;
6143
+ }
6144
+ if (Array.isArray(out.customProviders)) {
6145
+ for (const p of out.customProviders) if (p && p.key) p.key = SECRET_MASK;
6146
+ }
6116
6147
  return out;
6117
6148
  });
6118
6149
 
6119
6150
  /* ---------------- MCP IPC ---------------- */
6120
6151
 
6152
+ /* UI'a giden raw JSON'da env sırlarını maskeler (düz metin anahtar sızmasın) */
6153
+ function maskMcpRaw(raw) {
6154
+ try {
6155
+ const obj = JSON.parse(String(raw || ''));
6156
+ if (!obj || typeof obj !== 'object' || !obj.servers || typeof obj.servers !== 'object') return raw;
6157
+ for (const s of Object.values(obj.servers)) {
6158
+ if (s && typeof s === 'object' && s.env && typeof s.env === 'object') {
6159
+ for (const k of Object.keys(s.env)) if (String(s.env[k] || '')) s.env[k] = SECRET_MASK;
6160
+ }
6161
+ }
6162
+ return JSON.stringify(obj, null, 2);
6163
+ } catch {
6164
+ return raw;
6165
+ }
6166
+ }
6167
+
6121
6168
  ipcMain.handle('mcp:status', () => {
6122
6169
  try { return mcpMod.status(); } catch { return { path: '', servers: [] }; }
6123
6170
  });
@@ -6127,15 +6174,17 @@ ipcMain.handle('mcp:config:get', () => {
6127
6174
  const p = mcpMod.configPath();
6128
6175
  let raw = '';
6129
6176
  try { raw = require('fs').readFileSync(p, 'utf8'); } catch {}
6130
- return { path: p, raw };
6177
+ return { path: p, raw: maskMcpRaw(raw) };
6131
6178
  } catch { return { path: '', raw: '' }; }
6132
6179
  });
6133
6180
 
6134
6181
  ipcMain.handle('mcp:config:set', (_e, raw) => {
6135
6182
  try {
6136
6183
  const text = String(raw || '');
6137
- if (text.trim()) JSON.parse(text); /* bozuk JSON diske yazılmaz */
6138
- mcpMod.saveConfig(text.trim() ? JSON.parse(text) : { servers: {} });
6184
+ const cfg = text.trim() ? JSON.parse(text) : { servers: {} };
6185
+ /* maskeli env sırlarını mevcut diskteki değerlerle geri koy */
6186
+ try { mcpMod.restoreMaskedSecrets(cfg); } catch {}
6187
+ mcpMod.saveConfig(cfg);
6139
6188
  return { ok: true };
6140
6189
  } catch (e) {
6141
6190
  return { ok: false, error: String((e && e.message) || e) };
@@ -6163,31 +6212,45 @@ ipcMain.handle('fallout:get', () => {
6163
6212
  out.enabled = !!f.enabled;
6164
6213
  out.autoResume = f.autoResume !== false;
6165
6214
  if (Array.isArray(f.slots)) {
6166
- for (let i = 0; i < Math.min(10, f.slots.length); i++) out.slots[i] = f.slots[i] || null;
6215
+ for (let i = 0; i < Math.min(10, f.slots.length); i++) {
6216
+ const s = f.slots[i];
6217
+ /* kayıtlı anahtarlar UI'a maskeli gider */
6218
+ out.slots[i] = s && s.key ? { ...s, key: SECRET_MASK } : s || null;
6219
+ }
6167
6220
  }
6168
6221
  return out;
6169
6222
  });
6170
6223
 
6171
6224
  ipcMain.handle('fallout:set', (_e, cfg) => {
6172
6225
  const cur = settings.fallout || defaultFallout();
6226
+ const prevSlots = Array.isArray(cur.slots) ? cur.slots : [];
6173
6227
  const next = defaultFallout();
6174
6228
  next.enabled = !!(cfg && cfg.enabled);
6175
6229
  next.autoResume = !cfg || cfg.autoResume !== false;
6176
6230
  const slots = Array.isArray(cfg && cfg.slots) ? cfg.slots : [];
6177
6231
  for (let i = 0; i < Math.min(10, slots.length); i++) {
6178
6232
  const s = slots[i];
6179
- if (!s || typeof s !== 'object' || !s.providerId || !s.model || !s.key) continue;
6233
+ if (!s || typeof s !== 'object' || !s.providerId || !s.model) continue;
6234
+ let key = String(s.key || '');
6235
+ /* maskeli/boş anahtar: aynı provider'ın önceki kayıtlı anahtarı korunur */
6236
+ if (key === SECRET_MASK || !key) {
6237
+ const prev = prevSlots[i];
6238
+ key = prev && prev.providerId === String(s.providerId) && prev.key ? String(prev.key) : '';
6239
+ }
6240
+ if (!key) continue;
6180
6241
  next.slots[i] = {
6181
6242
  providerId: String(s.providerId),
6182
6243
  providerName: String(s.providerName || s.providerId),
6183
6244
  model: String(s.model),
6184
- key: String(s.key),
6245
+ key,
6185
6246
  };
6186
6247
  }
6187
6248
  settings.fallout = next;
6188
6249
  saveSettings();
6189
6250
  if (engine) engine.setFallout(next);
6190
- return JSON.parse(JSON.stringify(next));
6251
+ const out = JSON.parse(JSON.stringify(next));
6252
+ for (const s of out.slots) if (s && s.key) s.key = SECRET_MASK;
6253
+ return out;
6191
6254
  });
6192
6255
 
6193
6256
  /* #Limit: provider bazlı max input token limiti + bağlam sıkıştırma */
@@ -6217,10 +6280,13 @@ ipcMain.handle('sec:set', (_e, cfg) => {
6217
6280
  ipcMain.handle('supermemory:set', (_e, cfg) => {
6218
6281
  try {
6219
6282
  const base = String((cfg && cfg.baseUrl) || '').trim() || 'http://localhost:6767';
6283
+ const incomingKey = String((cfg && cfg.apiKey) || '').trim();
6284
+ const prevKey = settings.supermemory && settings.supermemory.apiKey ? String(settings.supermemory.apiKey) : '';
6220
6285
  settings.supermemory = {
6221
6286
  enabled: !!(cfg && cfg.enabled),
6222
6287
  baseUrl: base.replace(/\/+$/, ''),
6223
- apiKey: String((cfg && cfg.apiKey) || '').trim(),
6288
+ /* maskeli anahtar geri gelirse mevcut korunur */
6289
+ apiKey: incomingKey === SECRET_MASK ? prevKey : incomingKey,
6224
6290
  containerTag: String((cfg && cfg.containerTag) || 'beast').trim() || 'beast',
6225
6291
  };
6226
6292
  saveSettings();
@@ -6230,7 +6296,7 @@ ipcMain.handle('supermemory:set', (_e, cfg) => {
6230
6296
  engine._smCheckedAt = 0;
6231
6297
  engine._smWarned = false;
6232
6298
  }
6233
- return { ok: true, supermemory: settings.supermemory };
6299
+ return { ok: true, supermemory: { ...settings.supermemory, apiKey: settings.supermemory.apiKey ? SECRET_MASK : '' } };
6234
6300
  } catch (e) {
6235
6301
  return { ok: false, error: String((e && e.message) || e) };
6236
6302
  }
@@ -6240,7 +6306,7 @@ ipcMain.handle('supermemory:get', () => {
6240
6306
  return {
6241
6307
  enabled: sm.enabled !== false,
6242
6308
  baseUrl: sm.baseUrl || 'http://localhost:6767',
6243
- apiKey: sm.apiKey || '',
6309
+ apiKey: sm.apiKey ? SECRET_MASK : '',
6244
6310
  containerTag: sm.containerTag || 'beast',
6245
6311
  up: engine ? !!engine._smUp : null,
6246
6312
  };
@@ -7317,7 +7383,10 @@ ipcMain.handle('providers:keys', () => {
7317
7383
  }
7318
7384
  } catch {}
7319
7385
  }
7320
- return map;
7386
+ /* UI'a anahtarlar DÜZ METİN gitmez — yalnız "kayıtlı var mı" bilgisi taşınır */
7387
+ const out = {};
7388
+ for (const [k, v] of Object.entries(map)) out[k] = String(v || '') ? SECRET_MASK : '';
7389
+ return out;
7321
7390
  });
7322
7391
  /* #22 izleyici paneli IPC */
7323
7392
  ipcMain.handle('watchers:list', () => watchers.list());
@@ -10266,7 +10335,17 @@ ipcMain.handle('ide:previewUrl', async (_e, url) => {
10266
10335
  });
10267
10336
 
10268
10337
  ipcMain.handle('custom:set', (_e, list) => {
10269
- settings.customProviders = Array.isArray(list) ? list : [];
10338
+ const prev = Array.isArray(settings.customProviders) ? settings.customProviders : [];
10339
+ const byId = new Map(prev.filter((p) => p && p.id).map((p) => [p.id, p]));
10340
+ settings.customProviders = (Array.isArray(list) ? list : []).map((p) => {
10341
+ if (!p || typeof p !== 'object') return p;
10342
+ const item = { ...p };
10343
+ const old = byId.get(item.id);
10344
+ const k = String(item.key == null ? '' : item.key).trim();
10345
+ /* UI'dan maskeli/boş anahtar geldiyse aynı id'nin kayıtlı anahtarı korunur */
10346
+ if ((k === SECRET_MASK || !k) && old && old.key) item.key = String(old.key);
10347
+ return item;
10348
+ });
10270
10349
  saveSettings();
10271
10350
  engine.setCustomProviders(settings.customProviders);
10272
10351
  headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
@@ -10895,6 +10974,12 @@ async function deliverFile(sessionId, filePath, caption) {
10895
10974
  try {
10896
10975
  const abs = path.isAbsolute(filePath) ? filePath : path.join(engine.workspace || process.cwd(), filePath);
10897
10976
  if (!fs.existsSync(abs)) return { ok: false, error: 'dosya bulunamadı: ' + abs };
10977
+ /* GÜVENLİK: sır/anahtar dosyaları dışa gönderilmez (settings.json,
10978
+ wa-auth, .ssh, .env, *.pem...). Ajan bunu kullanıcıya söylesin. */
10979
+ if (fsguard.isSensitiveSendPath(abs)) {
10980
+ log.warn('sec', `send_file engellendi (hassas yol): ${abs}`);
10981
+ return { ok: false, error: 'güvenlik: hassas dosya (sır/anahtar) dışa gönderilmedi: ' + path.basename(abs) };
10982
+ }
10898
10983
  const name = path.basename(abs);
10899
10984
  const ext = path.extname(abs).toLowerCase();
10900
10985
  let sid = String(sessionId || '');
@@ -656,6 +656,37 @@
656
656
  store_install_fail: 'Kurulum başarısız',
657
657
  store_like_ok: 'Beğeni kaydedildi',
658
658
  store_login_note: 'Beast Kimliği anonimdir; gerçek Beast Kodun asla paylaşılmaz.',
659
+ /* Cron paneli + WhatsApp kişi/yetki editörü */
660
+ cr_empty: 'Henüz görev yok — yukarıdan ekle.',
661
+ cr_dot_on: 'aktif — tıkla: forma doldur',
662
+ cr_dot_off: 'kapalı — tıkla: forma doldur',
663
+ cr_next: 'sıradaki: ',
664
+ cr_last: ' · son: ',
665
+ cr_run: 'Şimdi çalıştır',
666
+ cr_pause: 'Durdur',
667
+ cr_start: 'Başlat',
668
+ cr_del: 'Sil',
669
+ cr_modal_empty: 'Kayıtlı cron görev yok — Ayarlar → Cron sekmesinden ekle.',
670
+ cr_ran: 'Çalıştırıldı',
671
+ cr_toggle_pause: 'Duraklat',
672
+ wa_vazgec: 'Vazgeç',
673
+ wa_kaydet: 'kaydet',
674
+ wa_updated: 'Güncellendi',
675
+ wa_isim_ph: 'isim',
676
+ wa_numara_ph: 'numara',
677
+ wa_no_name_title: 'Bu kişiye cevap VERİLMEYECEK — tıklayıp isim ver',
678
+ wa_edit_title: 'Tıkla: isim/numara düzenle',
679
+ wa_perm_title: 'Yetki seviyesi — bu kişinin agent erişimini belirler',
680
+ wa_owner_title_on: 'Bu kişi BEAST\u2019İN SAHİBİ — tıklayıp başka kişiye devret',
681
+ wa_owner_title_off: 'Tıkla: bu kişiyi SAHİP yap (yalnız biri olabilir)',
682
+ wa_owner_on: '👑 sahip',
683
+ wa_owner_off: 'sahip yap',
684
+ wa_perm_all: 'serbest',
685
+ wa_perm_read: 'okuma',
686
+ wa_perm_chat: 'kısıtlı',
687
+ wa_queue_info: 'Mesaj kuyruğu: ${n} bekleyen',
688
+ wa_queue_failed: '${n} gönderilemedi',
689
+ wa_queue_empty_ok: 'Kuyruk temiz',
659
690
  },
660
691
  en: {
661
692
  newChat: '+ New Chat',
@@ -1076,36 +1107,6 @@
1076
1107
  wa_queue_info: 'Message queue: ${n} pending',
1077
1108
  wa_queue_failed: '${n} could not be sent',
1078
1109
  wa_queue_empty_ok: 'Queue clear',
1079
- cr_empty: 'Henüz görev yok — yukarıdan ekle.',
1080
- cr_dot_on: 'aktif — tıkla: forma doldur',
1081
- cr_dot_off: 'kapalı — tıkla: forma doldur',
1082
- cr_next: 'sıradaki: ',
1083
- cr_last: ' · son: ',
1084
- cr_run: 'Şimdi çalıştır',
1085
- cr_pause: 'Durdur',
1086
- cr_start: 'Başlat',
1087
- cr_del: 'Sil',
1088
- cr_modal_empty: 'Kayıtlı cron görev yok — Ayarlar → Cron sekmesinden ekle.',
1089
- cr_ran: 'Çalıştırıldı',
1090
- cr_toggle_pause: 'Duraklat',
1091
- wa_vazgec: 'Vazgeç',
1092
- wa_kaydet: 'kaydet',
1093
- wa_updated: 'Güncellendi',
1094
- wa_isim_ph: 'isim',
1095
- wa_numara_ph: 'numara',
1096
- wa_no_name_title: 'Bu kişiye cevap VERİLMEYECEK — tıklayıp isim ver',
1097
- wa_edit_title: 'Tıkla: isim/numara düzenle',
1098
- wa_perm_title: 'Yetki seviyesi — bu kişinin agent erişimini belirler',
1099
- wa_owner_title_on: 'Bu kişi BEAST\u2019İN SAHİBİ — tıklayıp başka kişiye devret',
1100
- wa_owner_title_off: 'Tıkla: bu kişiyi SAHİP yap (yalnız biri olabilir)',
1101
- wa_owner_on: '👑 sahip',
1102
- wa_owner_off: 'sahip yap',
1103
- wa_perm_all: 'serbest',
1104
- wa_perm_read: 'okuma',
1105
- wa_perm_chat: 'kısıtlı',
1106
- wa_queue_info: 'Mesaj kuyruğu: ${n} bekleyen',
1107
- wa_queue_failed: '${n} gönderilemedi',
1108
- wa_queue_empty_ok: 'Kuyruk temiz',
1109
1110
  us_reset_toast: 'Usage counters reset',
1110
1111
  us_backup_ok: 'Backup taken: Desktop\\Beast-Backups',
1111
1112
  us_where_on_toast: 'Startup summary on',