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 +1 -1
- package/src/agent/engine.js +95 -22
- package/src/agent/fsguard.js +46 -0
- package/src/agent/mcp.js +98 -18
- package/src/agent/opencode/askperm.js +7 -1
- package/src/agent/tools.js +114 -4
- package/src/main.js +108 -21
- package/src/renderer/i18n.js +39 -38
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -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
|
-
|
|
1612
|
-
|
|
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
|
|
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
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
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'ı):
|
|
4979
|
-
|
|
4980
|
-
ask
|
|
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ı:
|
|
5004
|
-
|
|
5005
|
-
izin sistemi
|
|
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
|
-
|
|
5008
|
-
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
const
|
|
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
|
|
package/src/agent/tools.js
CHANGED
|
@@ -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
|
-
|
|
1010
|
-
|
|
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
|
|
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
|
-
|
|
2353
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
6138
|
-
|
|
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++)
|
|
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
|
|
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
|
|
6245
|
+
key,
|
|
6185
6246
|
};
|
|
6186
6247
|
}
|
|
6187
6248
|
settings.fallout = next;
|
|
6188
6249
|
saveSettings();
|
|
6189
6250
|
if (engine) engine.setFallout(next);
|
|
6190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || '');
|
package/src/renderer/i18n.js
CHANGED
|
@@ -435,14 +435,14 @@
|
|
|
435
435
|
security_h2: 'Güvenlik',
|
|
436
436
|
st_open: 'Aç',
|
|
437
437
|
st_del: 'Sil',
|
|
438
|
-
sec_toggle: '
|
|
439
|
-
sec_sub: 'Kapalıyken
|
|
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
|
|
445
|
-
sec_off_toast: 'Onay
|
|
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
|
|
1151
|
-
sec_sub: 'When off,
|
|
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
|
|
1157
|
-
sec_off_toast: 'Approval
|
|
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',
|