daemora 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +666 -0
- package/SOUL.md +104 -0
- package/config/hooks.json +14 -0
- package/config/mcp.json +145 -0
- package/package.json +86 -0
- package/skills/.gitkeep +0 -0
- package/skills/apple-notes.md +193 -0
- package/skills/apple-reminders.md +189 -0
- package/skills/camsnap.md +162 -0
- package/skills/coding.md +14 -0
- package/skills/documents.md +13 -0
- package/skills/email.md +13 -0
- package/skills/gif-search.md +196 -0
- package/skills/healthcheck.md +225 -0
- package/skills/image-gen.md +147 -0
- package/skills/model-usage.md +182 -0
- package/skills/obsidian.md +207 -0
- package/skills/pdf.md +211 -0
- package/skills/research.md +13 -0
- package/skills/skill-creator.md +142 -0
- package/skills/spotify.md +149 -0
- package/skills/summarize.md +230 -0
- package/skills/things.md +199 -0
- package/skills/tmux.md +204 -0
- package/skills/trello.md +183 -0
- package/skills/video-frames.md +202 -0
- package/skills/weather.md +127 -0
- package/src/a2a/A2AClient.js +136 -0
- package/src/a2a/A2AServer.js +316 -0
- package/src/a2a/AgentCard.js +79 -0
- package/src/agents/SubAgentManager.js +369 -0
- package/src/agents/Supervisor.js +192 -0
- package/src/channels/BaseChannel.js +104 -0
- package/src/channels/DiscordChannel.js +288 -0
- package/src/channels/EmailChannel.js +172 -0
- package/src/channels/GoogleChatChannel.js +316 -0
- package/src/channels/HttpChannel.js +26 -0
- package/src/channels/LineChannel.js +168 -0
- package/src/channels/SignalChannel.js +186 -0
- package/src/channels/SlackChannel.js +329 -0
- package/src/channels/TeamsChannel.js +272 -0
- package/src/channels/TelegramChannel.js +347 -0
- package/src/channels/WhatsAppChannel.js +219 -0
- package/src/channels/index.js +198 -0
- package/src/cli.js +1267 -0
- package/src/config/agentProfiles.js +120 -0
- package/src/config/channels.js +32 -0
- package/src/config/default.js +206 -0
- package/src/config/models.js +123 -0
- package/src/config/permissions.js +167 -0
- package/src/core/AgentLoop.js +446 -0
- package/src/core/Compaction.js +143 -0
- package/src/core/CostTracker.js +116 -0
- package/src/core/EventBus.js +46 -0
- package/src/core/Task.js +67 -0
- package/src/core/TaskQueue.js +206 -0
- package/src/core/TaskRunner.js +226 -0
- package/src/daemon/DaemonManager.js +301 -0
- package/src/hooks/HookRunner.js +230 -0
- package/src/index.js +482 -0
- package/src/mcp/MCPAgentRunner.js +112 -0
- package/src/mcp/MCPClient.js +186 -0
- package/src/mcp/MCPManager.js +412 -0
- package/src/models/ModelRouter.js +180 -0
- package/src/safety/AuditLog.js +135 -0
- package/src/safety/CircuitBreaker.js +126 -0
- package/src/safety/FilesystemGuard.js +169 -0
- package/src/safety/GitRollback.js +139 -0
- package/src/safety/HumanApproval.js +156 -0
- package/src/safety/InputSanitizer.js +72 -0
- package/src/safety/PermissionGuard.js +83 -0
- package/src/safety/Sandbox.js +70 -0
- package/src/safety/SecretScanner.js +100 -0
- package/src/safety/SecretVault.js +250 -0
- package/src/scheduler/Heartbeat.js +115 -0
- package/src/scheduler/Scheduler.js +228 -0
- package/src/services/models/outputSchema.js +15 -0
- package/src/services/openai.js +25 -0
- package/src/services/sessions.js +65 -0
- package/src/setup/theme.js +110 -0
- package/src/setup/wizard.js +788 -0
- package/src/skills/SkillLoader.js +168 -0
- package/src/storage/TaskStore.js +69 -0
- package/src/systemPrompt.js +526 -0
- package/src/tenants/TenantContext.js +19 -0
- package/src/tenants/TenantManager.js +379 -0
- package/src/tools/ToolRegistry.js +141 -0
- package/src/tools/applyPatch.js +144 -0
- package/src/tools/browserAutomation.js +223 -0
- package/src/tools/createDocument.js +265 -0
- package/src/tools/cronTool.js +105 -0
- package/src/tools/editFile.js +139 -0
- package/src/tools/executeCommand.js +123 -0
- package/src/tools/glob.js +67 -0
- package/src/tools/grep.js +121 -0
- package/src/tools/imageAnalysis.js +120 -0
- package/src/tools/index.js +173 -0
- package/src/tools/listDirectory.js +47 -0
- package/src/tools/manageAgents.js +47 -0
- package/src/tools/manageMCP.js +159 -0
- package/src/tools/memory.js +478 -0
- package/src/tools/messageChannel.js +45 -0
- package/src/tools/projectTracker.js +259 -0
- package/src/tools/readFile.js +52 -0
- package/src/tools/screenCapture.js +112 -0
- package/src/tools/searchContent.js +76 -0
- package/src/tools/searchFiles.js +75 -0
- package/src/tools/sendEmail.js +118 -0
- package/src/tools/sendFile.js +63 -0
- package/src/tools/textToSpeech.js +161 -0
- package/src/tools/transcribeAudio.js +82 -0
- package/src/tools/useMCP.js +29 -0
- package/src/tools/webFetch.js +150 -0
- package/src/tools/webSearch.js +134 -0
- package/src/tools/writeFile.js +26 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import eventBus from "../core/EventBus.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Git Rollback — snapshot workspace before agent file writes, enable undo.
|
|
6
|
+
*
|
|
7
|
+
* Before the first write tool (writeFile/editFile/applyPatch) in a task,
|
|
8
|
+
* creates a git stash snapshot. If the user later says "undo", the TaskRunner
|
|
9
|
+
* calls undo(taskId) to restore the snapshot.
|
|
10
|
+
*
|
|
11
|
+
* Only activates if the working directory is inside a git repository.
|
|
12
|
+
* Gracefully no-ops if git is unavailable.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class GitRollback {
|
|
16
|
+
constructor() {
|
|
17
|
+
/** taskId → stash message (used to find stash later) */
|
|
18
|
+
this.snapshots = new Map();
|
|
19
|
+
/** taskId → true if we already snapshotted this task */
|
|
20
|
+
this.snapshotted = new Set();
|
|
21
|
+
this.enabled = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Check if cwd is inside a git repo. */
|
|
25
|
+
isGitRepo() {
|
|
26
|
+
try {
|
|
27
|
+
execSync("git rev-parse --git-dir", { stdio: "pipe" });
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a snapshot before the first write in a task.
|
|
36
|
+
* Safe to call multiple times — only snapshots once per task.
|
|
37
|
+
* Returns the stash message used as a key, or null if nothing to snapshot.
|
|
38
|
+
*/
|
|
39
|
+
snapshot(taskId) {
|
|
40
|
+
if (!this.enabled || !taskId) return null;
|
|
41
|
+
if (this.snapshotted.has(taskId)) return null; // already done for this task
|
|
42
|
+
if (!this.isGitRepo()) return null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Check if there are any tracked changes to stash
|
|
46
|
+
const status = execSync("git status --porcelain", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
47
|
+
if (!status) {
|
|
48
|
+
// No changes — mark as snapshotted so we don't keep trying
|
|
49
|
+
this.snapshotted.add(taskId);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const msg = `daemora-snapshot-${taskId.slice(0, 8)}-${Date.now()}`;
|
|
54
|
+
execSync(`git stash push -m "${msg}" --include-untracked`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
55
|
+
|
|
56
|
+
this.snapshotted.add(taskId);
|
|
57
|
+
this.snapshots.set(taskId, msg);
|
|
58
|
+
|
|
59
|
+
console.log(`[GitRollback] Snapshot created for task ${taskId.slice(0, 8)}: "${msg}"`);
|
|
60
|
+
eventBus.emitEvent("audit:git_snapshot", { taskId, ref: msg });
|
|
61
|
+
return msg;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Don't let rollback failures block the agent
|
|
64
|
+
console.log(`[GitRollback] Snapshot failed (non-fatal): ${error.message}`);
|
|
65
|
+
this.snapshotted.add(taskId); // Don't retry
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Roll back changes for a task by popping its stash.
|
|
72
|
+
* Returns a human-readable result string.
|
|
73
|
+
*/
|
|
74
|
+
undo(taskId) {
|
|
75
|
+
if (!this.isGitRepo()) return "Not a git repository — cannot undo.";
|
|
76
|
+
|
|
77
|
+
const stashMsg = this.snapshots.get(taskId);
|
|
78
|
+
if (!stashMsg) {
|
|
79
|
+
return `No snapshot found for this task. Either no files were modified, or the snapshot was already applied.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Find the stash index by message
|
|
84
|
+
const stashList = execSync("git stash list", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
85
|
+
const lines = stashList.split("\n").filter(Boolean);
|
|
86
|
+
const idx = lines.findIndex((l) => l.includes(stashMsg));
|
|
87
|
+
|
|
88
|
+
if (idx === -1) {
|
|
89
|
+
this.snapshots.delete(taskId);
|
|
90
|
+
return `Snapshot not found in git stash list — it may have already been applied or cleared.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// First, discard any uncommitted changes from the agent's work
|
|
94
|
+
execSync("git checkout -- .", { stdio: ["pipe", "pipe", "pipe"] });
|
|
95
|
+
execSync("git clean -fd", { stdio: ["pipe", "pipe", "pipe"] });
|
|
96
|
+
// Restore the stash
|
|
97
|
+
execSync(`git stash pop stash@{${idx}}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
98
|
+
|
|
99
|
+
this.snapshots.delete(taskId);
|
|
100
|
+
this.snapshotted.delete(taskId);
|
|
101
|
+
|
|
102
|
+
console.log(`[GitRollback] Rolled back task ${taskId.slice(0, 8)} from stash@{${idx}}`);
|
|
103
|
+
eventBus.emitEvent("audit:git_rollback", { taskId, ref: stashMsg, success: true });
|
|
104
|
+
return `All agent changes for this task have been rolled back.`;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
eventBus.emitEvent("audit:git_rollback", { taskId, ref: stashMsg, success: false });
|
|
107
|
+
return `Rollback failed: ${error.message}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Drop the snapshot for a completed task (cleanup).
|
|
113
|
+
* Call this when a task completes successfully and user doesn't need undo.
|
|
114
|
+
*/
|
|
115
|
+
dropSnapshot(taskId) {
|
|
116
|
+
if (!this.snapshots.has(taskId)) return;
|
|
117
|
+
const stashMsg = this.snapshots.get(taskId);
|
|
118
|
+
try {
|
|
119
|
+
const stashList = execSync("git stash list", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
120
|
+
const lines = stashList.split("\n").filter(Boolean);
|
|
121
|
+
const idx = lines.findIndex((l) => l.includes(stashMsg));
|
|
122
|
+
if (idx !== -1) {
|
|
123
|
+
execSync(`git stash drop stash@{${idx}}`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
124
|
+
console.log(`[GitRollback] Dropped snapshot for completed task ${taskId.slice(0, 8)}`);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Non-fatal
|
|
128
|
+
}
|
|
129
|
+
this.snapshots.delete(taskId);
|
|
130
|
+
this.snapshotted.delete(taskId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
hasSnapshot(taskId) {
|
|
134
|
+
return this.snapshots.has(taskId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const gitRollback = new GitRollback();
|
|
139
|
+
export default gitRollback;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import eventBus from "../core/EventBus.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Human Approval — pause agent and ask user before dangerous tool calls.
|
|
5
|
+
*
|
|
6
|
+
* Approval modes:
|
|
7
|
+
* "auto" — fully autonomous, no pauses
|
|
8
|
+
* "dangerous-only" — pause before destructive tools (default for most tasks)
|
|
9
|
+
* "every-tool" — approve every single tool call
|
|
10
|
+
*
|
|
11
|
+
* Flow:
|
|
12
|
+
* 1. AgentLoop calls requestApproval(taskId, tool_name, params, channelMeta, mode)
|
|
13
|
+
* 2. HumanApproval emits "approval:request" event (channels pick this up)
|
|
14
|
+
* 3. Channel sends user a message: "Agent wants to run X. Reply approve/deny + requestId"
|
|
15
|
+
* 4. User replies → channel calls humanApproval.handleReply(text)
|
|
16
|
+
* 5. Promise resolves with true/false → AgentLoop proceeds or skips the tool
|
|
17
|
+
*
|
|
18
|
+
* Timeout: if user doesn't reply within timeoutMs, falls back to onTimeout ("deny" by default).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Tools that require approval in "dangerous-only" mode.
|
|
22
|
+
// Only external/irreversible operations that affect people outside the agent:
|
|
23
|
+
// - Communications (email, messages) — can't unsend
|
|
24
|
+
// - Scheduling (cron) — creates recurring side effects
|
|
25
|
+
// File writes, commands, browser, etc. are fully autonomous.
|
|
26
|
+
const DANGEROUS_TOOLS = new Set([
|
|
27
|
+
"sendEmail",
|
|
28
|
+
"messageChannel",
|
|
29
|
+
"cron",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
class HumanApproval {
|
|
33
|
+
constructor() {
|
|
34
|
+
/** Map of requestId → { resolve, timer, taskId, tool_name, params } */
|
|
35
|
+
this.pending = new Map();
|
|
36
|
+
this.timeoutMs = 120_000; // 2 minutes
|
|
37
|
+
this.onTimeout = "deny"; // "deny" | "allow"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check whether a tool call needs approval given the current mode.
|
|
42
|
+
*/
|
|
43
|
+
needsApproval(tool_name, mode) {
|
|
44
|
+
if (!mode || mode === "auto") return false;
|
|
45
|
+
if (mode === "every-tool") return true;
|
|
46
|
+
if (mode === "dangerous-only") return DANGEROUS_TOOLS.has(tool_name);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Request approval from the user for a tool call.
|
|
52
|
+
* Returns a Promise<boolean> — true = approved, false = denied.
|
|
53
|
+
*/
|
|
54
|
+
async requestApproval(taskId, tool_name, params, channelMeta) {
|
|
55
|
+
const requestId = `apr-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
56
|
+
const paramPreview = (params || []).slice(0, 2).map((p) => String(p).slice(0, 80)).join(", ");
|
|
57
|
+
|
|
58
|
+
console.log(`[HumanApproval] Waiting for approval: ${requestId} — ${tool_name}(${paramPreview})`);
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
if (!this.pending.has(requestId)) return;
|
|
63
|
+
this.pending.delete(requestId);
|
|
64
|
+
const decision = this.onTimeout === "allow";
|
|
65
|
+
console.log(`[HumanApproval] Request ${requestId} timed out → ${decision ? "allowed" : "denied"}`);
|
|
66
|
+
eventBus.emitEvent("audit:approval_result", { requestId, tool_name, taskId, approved: decision, source: "timeout" });
|
|
67
|
+
resolve(decision);
|
|
68
|
+
}, this.timeoutMs);
|
|
69
|
+
|
|
70
|
+
this.pending.set(requestId, { resolve, timer, taskId, tool_name, params });
|
|
71
|
+
|
|
72
|
+
// Emit event — channels (Telegram, HTTP) pick this up and message the user
|
|
73
|
+
eventBus.emitEvent("approval:request", {
|
|
74
|
+
requestId,
|
|
75
|
+
taskId,
|
|
76
|
+
tool_name,
|
|
77
|
+
params,
|
|
78
|
+
channelMeta,
|
|
79
|
+
timeoutMs: this.timeoutMs,
|
|
80
|
+
message: this._buildMessage(requestId, tool_name, paramPreview),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
eventBus.emitEvent("audit:approval_requested", { requestId, tool_name, taskId, channelMeta });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Approve a pending request.
|
|
89
|
+
* Returns true if the requestId was found and resolved.
|
|
90
|
+
*/
|
|
91
|
+
approve(requestId, source = "user") {
|
|
92
|
+
const pending = this.pending.get(requestId);
|
|
93
|
+
if (!pending) return false;
|
|
94
|
+
clearTimeout(pending.timer);
|
|
95
|
+
this.pending.delete(requestId);
|
|
96
|
+
console.log(`[HumanApproval] APPROVED: ${requestId} by ${source}`);
|
|
97
|
+
eventBus.emitEvent("audit:approval_result", { requestId, tool_name: pending.tool_name, taskId: pending.taskId, approved: true, source });
|
|
98
|
+
pending.resolve(true);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Deny a pending request.
|
|
104
|
+
*/
|
|
105
|
+
deny(requestId, source = "user") {
|
|
106
|
+
const pending = this.pending.get(requestId);
|
|
107
|
+
if (!pending) return false;
|
|
108
|
+
clearTimeout(pending.timer);
|
|
109
|
+
this.pending.delete(requestId);
|
|
110
|
+
console.log(`[HumanApproval] DENIED: ${requestId} by ${source}`);
|
|
111
|
+
eventBus.emitEvent("audit:approval_result", { requestId, tool_name: pending.tool_name, taskId: pending.taskId, approved: false, source });
|
|
112
|
+
pending.resolve(false);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse a free-form user reply for approval/denial.
|
|
118
|
+
* Returns true if a pending request was found and handled.
|
|
119
|
+
*/
|
|
120
|
+
handleReply(text) {
|
|
121
|
+
const match = text.match(/apr-[a-z0-9]+-[a-z0-9]+/i);
|
|
122
|
+
if (!match) return false;
|
|
123
|
+
const requestId = match[0];
|
|
124
|
+
if (!this.pending.has(requestId)) return false;
|
|
125
|
+
const approved = /\b(yes|approve|allow|ok|okay|go|run|do it|confirm|✓|👍)\b/i.test(text);
|
|
126
|
+
return approved ? this.approve(requestId) : this.deny(requestId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** List all pending approvals. */
|
|
130
|
+
pendingList() {
|
|
131
|
+
return [...this.pending.entries()].map(([id, p]) => ({
|
|
132
|
+
requestId: id,
|
|
133
|
+
taskId: p.taskId,
|
|
134
|
+
tool_name: p.tool_name,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_buildMessage(requestId, tool_name, paramPreview) {
|
|
139
|
+
const lines = [
|
|
140
|
+
`⚠️ Agent wants to run a tool that requires your approval:`,
|
|
141
|
+
``,
|
|
142
|
+
` Tool: ${tool_name}`,
|
|
143
|
+
paramPreview ? ` Args: ${paramPreview}` : null,
|
|
144
|
+
``,
|
|
145
|
+
`Reply with:`,
|
|
146
|
+
` ✅ approve ${requestId}`,
|
|
147
|
+
` ❌ deny ${requestId}`,
|
|
148
|
+
``,
|
|
149
|
+
`(Auto-${this.onTimeout}s in ${Math.round(this.timeoutMs / 1000)}s if no reply)`,
|
|
150
|
+
].filter((l) => l !== null);
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const humanApproval = new HumanApproval();
|
|
156
|
+
export default humanApproval;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Sanitizer — wraps untrusted content to prevent prompt injection.
|
|
3
|
+
*
|
|
4
|
+
* All file content injected into prompts is wrapped with <untrusted-content>
|
|
5
|
+
* tags. The system prompt explicitly tells the agent to treat tagged content
|
|
6
|
+
* as DATA, not instructions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class InputSanitizer {
|
|
10
|
+
/**
|
|
11
|
+
* Wrap file content with untrusted-content tags.
|
|
12
|
+
* @param {string} content - Raw file content
|
|
13
|
+
* @param {string} source - Source description (e.g., "file: /path/to/file")
|
|
14
|
+
* @returns {string} Wrapped content
|
|
15
|
+
*/
|
|
16
|
+
wrapUntrusted(content, source) {
|
|
17
|
+
if (!content) return content;
|
|
18
|
+
return `<untrusted-content source="${source}">\n${content}\n</untrusted-content>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize content that will be injected into a prompt.
|
|
23
|
+
* Removes known injection patterns.
|
|
24
|
+
*/
|
|
25
|
+
sanitize(content) {
|
|
26
|
+
if (!content || typeof content !== "string") return content;
|
|
27
|
+
|
|
28
|
+
let sanitized = content;
|
|
29
|
+
|
|
30
|
+
// Remove attempts to close/override system prompt
|
|
31
|
+
sanitized = sanitized.replace(
|
|
32
|
+
/(?:system|assistant|developer)\s*:\s*/gi,
|
|
33
|
+
"[role-override-removed]: "
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Remove attempts to inject tool calls
|
|
37
|
+
sanitized = sanitized.replace(
|
|
38
|
+
/\{"type"\s*:\s*"tool_call"/g,
|
|
39
|
+
'{"type": "[injection-removed]"'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Remove attempts to set finalResponse
|
|
43
|
+
sanitized = sanitized.replace(
|
|
44
|
+
/"finalResponse"\s*:\s*true/g,
|
|
45
|
+
'"finalResponse": "[injection-removed]"'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return sanitized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sanitize memory write content.
|
|
53
|
+
* Memory entries should be plain text facts, not code or instructions.
|
|
54
|
+
*/
|
|
55
|
+
sanitizeMemoryWrite(content) {
|
|
56
|
+
if (!content) return { valid: false, reason: "Empty content" };
|
|
57
|
+
|
|
58
|
+
// Check for suspicious patterns
|
|
59
|
+
if (content.includes("```") && content.length > 500) {
|
|
60
|
+
return { valid: false, reason: "Memory entries should be plain text facts, not code blocks" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (content.match(/(?:ignore|forget|override|disregard)\s+(?:previous|all|system)/i)) {
|
|
64
|
+
return { valid: false, reason: "Memory entry contains suspicious override attempt" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { valid: true, content: content.trim() };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const inputSanitizer = new InputSanitizer();
|
|
72
|
+
export default inputSanitizer;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { config } from "../config/default.js";
|
|
2
|
+
import { permissionTiers } from "../config/permissions.js";
|
|
3
|
+
import eventBus from "../core/EventBus.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Permission Guard — enforces tool access based on permission tiers.
|
|
7
|
+
*
|
|
8
|
+
* 3 tiers:
|
|
9
|
+
* - minimal: read-only tools only
|
|
10
|
+
* - standard: + write/edit/sandboxed commands
|
|
11
|
+
* - full: everything including email, unsandboxed commands, agents
|
|
12
|
+
*/
|
|
13
|
+
class PermissionGuard {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.tier = config.permissionTier;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a tool is allowed under the current permission tier.
|
|
20
|
+
* @param {string} toolName - Name of the tool
|
|
21
|
+
* @param {object} [params] - Tool parameters (for fine-grained checks)
|
|
22
|
+
* @returns {{ allowed: boolean, reason?: string }}
|
|
23
|
+
*/
|
|
24
|
+
check(toolName, params) {
|
|
25
|
+
const tierConfig = permissionTiers[this.tier];
|
|
26
|
+
if (!tierConfig) {
|
|
27
|
+
return { allowed: false, reason: `Unknown permission tier: ${this.tier}` };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MCP tools (mcp__server__tool) are user-configured integrations.
|
|
31
|
+
// Allow them in standard and full tiers — the user explicitly set them up.
|
|
32
|
+
if (toolName.startsWith("mcp__")) {
|
|
33
|
+
if (this.tier === "minimal") {
|
|
34
|
+
eventBus.emitEvent("permission:denied", { toolName, tier: this.tier });
|
|
35
|
+
return { allowed: false, reason: `MCP tools not available in minimal permission tier.` };
|
|
36
|
+
}
|
|
37
|
+
return { allowed: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if tool is in the allowed list
|
|
41
|
+
if (!tierConfig.allowedTools.includes(toolName) && !tierConfig.allowedTools.includes("*")) {
|
|
42
|
+
eventBus.emitEvent("permission:denied", { toolName, tier: this.tier });
|
|
43
|
+
return {
|
|
44
|
+
allowed: false,
|
|
45
|
+
reason: `Tool "${toolName}" not allowed in "${this.tier}" permission tier. Allowed: ${tierConfig.allowedTools.join(", ")}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { allowed: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Filter a tool map to only include allowed tools.
|
|
54
|
+
*/
|
|
55
|
+
filterTools(tools) {
|
|
56
|
+
const tierConfig = permissionTiers[this.tier];
|
|
57
|
+
if (!tierConfig) return tools;
|
|
58
|
+
if (tierConfig.allowedTools.includes("*")) return tools;
|
|
59
|
+
|
|
60
|
+
const filtered = {};
|
|
61
|
+
for (const [name, fn] of Object.entries(tools)) {
|
|
62
|
+
// MCP tools pass through in standard/full tier
|
|
63
|
+
if (name.startsWith("mcp__")) {
|
|
64
|
+
if (this.tier !== "minimal") filtered[name] = fn;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (tierConfig.allowedTools.includes(name)) {
|
|
68
|
+
filtered[name] = fn;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return filtered;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get current tier name.
|
|
76
|
+
*/
|
|
77
|
+
getTier() {
|
|
78
|
+
return this.tier;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const permissionGuard = new PermissionGuard();
|
|
83
|
+
export default permissionGuard;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { blockedCommands } from "../config/permissions.js";
|
|
2
|
+
import eventBus from "../core/EventBus.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox — command execution safety.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
* 1. Blocklist (default): Block known dangerous commands via regex patterns.
|
|
9
|
+
* 2. Docker (optional): Run in ephemeral container with restricted access.
|
|
10
|
+
*
|
|
11
|
+
* Blocklist catches:
|
|
12
|
+
* - rm -rf /, sudo rm, mkfs, dd if=, curl|sh, chmod 777 /, > /dev/sda
|
|
13
|
+
* - Fork bombs, shutdown/reboot, format commands
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
class Sandbox {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.mode = process.env.SANDBOX_MODE || "blocklist";
|
|
19
|
+
this.blockedCount = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a command is safe to execute.
|
|
24
|
+
* @param {string} command - The command to check
|
|
25
|
+
* @returns {{ safe: boolean, reason?: string }}
|
|
26
|
+
*/
|
|
27
|
+
check(command) {
|
|
28
|
+
if (!command || typeof command !== "string") {
|
|
29
|
+
return { safe: false, reason: "Empty command" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cmd = command.toLowerCase().trim();
|
|
33
|
+
|
|
34
|
+
// Check against blocked patterns
|
|
35
|
+
for (const pattern of blockedCommands) {
|
|
36
|
+
if (pattern.test(cmd)) {
|
|
37
|
+
this.blockedCount++;
|
|
38
|
+
eventBus.emitEvent("sandbox:blocked", {
|
|
39
|
+
command: command.slice(0, 100),
|
|
40
|
+
pattern: pattern.toString(),
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
safe: false,
|
|
44
|
+
reason: `Command blocked by safety sandbox: matches pattern ${pattern}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Additional heuristic checks
|
|
50
|
+
if (cmd.includes(":(){ :|:& };:") || cmd.includes("fork bomb")) {
|
|
51
|
+
this.blockedCount++;
|
|
52
|
+
return { safe: false, reason: "Fork bomb detected" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { safe: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get stats.
|
|
60
|
+
*/
|
|
61
|
+
stats() {
|
|
62
|
+
return {
|
|
63
|
+
mode: this.mode,
|
|
64
|
+
blockedCount: this.blockedCount,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sandbox = new Sandbox();
|
|
70
|
+
export default sandbox;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import eventBus from "../core/EventBus.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secret Scanner — detects and redacts sensitive data in tool I/O.
|
|
5
|
+
*
|
|
6
|
+
* Scans for: API keys, private keys, passwords, AWS keys, tokens,
|
|
7
|
+
* connection strings, .env contents.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
{ name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
12
|
+
{ name: "AWS Secret Key", pattern: /(?:aws_secret_access_key|secret_key)\s*[:=]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi },
|
|
13
|
+
{ name: "Generic API Key", pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([A-Za-z0-9_\-]{20,})['"]?/gi },
|
|
14
|
+
{ name: "Generic Secret", pattern: /(?:secret|password|passwd|pwd|token)\s*[:=]\s*['"]?([^\s'"]{8,})['"]?/gi },
|
|
15
|
+
{ name: "Private Key", pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
16
|
+
{ name: "GitHub Token", pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g },
|
|
17
|
+
{ name: "Slack Token", pattern: /xox[bprs]-[A-Za-z0-9\-]{10,}/g },
|
|
18
|
+
{ name: "OpenAI Key", pattern: /sk-[A-Za-z0-9]{20,}/g },
|
|
19
|
+
{ name: "Bearer Token", pattern: /Bearer\s+[A-Za-z0-9\-._~+\/]{20,}/g },
|
|
20
|
+
{ name: "Connection String", pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^\s'"]+/gi },
|
|
21
|
+
{ name: "JWT", pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
class SecretScanner {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.detectionCount = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scan text for secrets and return findings.
|
|
31
|
+
* @param {string} text - Text to scan
|
|
32
|
+
* @returns {{ found: boolean, secrets: Array, redacted: string }}
|
|
33
|
+
*/
|
|
34
|
+
scan(text) {
|
|
35
|
+
if (!text || typeof text !== "string") {
|
|
36
|
+
return { found: false, secrets: [], redacted: text };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const secrets = [];
|
|
40
|
+
let redacted = text;
|
|
41
|
+
|
|
42
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
43
|
+
// Reset regex state
|
|
44
|
+
pattern.lastIndex = 0;
|
|
45
|
+
let match;
|
|
46
|
+
|
|
47
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
48
|
+
const value = match[0];
|
|
49
|
+
// Skip very short matches or common false positives
|
|
50
|
+
if (value.length < 8) continue;
|
|
51
|
+
|
|
52
|
+
secrets.push({
|
|
53
|
+
type: name,
|
|
54
|
+
value: `${value.slice(0, 4)}...${value.slice(-4)}`,
|
|
55
|
+
position: match.index,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Redact in output
|
|
59
|
+
redacted = redacted.replace(value, `[REDACTED:${name}]`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (secrets.length > 0) {
|
|
64
|
+
this.detectionCount += secrets.length;
|
|
65
|
+
eventBus.emitEvent("secret:detected", {
|
|
66
|
+
count: secrets.length,
|
|
67
|
+
types: [...new Set(secrets.map((s) => s.type))],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
found: secrets.length > 0,
|
|
73
|
+
secrets,
|
|
74
|
+
redacted,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Scan and redact tool output before feeding back to agent.
|
|
80
|
+
*/
|
|
81
|
+
redactOutput(text) {
|
|
82
|
+
const result = this.scan(text);
|
|
83
|
+
if (result.found) {
|
|
84
|
+
console.log(
|
|
85
|
+
` [SecretScanner] Redacted ${result.secrets.length} secret(s): ${result.secrets.map((s) => s.type).join(", ")}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return result.redacted;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get detection stats.
|
|
93
|
+
*/
|
|
94
|
+
stats() {
|
|
95
|
+
return { totalDetections: this.detectionCount };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const secretScanner = new SecretScanner();
|
|
100
|
+
export default secretScanner;
|