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,180 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ import { createAnthropic } from "@ai-sdk/anthropic";
3
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
4
+ import { createOllama } from "ollama-ai-provider";
5
+ import { models, fallbackChains } from "../config/models.js";
6
+
7
+ /**
8
+ * Provider factory — lazily created so vault secrets are available.
9
+ * Per-tenant apiKeys overlay: if apiKeys[KEY] is set, create a fresh provider instance
10
+ * (never cached) to avoid cross-tenant bleed in concurrent requests.
11
+ */
12
+ const providerCache = {};
13
+
14
+ function getProvider(name, apiKeys = {}) {
15
+ const keyMap = { openai: "OPENAI_API_KEY", anthropic: "ANTHROPIC_API_KEY", google: "GOOGLE_AI_API_KEY" };
16
+ const envKeyName = keyMap[name];
17
+ const tenantKey = envKeyName ? apiKeys[envKeyName] : null;
18
+
19
+ if (name === "ollama") {
20
+ if (providerCache[name]) return providerCache[name];
21
+ providerCache[name] = createOllama({ baseURL: process.env.OLLAMA_BASE_URL || "http://localhost:11434/api" });
22
+ return providerCache[name];
23
+ }
24
+
25
+ const globalKey = envKeyName ? process.env[envKeyName] : null;
26
+ if (!tenantKey && !globalKey) return null;
27
+
28
+ if (tenantKey) {
29
+ // Per-tenant key: always create a fresh instance — never cache, prevents cross-tenant bleed
30
+ if (name === "openai") return createOpenAI({ apiKey: tenantKey });
31
+ if (name === "anthropic") return createAnthropic({ apiKey: tenantKey });
32
+ if (name === "google") return createGoogleGenerativeAI({ apiKey: tenantKey });
33
+ }
34
+
35
+ // Global key: existing singleton cache behavior (zero overhead for single-user mode)
36
+ if (providerCache[name]) return providerCache[name];
37
+ if (name === "openai") providerCache[name] = createOpenAI({ apiKey: globalKey });
38
+ if (name === "anthropic") providerCache[name] = createAnthropic({ apiKey: globalKey });
39
+ if (name === "google") providerCache[name] = createGoogleGenerativeAI({ apiKey: globalKey });
40
+ return providerCache[name] || null;
41
+ }
42
+
43
+ /**
44
+ * Get a Vercel AI SDK model instance from a "provider:model" string.
45
+ *
46
+ * @param {string} modelId - e.g. "openai:gpt-4.1-mini" or "anthropic:claude-sonnet-4-6"
47
+ * @param {object} apiKeys - Per-tenant API key overlay { OPENAI_API_KEY: "...", ... }
48
+ * @returns {{ model: object, meta: object }} AI SDK model instance + metadata
49
+ */
50
+ export function getModel(modelId, apiKeys = {}) {
51
+ const meta = models[modelId];
52
+ if (!meta) {
53
+ throw new Error(`Unknown model: ${modelId}. Available: ${Object.keys(models).join(", ")}`);
54
+ }
55
+
56
+ const provider = getProvider(meta.provider, apiKeys);
57
+ if (!provider) {
58
+ throw new Error(
59
+ `Provider "${meta.provider}" not configured. Set the API key in .env (e.g. ${meta.provider.toUpperCase()}_API_KEY).`
60
+ );
61
+ }
62
+
63
+ return {
64
+ model: provider(meta.model),
65
+ meta,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Get model with fallback chain.
71
+ * Tries the preferred model first, then falls through the chain.
72
+ *
73
+ * @param {string} preferredModelId - e.g. "openai:gpt-4.1-mini"
74
+ * @param {object} apiKeys - Per-tenant API key overlay
75
+ * @returns {{ model: object, meta: object, modelId: string }}
76
+ */
77
+ export function getModelWithFallback(preferredModelId, apiKeys = {}) {
78
+ // Try preferred model first
79
+ try {
80
+ const result = getModel(preferredModelId, apiKeys);
81
+ return { ...result, modelId: preferredModelId };
82
+ } catch (e) {
83
+ console.log(`[ModelRouter] Preferred model "${preferredModelId}" unavailable: ${e.message}`);
84
+ }
85
+
86
+ // Find the fallback chain for this model's tier
87
+ const meta = models[preferredModelId];
88
+ const tier = meta?.tier || "cheap";
89
+ const chain = fallbackChains[tier] || fallbackChains.cheap;
90
+
91
+ for (const fallbackId of chain) {
92
+ if (fallbackId === preferredModelId) continue;
93
+ try {
94
+ const result = getModel(fallbackId, apiKeys);
95
+ console.log(`[ModelRouter] Falling back to "${fallbackId}"`);
96
+ return { ...result, modelId: fallbackId };
97
+ } catch (e) {
98
+ continue;
99
+ }
100
+ }
101
+
102
+ throw new Error(`No available model found. Tried: ${preferredModelId} + fallback chain.`);
103
+ }
104
+
105
+ /**
106
+ * Get the cheapest available model (for compaction/summarization).
107
+ *
108
+ * @param {object} apiKeys - Per-tenant API key overlay
109
+ */
110
+ export function getCheapModel(apiKeys = {}) {
111
+ const cheapChain = ["google:gemini-2.0-flash", "openai:gpt-4.1-mini", "anthropic:claude-haiku-4-5"];
112
+ for (const modelId of cheapChain) {
113
+ try {
114
+ const result = getModel(modelId, apiKeys);
115
+ return { ...result, modelId };
116
+ } catch {
117
+ continue;
118
+ }
119
+ }
120
+ // Last resort: whatever the default is
121
+ const defaultId = process.env.DEFAULT_MODEL || "openai:gpt-4.1-mini";
122
+ return { ...getModel(defaultId, apiKeys), modelId: defaultId };
123
+ }
124
+
125
+ /**
126
+ * List all available models (ones that have API keys configured).
127
+ */
128
+ export function listAvailableModels() {
129
+ const available = [];
130
+ for (const modelId of Object.keys(models)) {
131
+ try {
132
+ getModel(modelId);
133
+ available.push({ id: modelId, ...models[modelId] });
134
+ } catch {
135
+ // skip unavailable
136
+ }
137
+ }
138
+ return available;
139
+ }
140
+
141
+ // ── Task-Type Model Routing ────────────────────────────────────────────────────
142
+
143
+ const _profileEnvMap = {
144
+ coder: "CODE_MODEL",
145
+ researcher: "RESEARCH_MODEL",
146
+ writer: "WRITER_MODEL",
147
+ analyst: "ANALYST_MODEL",
148
+ };
149
+
150
+ /**
151
+ * Resolve the best model for a given agent profile, using a priority chain:
152
+ * 1. explicitModel (caller override)
153
+ * 2. Per-tenant modelRoutes[profile]
154
+ * 3. Global CODE_MODEL / RESEARCH_MODEL / WRITER_MODEL / ANALYST_MODEL env vars
155
+ * 4. Per-tenant general model override
156
+ * 5. DEFAULT_MODEL env var / hardcoded default
157
+ *
158
+ * @param {string|null} profile - e.g. "coder", "researcher", "writer", "analyst"
159
+ * @param {object} tenantConfig - resolvedConfig from TenantManager (may have .modelRoutes, .model)
160
+ * @param {string|null} explicitModel - Caller-supplied model override (highest priority)
161
+ * @returns {string} Resolved model ID
162
+ */
163
+ export function resolveModelForProfile(profile, tenantConfig = {}, explicitModel = null) {
164
+ if (explicitModel) return explicitModel;
165
+ if (profile && tenantConfig.modelRoutes?.[profile]) return tenantConfig.modelRoutes[profile];
166
+ if (profile && process.env[_profileEnvMap[profile]]) return process.env[_profileEnvMap[profile]];
167
+ if (tenantConfig.model) return tenantConfig.model;
168
+ return process.env.DEFAULT_MODEL || "openai:gpt-4.1-mini";
169
+ }
170
+
171
+ /**
172
+ * Get the task-type model for a profile from global env vars only.
173
+ * Utility for callers without a tenant config.
174
+ *
175
+ * @param {string|null} profile - e.g. "coder", "researcher"
176
+ * @returns {string} Model ID
177
+ */
178
+ export function getTaskTypeModel(profile) {
179
+ return (profile && process.env[_profileEnvMap[profile]]) || process.env.DEFAULT_MODEL || "openai:gpt-4.1-mini";
180
+ }
@@ -0,0 +1,135 @@
1
+ import { appendFileSync, mkdirSync } from "fs";
2
+ import { config } from "../config/default.js";
3
+ import eventBus from "../core/EventBus.js";
4
+ import tenantContext from "../tenants/TenantContext.js";
5
+
6
+ /**
7
+ * Audit Log — append-only logging of all agent actions.
8
+ *
9
+ * Writes to: data/audit/YYYY-MM-DD.jsonl
10
+ *
11
+ * Listens to EventBus events already emitted by AgentLoop, Supervisor,
12
+ * SubAgentManager, and TaskRunner. No changes needed to other files —
13
+ * just start() this and it captures everything automatically.
14
+ */
15
+
16
+ class AuditLog {
17
+ constructor() {
18
+ this.enabled = true;
19
+ this.entryCount = 0;
20
+ }
21
+
22
+ start() {
23
+ mkdirSync(config.auditDir, { recursive: true });
24
+
25
+ // ── Tool lifecycle (emitted by AgentLoop) ─────────────────────────────
26
+ eventBus.on("tool:before", ({ tool_name, params, taskId, stepCount }) => {
27
+ this.write({ event: "tool_attempted", tool_name, taskId, stepCount,
28
+ params: params?.slice(0, 3).map((p) => String(p).slice(0, 120)) });
29
+ });
30
+
31
+ eventBus.on("tool:after", ({ tool_name, taskId, stepCount, duration, outputLength, error }) => {
32
+ if (error) {
33
+ this.write({ event: "tool_failed", tool_name, taskId, stepCount, duration, error });
34
+ } else {
35
+ this.write({ event: "tool_executed", tool_name, taskId, stepCount, duration, outputLength });
36
+ }
37
+ });
38
+
39
+ // ── Model calls (emitted by AgentLoop) ───────────────────────────────
40
+ eventBus.on("model:called", ({ modelId, loopCount, elapsed, inputTokens, outputTokens, taskId }) => {
41
+ this.write({ event: "model_called", modelId, loopCount, elapsed, inputTokens, outputTokens, taskId });
42
+ });
43
+
44
+ // ── Agent lifecycle (emitted by SubAgentManager) ─────────────────────
45
+ eventBus.on("agent:spawned", ({ agentId, taskDescription, depth, parentTaskId }) => {
46
+ this.write({ event: "agent_spawned", agentId, depth, parentTaskId,
47
+ task: taskDescription?.slice(0, 120) });
48
+ });
49
+
50
+ eventBus.on("agent:killed", ({ agentId, reason }) => {
51
+ this.write({ event: "agent_killed", agentId, reason });
52
+ });
53
+
54
+ // ── Safety events (emitted by PermissionGuard, Sandbox, HumanApproval, AgentLoop) ──
55
+ eventBus.on("audit:permission_denied", ({ tool_name, reason, taskId }) => {
56
+ this.write({ event: "permission_denied", tool_name, reason, taskId });
57
+ });
58
+
59
+ eventBus.on("audit:sandbox_blocked", ({ command, reason, taskId }) => {
60
+ this.write({ event: "sandbox_blocked", command: command?.slice(0, 200), reason, taskId });
61
+ });
62
+
63
+ eventBus.on("audit:hook_blocked", ({ tool_name, reason, taskId }) => {
64
+ this.write({ event: "hook_blocked", tool_name, reason, taskId });
65
+ });
66
+
67
+ eventBus.on("audit:secret_detected", ({ tool_name, taskId, count }) => {
68
+ this.write({ event: "secret_detected", tool_name, taskId, count });
69
+ });
70
+
71
+ eventBus.on("audit:approval_requested", ({ requestId, tool_name, taskId, channelMeta }) => {
72
+ this.write({ event: "approval_requested", requestId, tool_name, taskId,
73
+ channel: channelMeta?.channel });
74
+ });
75
+
76
+ eventBus.on("audit:approval_result", ({ requestId, tool_name, taskId, approved, source }) => {
77
+ this.write({ event: "approval_result", requestId, tool_name, taskId, approved, source });
78
+ });
79
+
80
+ eventBus.on("audit:git_snapshot", ({ taskId, ref }) => {
81
+ this.write({ event: "git_snapshot", taskId, ref });
82
+ });
83
+
84
+ eventBus.on("audit:git_rollback", ({ taskId, ref, success }) => {
85
+ this.write({ event: "git_rollback", taskId, ref, success });
86
+ });
87
+
88
+ eventBus.on("audit:memory_written", ({ category, entryLength, taskId }) => {
89
+ this.write({ event: "memory_written", category, entryLength, taskId });
90
+ });
91
+
92
+ // ── Supervisor (emitted by Supervisor) ───────────────────────────────
93
+ eventBus.on("supervisor:warning", (data) => {
94
+ this.write({ event: "supervisor_warning", ...data });
95
+ });
96
+
97
+ eventBus.on("supervisor:alert", (data) => {
98
+ this.write({ event: "supervisor_alert", ...data });
99
+ });
100
+
101
+ eventBus.on("supervisor:kill", ({ taskId, reason }) => {
102
+ this.write({ event: "supervisor_kill", taskId, reason });
103
+ });
104
+
105
+ // ── Manual audit events (catch-all for anything else) ────────────────
106
+ eventBus.on("audit:event", (data) => {
107
+ if (!this.enabled) return;
108
+ this.write(data);
109
+ });
110
+
111
+ console.log(`[AuditLog] Started — logging to ${config.auditDir}`);
112
+ }
113
+
114
+ write(data) {
115
+ if (!this.enabled) return;
116
+ try {
117
+ const today = new Date().toISOString().split("T")[0];
118
+ const logFile = `${config.auditDir}/${today}.jsonl`;
119
+ const tenantId = tenantContext.getStore()?.tenant?.id || null;
120
+ const entry = { timestamp: new Date().toISOString(), tenantId, ...data };
121
+ appendFileSync(logFile, JSON.stringify(entry) + "\n", "utf-8");
122
+ this.entryCount++;
123
+ } catch (error) {
124
+ // Never let audit failures crash the system
125
+ console.log(`[AuditLog] Write error: ${error.message}`);
126
+ }
127
+ }
128
+
129
+ stats() {
130
+ return { enabled: this.enabled, entriesWritten: this.entryCount };
131
+ }
132
+ }
133
+
134
+ const auditLog = new AuditLog();
135
+ export default auditLog;
@@ -0,0 +1,126 @@
1
+ import eventBus from "../core/EventBus.js";
2
+
3
+ /**
4
+ * Circuit Breaker — prevents cascading failures in agent chains.
5
+ *
6
+ * Per-agent: 3 consecutive failures → stop the agent.
7
+ * Per-tool: 5 failures in 1 minute → disable tool temporarily.
8
+ * Reset after cooldown period.
9
+ */
10
+
11
+ class CircuitBreaker {
12
+ constructor() {
13
+ // Track failures per tool
14
+ this.toolFailures = new Map(); // toolName -> { count, lastFailure, disabled }
15
+ // Track failures per task/agent
16
+ this.agentFailures = new Map(); // taskId -> { consecutive, lastFailure }
17
+
18
+ this.maxToolFailures = 5;
19
+ this.toolCooldownMs = 60000; // 1 minute
20
+ this.maxAgentFailures = 3;
21
+ }
22
+
23
+ /**
24
+ * Record a tool failure.
25
+ * @returns {{ tripped: boolean, reason?: string }}
26
+ */
27
+ recordToolFailure(toolName) {
28
+ const now = Date.now();
29
+ const entry = this.toolFailures.get(toolName) || {
30
+ count: 0,
31
+ firstFailure: now,
32
+ disabled: false,
33
+ };
34
+
35
+ // Reset if cooldown has passed
36
+ if (now - entry.firstFailure > this.toolCooldownMs) {
37
+ entry.count = 0;
38
+ entry.firstFailure = now;
39
+ entry.disabled = false;
40
+ }
41
+
42
+ entry.count++;
43
+
44
+ if (entry.count >= this.maxToolFailures) {
45
+ entry.disabled = true;
46
+ this.toolFailures.set(toolName, entry);
47
+ eventBus.emitEvent("circuit:tool_disabled", {
48
+ toolName,
49
+ failures: entry.count,
50
+ });
51
+ return {
52
+ tripped: true,
53
+ reason: `Tool "${toolName}" disabled: ${entry.count} failures in ${this.toolCooldownMs / 1000}s. Will reset automatically.`,
54
+ };
55
+ }
56
+
57
+ this.toolFailures.set(toolName, entry);
58
+ return { tripped: false };
59
+ }
60
+
61
+ /**
62
+ * Record an agent/task failure.
63
+ * @returns {{ tripped: boolean, reason?: string }}
64
+ */
65
+ recordAgentFailure(taskId) {
66
+ const entry = this.agentFailures.get(taskId) || { consecutive: 0 };
67
+ entry.consecutive++;
68
+
69
+ if (entry.consecutive >= this.maxAgentFailures) {
70
+ this.agentFailures.set(taskId, entry);
71
+ eventBus.emitEvent("circuit:agent_stopped", {
72
+ taskId,
73
+ failures: entry.consecutive,
74
+ });
75
+ return {
76
+ tripped: true,
77
+ reason: `Agent stopped: ${entry.consecutive} consecutive failures. Task may need human intervention.`,
78
+ };
79
+ }
80
+
81
+ this.agentFailures.set(taskId, entry);
82
+ return { tripped: false };
83
+ }
84
+
85
+ /**
86
+ * Record success — resets consecutive failure counter.
87
+ */
88
+ recordSuccess(taskId) {
89
+ if (taskId) {
90
+ this.agentFailures.delete(taskId);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Check if a tool is currently disabled.
96
+ */
97
+ isToolDisabled(toolName) {
98
+ const entry = this.toolFailures.get(toolName);
99
+ if (!entry || !entry.disabled) return false;
100
+
101
+ // Check if cooldown has passed
102
+ if (Date.now() - entry.firstFailure > this.toolCooldownMs) {
103
+ entry.disabled = false;
104
+ entry.count = 0;
105
+ this.toolFailures.set(toolName, entry);
106
+ return false;
107
+ }
108
+
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * Get stats.
114
+ */
115
+ stats() {
116
+ return {
117
+ disabledTools: [...this.toolFailures.entries()]
118
+ .filter(([, e]) => e.disabled)
119
+ .map(([name]) => name),
120
+ activeBreakers: this.agentFailures.size,
121
+ };
122
+ }
123
+ }
124
+
125
+ const circuitBreaker = new CircuitBreaker();
126
+ export default circuitBreaker;
@@ -0,0 +1,169 @@
1
+ import { resolve, sep } from "path";
2
+ import { config } from "../config/default.js";
3
+ import eventBus from "../core/EventBus.js";
4
+ import tenantContext from "../tenants/TenantContext.js";
5
+
6
+ /**
7
+ * Filesystem Guard — restricts file access to safe paths.
8
+ *
9
+ * Two layers of protection:
10
+ *
11
+ * 1. HARDCODED BLOCKED PATTERNS — sensitive system files that are ALWAYS blocked
12
+ * (~/.ssh, .env, /etc/shadow, certificates, etc.)
13
+ *
14
+ * 2. USER-CONFIGURABLE SCOPING — like Docker volume mounts
15
+ * ALLOWED_PATHS=/Users/you/Downloads,/Users/you/Projects
16
+ * → Agent can ONLY access files inside those directories.
17
+ * → If unset: no directory restriction (global mode).
18
+ * BLOCKED_PATHS=/Users/you/Desktop,/Users/you/Documents
19
+ * → Always blocked regardless of ALLOWED_PATHS.
20
+ * → Useful for saying "everything except these folders".
21
+ *
22
+ * Examples:
23
+ * ALLOWED_PATHS=/home/john/workspace → locked to workspace only
24
+ * BLOCKED_PATHS=/home/john/private → blocks one folder, rest is open
25
+ * (neither set) → only hardcoded patterns blocked
26
+ */
27
+
28
+ // Paths the agent should NEVER read or write
29
+ const BLOCKED_PATTERNS = [
30
+ /\.ssh[\/\\]/,
31
+ /\.gnupg[\/\\]/,
32
+ /\.vault\.enc$/,
33
+ /\.vault\.salt$/,
34
+ /\/etc\/shadow/,
35
+ /\/etc\/sudoers/,
36
+ /\.aws\/credentials/,
37
+ /\.docker\/config\.json/,
38
+ /\.kube\/config/,
39
+ /\.npmrc$/,
40
+ /\.pypirc$/,
41
+ /\.netrc$/,
42
+ /id_rsa/,
43
+ /id_ed25519/,
44
+ /id_ecdsa/,
45
+ /\.pem$/,
46
+ /\.key$/,
47
+ ];
48
+
49
+ // Patterns to block writing to (reading is ok)
50
+ const WRITE_BLOCKED_PATTERNS = [
51
+ /\.env$/,
52
+ /\.env\..+$/,
53
+ /\/etc\//,
54
+ /\/usr\//,
55
+ /\/bin\//,
56
+ /\/sbin\//,
57
+ /\/System\//,
58
+ /\/Library\/LaunchDaemons\//,
59
+ ];
60
+
61
+ class FilesystemGuard {
62
+ constructor() {
63
+ this.blockedCount = 0;
64
+ }
65
+
66
+ /**
67
+ * Check if a read operation is allowed.
68
+ * @param {string} filePath
69
+ * @returns {{ allowed: boolean, reason?: string }}
70
+ */
71
+ checkRead(filePath) {
72
+ return this._check(filePath, "read");
73
+ }
74
+
75
+ /**
76
+ * Check if a write operation is allowed.
77
+ * @param {string} filePath
78
+ * @returns {{ allowed: boolean, reason?: string }}
79
+ */
80
+ checkWrite(filePath) {
81
+ return this._check(filePath, "write");
82
+ }
83
+
84
+ _check(filePath, operation) {
85
+ if (!filePath) return { allowed: false, reason: "No file path provided" };
86
+
87
+ const resolved = resolve(filePath);
88
+
89
+ // ── Layer 1: Hardcoded blocked patterns ───────────────────────────────────
90
+ for (const pattern of BLOCKED_PATTERNS) {
91
+ if (pattern.test(resolved)) {
92
+ this._block(operation, resolved, pattern.toString());
93
+ return {
94
+ allowed: false,
95
+ reason: `Access denied: "${filePath}" matches a blocked path pattern (sensitive file).`,
96
+ };
97
+ }
98
+ }
99
+
100
+ if (operation === "write") {
101
+ for (const pattern of WRITE_BLOCKED_PATTERNS) {
102
+ if (pattern.test(resolved)) {
103
+ this._block(operation, resolved, pattern.toString());
104
+ return {
105
+ allowed: false,
106
+ reason: `Write access denied: "${filePath}" is in a protected system directory.`,
107
+ };
108
+ }
109
+ }
110
+ }
111
+
112
+ // ── Resolve per-tenant or global path config ──────────────────────────────
113
+ // When a task runs inside tenantContext.run(), use per-tenant resolved config.
114
+ // Otherwise fall back to global filesystem config.
115
+ const store = tenantContext.getStore();
116
+ const resolvedConfig = store?.resolvedConfig;
117
+
118
+ // ── Layer 2: User-defined blocked paths ───────────────────────────────────
119
+ const userBlocked = resolvedConfig
120
+ ? (resolvedConfig.blockedPaths || [])
121
+ : (config.filesystem?.blockedPaths || []);
122
+ for (const dir of userBlocked) {
123
+ const dirResolved = resolve(dir);
124
+ if (resolved === dirResolved || resolved.startsWith(dirResolved + sep) || resolved.startsWith(dirResolved + "/")) {
125
+ this._block(operation, resolved, `user-blocked:${dir}`);
126
+ return {
127
+ allowed: false,
128
+ reason: `Access denied: "${filePath}" is in a blocked directory ("${dir}").`,
129
+ };
130
+ }
131
+ }
132
+
133
+ // ── Layer 3: User-defined allowed paths (scoped mode) ─────────────────────
134
+ const userAllowed = resolvedConfig
135
+ ? (resolvedConfig.allowedPaths || [])
136
+ : (config.filesystem?.allowedPaths || []);
137
+ if (userAllowed.length > 0) {
138
+ const inAllowed = userAllowed.some((dir) => {
139
+ const dirResolved = resolve(dir);
140
+ return resolved === dirResolved || resolved.startsWith(dirResolved + sep) || resolved.startsWith(dirResolved + "/");
141
+ });
142
+
143
+ if (!inAllowed) {
144
+ this._block(operation, resolved, "outside-allowed-paths");
145
+ const hint = resolvedConfig?.allowedPaths?.length
146
+ ? `Your workspace: ${userAllowed.join(", ")}.`
147
+ : `Allowed: ${userAllowed.join(", ")}. Add more directories to ALLOWED_PATHS in .env, or clear it to allow global access.`;
148
+ return {
149
+ allowed: false,
150
+ reason: `Access denied: "${filePath}" is outside the allowed directories. ${hint}`,
151
+ };
152
+ }
153
+ }
154
+
155
+ return { allowed: true };
156
+ }
157
+
158
+ _block(operation, path, reason) {
159
+ this.blockedCount++;
160
+ eventBus.emitEvent("filesystem:blocked", { operation, path, reason });
161
+ }
162
+
163
+ stats() {
164
+ return { blockedCount: this.blockedCount };
165
+ }
166
+ }
167
+
168
+ const filesystemGuard = new FilesystemGuard();
169
+ export default filesystemGuard;