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,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;
|