beast-agent 2.30.5 → 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.5",
4
+ "version": "2.31.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -327,6 +327,7 @@ class Engine {
327
327
  boyunca "always" desenlerini kural olarak taşır (opencode birebir). */
328
328
  this._perm = new AskService((ev) => this.emit(ev));
329
329
  this.bcAskApprovals = opts.approvals ? true : false; // onay ayarı → opencode user konfigi gibi "ask" üretir
330
+ this._perm.autoAllow = !this.bcAskApprovals; // güvenlik kapalı: BeastCode/Sandbox izin SORMAZ (deny yine geçerli)
330
331
  /* opencode reminders.ts: bu oturumda daha önce plan ajanı koştu mu?
331
332
  plan→build geçişinde build-switch.txt synthetic part'ı buna bağlanır */
332
333
  this._bcWasPlan = new Map(); // sessionId -> bool
@@ -1608,11 +1609,13 @@ class Engine {
1608
1609
  ? 'plan'
1609
1610
  : 'build';
1610
1611
  const ws = (session && session.workspace) || this.workspace;
1611
- const user = this.bcAskApprovals ? { bash: 'ask', edit: { '*': 'ask' } } : {};
1612
- const info = opencode.agents.get(name, { worktree: ws, promptExplore: opencode.PROMPT_EXPLORE, user });
1612
+ /* BeastCode/Sandbox onayı ARIZİ değil: güvenlik açıkken yalnız SİLME
1613
+ işlemleri _perm.ask ile sorulur (aşağıda _isDeleteOp) — bash/edit'in
1614
+ tamamına asks koymak "her işlemde sor" demekti, kaldırıldı. */
1615
+ const info = opencode.agents.get(name, { worktree: ws, promptExplore: opencode.PROMPT_EXPLORE });
1613
1616
  if (info) return info;
1614
1617
  const fallbackName = session.sbSandbox ? 'sandbox' : 'build';
1615
- return opencode.agents.get(fallbackName, { worktree: ws, promptExplore: opencode.PROMPT_EXPLORE, user });
1618
+ return opencode.agents.get(fallbackName, { worktree: ws, promptExplore: opencode.PROMPT_EXPLORE });
1616
1619
  }
1617
1620
 
1618
1621
  /* opencode opencode ajan değişimi: /plan /build — oturumun ajanı döner,
@@ -1638,6 +1641,21 @@ class Engine {
1638
1641
  }
1639
1642
  }
1640
1643
 
1644
+ /* Güvenlik ayarı canlı değişimi (Ayarlar → Güvenlik):
1645
+ - kapalı → BeastCode/Sandbox dahil açık izin akışı hiç sormaz (autoAllow)
1646
+ - açık → riskli araçlarda onay ister (approvals köprüsü + bash/edit user kuralı)
1647
+ Bekleyen istekler serbest bırakılır (kapı kalktı; araç devam eder). */
1648
+ setApprovals(on) {
1649
+ this.bcAskApprovals = !!on;
1650
+ this._perm.autoAllow = !this.bcAskApprovals;
1651
+ if (!this.bcAskApprovals) {
1652
+ try {
1653
+ for (const req of this._perm.list()) this._perm.reply(req.id, 'once');
1654
+ } catch {}
1655
+ }
1656
+ return { bcAskApprovals: this.bcAskApprovals, autoAllow: this._perm.autoAllow };
1657
+ }
1658
+
1641
1659
  /* ---------- Supermemory (lokal) bellek köprüsü ---------- */
1642
1660
 
1643
1661
  _smConfig() {
@@ -4126,15 +4144,23 @@ class Engine {
4126
4144
  if (dups >= DOOM_LOOP_THRESHOLD) {
4127
4145
  if (bcAgent) {
4128
4146
  try {
4129
- await this._perm.ask({
4130
- sessionId: sid,
4131
- ruleset: bcAgent.permission,
4132
- permission: 'doom_loop',
4133
- patterns: [name],
4134
- always: [name],
4135
- metadata: { callCount: DOOM_LOOP_THRESHOLD, arguments: (tc.function && tc.function.arguments) || '' },
4136
- tool: { name, args },
4137
- });
4147
+ /* doom_loop bir SİLME değil: onay kartıyla kullanıcıyı yorma —
4148
+ kural değerlendirmesi (deny) işler, ask otomatik geçer */
4149
+ const prevAuto = this._perm.autoAllow;
4150
+ this._perm.autoAllow = true;
4151
+ try {
4152
+ await this._perm.ask({
4153
+ sessionId: sid,
4154
+ ruleset: bcAgent.permission,
4155
+ permission: 'doom_loop',
4156
+ patterns: [name],
4157
+ always: [name],
4158
+ metadata: { callCount: DOOM_LOOP_THRESHOLD, arguments: (tc.function && tc.function.arguments) || '' },
4159
+ tool: { name, args },
4160
+ });
4161
+ } finally {
4162
+ this._perm.autoAllow = prevAuto;
4163
+ }
4138
4164
  out = await this._execTool(name, args, ctrl.signal, sid);
4139
4165
  } catch (permErr) {
4140
4166
  out = JSON.stringify({
@@ -4883,6 +4909,33 @@ const skills = require('./skills');
4883
4909
  opencode'da edit+write aynı "edit" iznine takılır — burada da ikisi birlikte */
4884
4910
  static RISKY_TOOLS = new Set(['run_command', 'write_file', 'edit_file', 'python_run', 'email_send', 'watcher_add', 'git_commit', 'git_pr_create', 'xlsx_write', 'xlsx_edit']);
4885
4911
 
4912
+ /* SİLME desenleri — güvenlik AÇIKken onay kapısı YALNIZCA silmelerde sorar
4913
+ (kullanıcı isteği: her işlem değil, sadece silmeler). */
4914
+ static DELETE_RE =
4915
+ /(remove-item|\bri\s|\bdel\s|\bdel\b|\brd\b|\brmdir\b|\berase\b|\brm\s|\bunlink|shutil\s*\.\s*rmtree|os\s*\.\s*(remove|unlink|rmdir)|\brmtree\b|\.unlink\s*\(|fs\s*\.\s*(rm|unlink|rmdir)|rimraf|clear-content|\breg\s+delete|git\s+clean)/i;
4916
+
4917
+ /* Bu araç çağrısı bir SİLME mi? (komut/betik içeriği + araç adı deseni) */
4918
+ _isDeleteOp(name, args) {
4919
+ const n = String(name || '');
4920
+ const a = args || {};
4921
+ if (n === 'run_command' || n === 'panel_run') return Engine.DELETE_RE.test(String(a.command || ''));
4922
+ if (n === 'python_run') return Engine.DELETE_RE.test(String(a.code || ''));
4923
+ return /(delete|remove|unlink|rmdir|rmtree)/i.test(n);
4924
+ }
4925
+
4926
+ /* "Bir daha sorma" anahtarı: aynı silme işlemi için tekrar sorulmaz */
4927
+ _deleteKey(name, args) {
4928
+ const a = args || {};
4929
+ const raw =
4930
+ name === 'run_command' || name === 'panel_run'
4931
+ ? String(a.command || '')
4932
+ : name === 'python_run'
4933
+ ? String(a.code || '')
4934
+ : String(name || '');
4935
+ const s = raw.replace(/\s+/g, ' ').trim();
4936
+ return (s.length > 200 ? s.slice(0, 200) + '…' : s) || '*';
4937
+ }
4938
+
4886
4939
  /* ANA KOD KİLİDİ: yıkıcı işlem desenleri (korumalı yol + bu desen = engel) */
4887
4940
  static DESTRUCTIVE_RE =
4888
4941
  /(remove-item|\bri\s|\bdel\s|\bdel\b|\brd\b|\brmdir\b|\berase\b|set-content|out-file|add-content|new-item|clear-content|move-item|rename-item|\bren\s|\bcopy-item|\bmove\b|\bmv\s|\bcp\s|>>|[^|]>\s|\bicacls|\btakeown|\bformat\b|\bfsutil|\breg\s+(add|delete)|schtasks|stop-process|\btaskkill|\bkill\b|\brm\s|\bunlink|shutil|os\.remove|os\.unlink|open\([^)]*['"][wa]['"]\)|write_text|to_csv\(|savefig\(|\.write\()/i;
@@ -4975,13 +5028,18 @@ const skills = require('./skills');
4975
5028
  }
4976
5029
  if (isBc) {
4977
5030
  /* opencode permission akışı (tool/external-directory.ts + her aracın
4978
- ctx.ask'ı): önce workspace DIŞI erişim izni, sonra araç izni.
4979
- deny anında hata döner (asla sorulmaz), allow sessiz geçer,
4980
- ask UI'ya 'permission.asked' eventi ile sorulur */
5031
+ ctx.ask'ı): deny anında hata döner (asla sorulmaz), allow sessiz geçer.
5032
+ GÜVENLİK AÇIKken yalnız SİLME işlemleri 'permission.asked' ile sorulur;
5033
+ diğer ask kuralları otomatik serbesttir (deny yine geçerlidir). */
4981
5034
  const ws = this._sessionWorkspace(sessionId);
4982
5035
  const bcAgent = this._bcAgentInfo(bcSess);
4983
5036
  const ruleset = bcAgent ? bcAgent.permission : [];
5037
+ const isDel = this.bcAskApprovals && this._isDeleteOp(name, args);
5038
+ const prevAuto = this._perm.autoAllow;
4984
5039
  try {
5040
+ /* 1) deny kuralları her durumda işler; ask'ler silme dışında otomatik
5041
+ geçer (güvenlik açıkken kullanıcıyı yormama) */
5042
+ this._perm.autoAllow = true;
4985
5043
  const ext = this._bcExternalCheck(name, args, ws);
4986
5044
  if (ext) {
4987
5045
  await this._perm.ask({ sessionId, ruleset, ...ext, metadata: { tool: name, args: args || {} } });
@@ -4996,19 +5054,34 @@ const skills = require('./skills');
4996
5054
  metadata: { args: args || {} },
4997
5055
  tool: { name, args: args || {} },
4998
5056
  });
5057
+ /* 2) güvenlik AÇIK + SİLME → onay sorusu (tek gerçek ask) */
5058
+ if (isDel) {
5059
+ this._perm.autoAllow = false;
5060
+ const key = this._deleteKey(name, args);
5061
+ await this._perm.ask({
5062
+ sessionId,
5063
+ ruleset: [{ permission: 'delete', pattern: '*', action: 'ask' }],
5064
+ permission: 'delete',
5065
+ patterns: [key],
5066
+ always: [key],
5067
+ metadata: { tool: name, args: args || {} },
5068
+ tool: { name, args: args || {} },
5069
+ });
5070
+ }
4999
5071
  } catch (permErr) {
5000
5072
  return JSON.stringify({ ok: false, error: AskService.errorText(permErr) });
5073
+ } finally {
5074
+ this._perm.autoAllow = prevAuto;
5001
5075
  }
5002
5076
  } else {
5003
- /* onay kapısı: riskli araçta dış onay bekle; reddedilirse araç çalışmaz.
5004
- "always" onaylı araçlar doğrudan geçer. (BC dışı oturumlar — opencode
5005
- izin sistemi yalnız Beast Code oturumlarında koşar) */
5077
+ /* onay kapısı: güvenlik açıkken YALNIZCA SİLME işlemlerinde dış onay
5078
+ beklenir; reddedilirse araç çalışmaz. "always" onaylılar doğrudan
5079
+ geçer. (BC dışı oturumlar — opencode izin sistemi Beast Code'da koşar) */
5006
5080
  if (
5007
- (Engine.RISKY_TOOLS.has(name) ||
5008
- String(name).startsWith('mcp__') ||
5009
- String(name).startsWith('tool__')) && /* kişisel tool = kullanıcı kodu koşturur */
5081
+ this.approvals &&
5082
+ typeof this.approvals.request === 'function' &&
5010
5083
  !this.alwaysAllowTools.has(name) &&
5011
- this.approvals && typeof this.approvals.request === 'function'
5084
+ this._isDeleteOp(name, args)
5012
5085
  ) {
5013
5086
  emitSafe(this, sessionId, { type: 'status', status: `onay bekleniyor: ${name}` });
5014
5087
  let ok = false;
@@ -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,
@@ -28,6 +28,9 @@ class AskService {
28
28
  this.emit = typeof emitFn === 'function' ? emitFn : () => {};
29
29
  this.pending = new Map(); // id -> { info, resolve, reject }
30
30
  this.approved = []; // oturumlar arası paylaşılan kural listesi (engine'e aşılanır)
31
+ /* Güvenlik ayarı KAPALI: 'ask' kuralı yine de sorulmaz — deny korunur,
32
+ ask otomatik serbesttir (tüm yetki ajanda). Engine ayarlar ile günceller. */
33
+ this.autoAllow = false;
31
34
  }
32
35
 
33
36
  /* opencode Permission.ask — throws: { kind:'denied'|'rejected'|'corrected', feedback? } */
@@ -40,6 +43,7 @@ class AskService {
40
43
  throw { kind: 'denied', permission, pattern };
41
44
  }
42
45
  if (rule.action === 'allow') continue;
46
+ if (this.autoAllow) continue; /* güvenlik kapalı: sorma, geç */
43
47
  needsAsk = true;
44
48
  }
45
49
  if (!needsAsk) return Promise.resolve();
@@ -60,12 +64,14 @@ class AskService {
60
64
  this.emit({ type: 'permission.asked', sessionId: info.sessionID, request: info });
61
65
  } catch {}
62
66
  /* güvenlik: emit kanalı koptuysa pending asılı kalmasın */
63
- setTimeout(() => {
67
+ const timer = setTimeout(() => {
64
68
  if (this.pending.has(id)) {
65
69
  this.pending.delete(id);
66
70
  reject({ kind: 'rejected' });
67
71
  }
68
72
  }, 5 * 60 * 1000);
73
+ /* bekleyen onay süreci/prosesi canlı tutmasın (özellikle test/çıkış) */
74
+ if (timer && typeof timer.unref === 'function') timer.unref();
69
75
  });
70
76
  }
71
77
 
@@ -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 */
@@ -6206,6 +6269,8 @@ ipcMain.handle('sec:set', (_e, cfg) => {
6206
6269
  if (engine) {
6207
6270
  engine.approvals = settings.security.approvals ? approvalsBridge : null;
6208
6271
  engine.alwaysAllowTools = new Set(settings.security.alwaysAllow);
6272
+ /* BeastCode + Sandbox izin akışı CANLI güncellenir: kapalıyken hiç sorulmaz */
6273
+ if (typeof engine.setApprovals === 'function') engine.setApprovals(settings.security.approvals);
6209
6274
  }
6210
6275
  log.info('sec', `güvenlik: onay kapısı ${settings.security.approvals ? 'AÇIK' : 'KAPALI (her şey serbest)'}`);
6211
6276
  return { approvals: settings.security.approvals, alwaysAllow: settings.security.alwaysAllow };
@@ -6215,10 +6280,13 @@ ipcMain.handle('sec:set', (_e, cfg) => {
6215
6280
  ipcMain.handle('supermemory:set', (_e, cfg) => {
6216
6281
  try {
6217
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) : '';
6218
6285
  settings.supermemory = {
6219
6286
  enabled: !!(cfg && cfg.enabled),
6220
6287
  baseUrl: base.replace(/\/+$/, ''),
6221
- apiKey: String((cfg && cfg.apiKey) || '').trim(),
6288
+ /* maskeli anahtar geri gelirse mevcut korunur */
6289
+ apiKey: incomingKey === SECRET_MASK ? prevKey : incomingKey,
6222
6290
  containerTag: String((cfg && cfg.containerTag) || 'beast').trim() || 'beast',
6223
6291
  };
6224
6292
  saveSettings();
@@ -6228,7 +6296,7 @@ ipcMain.handle('supermemory:set', (_e, cfg) => {
6228
6296
  engine._smCheckedAt = 0;
6229
6297
  engine._smWarned = false;
6230
6298
  }
6231
- return { ok: true, supermemory: settings.supermemory };
6299
+ return { ok: true, supermemory: { ...settings.supermemory, apiKey: settings.supermemory.apiKey ? SECRET_MASK : '' } };
6232
6300
  } catch (e) {
6233
6301
  return { ok: false, error: String((e && e.message) || e) };
6234
6302
  }
@@ -6238,7 +6306,7 @@ ipcMain.handle('supermemory:get', () => {
6238
6306
  return {
6239
6307
  enabled: sm.enabled !== false,
6240
6308
  baseUrl: sm.baseUrl || 'http://localhost:6767',
6241
- apiKey: sm.apiKey || '',
6309
+ apiKey: sm.apiKey ? SECRET_MASK : '',
6242
6310
  containerTag: sm.containerTag || 'beast',
6243
6311
  up: engine ? !!engine._smUp : null,
6244
6312
  };
@@ -7315,7 +7383,10 @@ ipcMain.handle('providers:keys', () => {
7315
7383
  }
7316
7384
  } catch {}
7317
7385
  }
7318
- 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;
7319
7390
  });
7320
7391
  /* #22 izleyici paneli IPC */
7321
7392
  ipcMain.handle('watchers:list', () => watchers.list());
@@ -10264,7 +10335,17 @@ ipcMain.handle('ide:previewUrl', async (_e, url) => {
10264
10335
  });
10265
10336
 
10266
10337
  ipcMain.handle('custom:set', (_e, list) => {
10267
- 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
+ });
10268
10349
  saveSettings();
10269
10350
  engine.setCustomProviders(settings.customProviders);
10270
10351
  headroom.syncProviders(); /* headroom açıksa proxy setini yeni listeye uydur */
@@ -10893,6 +10974,12 @@ async function deliverFile(sessionId, filePath, caption) {
10893
10974
  try {
10894
10975
  const abs = path.isAbsolute(filePath) ? filePath : path.join(engine.workspace || process.cwd(), filePath);
10895
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
+ }
10896
10983
  const name = path.basename(abs);
10897
10984
  const ext = path.extname(abs).toLowerCase();
10898
10985
  let sid = String(sessionId || '');
@@ -435,14 +435,14 @@
435
435
  security_h2: 'Güvenlik',
436
436
  st_open: 'Aç',
437
437
  st_del: 'Sil',
438
- sec_toggle: 'Tehlikeli işlemlerde onay iste (silme, değiştirme, komut çalıştırma, mail gönderme…)',
439
- sec_sub: 'Kapalıyken her şey serbesttir (varsayım). Açıkken ajan riskli bir araç çağırmadan önce onay ister: sohbette onay kartı + WhatsApp\u2019a mesaj gider.',
438
+ sec_toggle: 'İzin iste: yalnızca silme işlemlerinde onay sor (Beast Code, Sandbox dahil)',
439
+ sec_sub: 'Kapalıyken (varsayılan) HİÇBİR yerde izin istenmez — Beast Code, Sandbox ve tüm araçlar serbest; tüm yetki ajandadır. Açıkken YALNIZCA SİLME işlemlerinde onay istenir: sohbette onay kartı + WhatsApp\u2019a mesaj gider. Diğer işlemler (yazma, düzenleme, komut çalıştırma, mail…) sorulmaz.',
440
440
  sec_note: 'Onay: sohbet kartındaki butonlar ya da /approve · Bu araç için bir daha sorma: /approve always · Reddet: /deny',
441
441
  sec_always_h2: 'Bir Daha Sorulmayan Araçlar',
442
442
  sec_always_empty: 'Henüz "always" onaylı araç yok.',
443
443
  sec_clear_always: 'Listeyi Temizle',
444
- sec_on_toast: 'Onay kapısı AÇIK — riskli işlemlerde ajan sorar',
445
- sec_off_toast: 'Onay kapısı KAPALI — her şey serbest',
444
+ sec_on_toast: 'Onay açık — yalnızca silme işlemlerinde ajan sorar',
445
+ sec_off_toast: 'Onay kapalı — hiçbir yerde sorulmaz, her şey serbest',
446
446
  sec_ok: 'Onayla',
447
447
  sec_always: 'Her zaman',
448
448
  sec_no: 'Reddet',
@@ -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',
@@ -1147,14 +1148,14 @@
1147
1148
  security_h2: 'Security',
1148
1149
  st_open: 'Open',
1149
1150
  st_del: 'Delete',
1150
- sec_toggle: 'Ask approval for dangerous actions (delete, modify, run commands, send mail…)',
1151
- sec_sub: 'When off, everything is free (default). When on, the agent asks approval before a risky tool call: an approval card in chat + a WhatsApp message.',
1151
+ sec_toggle: 'Ask permission: only for deletions (including Beast Code and Sandbox)',
1152
+ sec_sub: 'When off (default) NOTHING asks for permission anywhere — Beast Code, Sandbox and all tools are free; the agent has full authority. When on, approval is asked ONLY for deletions: an approval card in chat + a WhatsApp message. Other actions (write, edit, run commands, mail…) never ask.',
1152
1153
  sec_note: 'Approve: chat card buttons or /approve · Never ask again for this tool: /approve always · Deny: /deny',
1153
1154
  sec_always_h2: 'Tools Never Asked Again',
1154
1155
  sec_always_empty: 'No "always" approved tools yet.',
1155
1156
  sec_clear_always: 'Clear List',
1156
- sec_on_toast: 'Approval gate ON — the agent asks before risky actions',
1157
- sec_off_toast: 'Approval gate OFF — everything free',
1157
+ sec_on_toast: 'Approval ON — the agent asks only for deletions',
1158
+ sec_off_toast: 'Approval OFF — nothing asks, everything free',
1158
1159
  sec_ok: 'Approve',
1159
1160
  sec_always: 'Always',
1160
1161
  sec_no: 'Deny',