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.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. 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;