daemora 1.0.4 → 1.0.6
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/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +29 -26
- package/config/mcp.json +126 -66
- package/daemora-ui/README.md +11 -0
- package/package.json +12 -2
- package/skills/api-development.md +35 -0
- package/skills/artifacts-builder/SKILL.md +74 -0
- package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/brand-guidelines.md +73 -0
- package/skills/browser.md +77 -0
- package/skills/changelog-generator.md +104 -0
- package/skills/coding.md +26 -10
- package/skills/content-research-writer.md +538 -0
- package/skills/data-analysis.md +27 -0
- package/skills/debugging.md +33 -0
- package/skills/devops.md +37 -0
- package/skills/document-docx.md +197 -0
- package/skills/document-pdf.md +294 -0
- package/skills/document-pptx.md +484 -0
- package/skills/document-xlsx.md +289 -0
- package/skills/domain-name-brainstormer.md +212 -0
- package/skills/file-organizer.md +433 -0
- package/skills/frontend-design.md +42 -0
- package/skills/image-enhancer.md +99 -0
- package/skills/invoice-organizer.md +446 -0
- package/skills/lead-research-assistant.md +199 -0
- package/skills/mcp-builder/SKILL.md +328 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/meeting-insights-analyzer.md +327 -0
- package/skills/orchestration.md +93 -0
- package/skills/raffle-winner-picker.md +159 -0
- package/skills/slack-gif-creator/SKILL.md +646 -0
- package/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/skills/slack-gif-creator/core/easing.py +230 -0
- package/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/skills/slack-gif-creator/core/typography.py +357 -0
- package/skills/slack-gif-creator/core/validators.py +264 -0
- package/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/skills/slack-gif-creator/requirements.txt +4 -0
- package/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/skills/slack-gif-creator/templates/explode.py +331 -0
- package/skills/slack-gif-creator/templates/fade.py +329 -0
- package/skills/slack-gif-creator/templates/flip.py +291 -0
- package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/skills/slack-gif-creator/templates/morph.py +329 -0
- package/skills/slack-gif-creator/templates/move.py +293 -0
- package/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/skills/slack-gif-creator/templates/shake.py +127 -0
- package/skills/slack-gif-creator/templates/slide.py +291 -0
- package/skills/slack-gif-creator/templates/spin.py +269 -0
- package/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/skills/system-admin.md +44 -0
- package/skills/tailored-resume-generator.md +345 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/video-downloader.md +99 -0
- package/skills/web-development.md +32 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/src/agents/SubAgentManager.js +134 -16
- package/src/agents/systemPrompt.js +427 -0
- package/src/api/openai-compat.js +212 -0
- package/src/channels/TelegramChannel.js +5 -2
- package/src/channels/index.js +7 -10
- package/src/cli.js +281 -55
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +15 -1
- package/src/config/models.js +314 -78
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +111 -11
- package/src/core/MessageQueue.js +90 -0
- package/src/core/Task.js +13 -0
- package/src/core/TaskQueue.js +1 -1
- package/src/core/TaskRunner.js +81 -6
- package/src/index.js +725 -59
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +74 -4
- package/src/safety/DockerSandbox.js +212 -0
- package/src/safety/ExecApproval.js +118 -0
- package/src/scheduler/Heartbeat.js +56 -21
- package/src/services/cleanup.js +106 -0
- package/src/services/sessions.js +39 -1
- package/src/setup/wizard.js +125 -75
- package/src/skills/SkillLoader.js +132 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +7 -1
- package/src/tools/manageAgents.js +55 -4
- package/src/tools/replyWithFile.js +62 -0
- package/src/tools/screenCapture.js +12 -1
- package/src/tools/taskManager.js +164 -0
- package/src/tools/useMCP.js +3 -1
- package/src/utils/Embeddings.js +236 -12
- package/src/webhooks/WebhookHandler.js +107 -0
- package/src/systemPrompt.js +0 -528
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSubAgent } from "../agents/SubAgentManager.js";
|
|
2
2
|
import mcpManager from "./MCPManager.js";
|
|
3
|
+
import { createSession, getSession, setMessages } from "../services/sessions.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* MCP Agent Runner - spawns specialist sub-agents for individual MCP servers.
|
|
@@ -69,6 +70,8 @@ All MCP tool params must be passed as a single JSON string (the first and only a
|
|
|
69
70
|
* @returns {Promise<string>} - Agent's final response
|
|
70
71
|
*/
|
|
71
72
|
export async function runMCPAgent(serverName, taskDescription, options = {}) {
|
|
73
|
+
const { mainSessionId, ...restOptions } = options;
|
|
74
|
+
|
|
72
75
|
// Get only this server's tool functions
|
|
73
76
|
const serverTools = mcpManager.getServerTools(serverName);
|
|
74
77
|
|
|
@@ -80,33 +83,67 @@ export async function runMCPAgent(serverName, taskDescription, options = {}) {
|
|
|
80
83
|
return `MCP server "${serverName}" not found or has no tools. Available servers: ${available.join(", ")}`;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
// Build tool docs for this server's system prompt
|
|
86
|
+
// Build tool docs for this server's system prompt (include nested schemas)
|
|
84
87
|
const toolDocs = Object.keys(serverTools)
|
|
85
88
|
.map((fullName) => {
|
|
86
89
|
const entry = mcpManager.toolMap.get(fullName);
|
|
87
90
|
if (!entry) return `### ${fullName}(argsJson: string)`;
|
|
88
|
-
const schema = entry.inputSchema?.properties || {};
|
|
89
|
-
const required = entry.inputSchema?.required || [];
|
|
90
|
-
const params = Object.entries(schema)
|
|
91
|
-
.map(([k, v]) => `${k}${required.includes(k) ? "" : "?"}: ${v.type || "any"}`)
|
|
92
|
-
.join(", ");
|
|
93
91
|
const desc = entry.description || entry.toolName;
|
|
94
|
-
const
|
|
95
|
-
|
|
92
|
+
const schema = entry.inputSchema;
|
|
93
|
+
// Show full JSON schema so the sub-agent knows exact field names
|
|
94
|
+
let schemaDoc = "";
|
|
95
|
+
if (schema) {
|
|
96
|
+
try {
|
|
97
|
+
schemaDoc = "\n- Schema:\n```json\n" + JSON.stringify(schema, null, 2) + "\n```";
|
|
98
|
+
} catch {
|
|
99
|
+
const props = schema.properties || {};
|
|
100
|
+
const required = schema.required || [];
|
|
101
|
+
const params = Object.entries(props)
|
|
102
|
+
.map(([k, v]) => `${k}${required.includes(k) ? "" : "?"}: ${v.type || "any"}`)
|
|
103
|
+
.join(", ");
|
|
104
|
+
schemaDoc = params ? `\n- argsJson: \`{${params}}\`` : "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return `### ${fullName}(argsJson: string)\n${desc}${schemaDoc}`;
|
|
96
108
|
})
|
|
97
109
|
.join("\n\n");
|
|
98
110
|
|
|
99
111
|
const systemPromptOverride = buildMCPAgentSystemPrompt(serverName, toolDocs);
|
|
100
112
|
|
|
113
|
+
// Load sub-agent session history (persistent across calls)
|
|
114
|
+
const subSessionId = mainSessionId ? `${mainSessionId}--${serverName}` : null;
|
|
115
|
+
let historyMessages = [];
|
|
116
|
+
if (subSessionId) {
|
|
117
|
+
const subSession = getSession(subSessionId);
|
|
118
|
+
if (subSession && subSession.messages.length > 0) {
|
|
119
|
+
historyMessages = subSession.messages.map(m => ({ role: m.role, content: m.content }));
|
|
120
|
+
console.log(`[MCPAgentRunner] Loaded ${historyMessages.length} history messages for "${serverName}"`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
console.log(
|
|
102
125
|
`[MCPAgentRunner] Spawning specialist for "${serverName}" (${Object.keys(serverTools).length} tools)`
|
|
103
126
|
);
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
...
|
|
128
|
+
const fullResult = await spawnSubAgent(taskDescription, {
|
|
129
|
+
...restOptions,
|
|
107
130
|
toolOverride: serverTools,
|
|
108
131
|
systemPromptOverride,
|
|
109
|
-
// MCP agents are always depth 1 - they don't spawn further sub-agents
|
|
110
132
|
depth: 1,
|
|
133
|
+
historyMessages,
|
|
134
|
+
returnFullResult: true,
|
|
111
135
|
});
|
|
136
|
+
|
|
137
|
+
// Save sub-agent session (cap at 100 messages)
|
|
138
|
+
if (subSessionId && fullResult.messages) {
|
|
139
|
+
let subSession = getSession(subSessionId);
|
|
140
|
+
if (!subSession) subSession = createSession(subSessionId);
|
|
141
|
+
const capped = fullResult.messages.length > 100
|
|
142
|
+
? fullResult.messages.slice(-100)
|
|
143
|
+
: fullResult.messages;
|
|
144
|
+
setMessages(subSessionId, capped);
|
|
145
|
+
console.log(`[MCPAgentRunner] Saved ${capped.length} messages to sub-session "${subSessionId}"`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return typeof fullResult === "string" ? fullResult : fullResult.text;
|
|
112
149
|
}
|
package/src/mcp/MCPManager.js
CHANGED
|
@@ -192,9 +192,47 @@ class MCPManager {
|
|
|
192
192
|
|
|
193
193
|
const servers = mcpConfig.mcpServers || {};
|
|
194
194
|
|
|
195
|
-
// Filter to only real, enabled servers
|
|
195
|
+
// Filter to only real, enabled servers with valid auth
|
|
196
196
|
const enabledServers = Object.entries(servers).filter(
|
|
197
|
-
([name, cfg]) =>
|
|
197
|
+
([name, cfg]) => {
|
|
198
|
+
if (name.startsWith("_comment") || typeof cfg !== "object" || cfg.enabled === false) return false;
|
|
199
|
+
|
|
200
|
+
// Validate auth: skip servers with placeholder or empty credentials
|
|
201
|
+
if (cfg.env) {
|
|
202
|
+
const hasPlaceholder = Object.entries(cfg.env).some(([, v]) =>
|
|
203
|
+
typeof v === "string" && (
|
|
204
|
+
v === "" ||
|
|
205
|
+
v.startsWith("YOUR_") ||
|
|
206
|
+
v === "your-token-here" ||
|
|
207
|
+
v === "your-key-here" ||
|
|
208
|
+
/^[A-Z_]+$/.test(v) // e.g. "GITHUB_TOKEN" as a value, not a reference
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
if (hasPlaceholder) {
|
|
212
|
+
console.log(`[MCPManager] Skipping "${name}" - env vars contain placeholder/empty values. Set real credentials.`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (cfg.headers) {
|
|
218
|
+
const expandedHeaders = Object.entries(cfg.headers).map(([k, v]) => {
|
|
219
|
+
if (typeof v === "string") {
|
|
220
|
+
return [k, v.replace(/\$\{([^}]+)\}/g, (_, envName) => process.env[envName] ?? "")];
|
|
221
|
+
}
|
|
222
|
+
return [k, v];
|
|
223
|
+
});
|
|
224
|
+
const hasEmpty = expandedHeaders.some(([, v]) => typeof v === "string" && v.trim() === "");
|
|
225
|
+
const hasPlaceholder = expandedHeaders.some(([, v]) =>
|
|
226
|
+
typeof v === "string" && (v.includes("YOUR_") || v === "Bearer " || v === "Bearer")
|
|
227
|
+
);
|
|
228
|
+
if (hasEmpty || hasPlaceholder) {
|
|
229
|
+
console.log(`[MCPManager] Skipping "${name}" - headers resolve to empty/placeholder values. Set env vars.`);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
198
236
|
);
|
|
199
237
|
|
|
200
238
|
if (enabledServers.length === 0) {
|
|
@@ -48,7 +48,28 @@ function getProvider(name, apiKeys = {}) {
|
|
|
48
48
|
* @returns {{ model: object, meta: object }} AI SDK model instance + metadata
|
|
49
49
|
*/
|
|
50
50
|
export function getModel(modelId, apiKeys = {}) {
|
|
51
|
-
|
|
51
|
+
let meta = models[modelId];
|
|
52
|
+
|
|
53
|
+
// Passthrough: if model isn't in the registry but follows "provider:model" format,
|
|
54
|
+
// create a dynamic entry so new models work without updating the registry.
|
|
55
|
+
if (!meta && modelId.includes(":")) {
|
|
56
|
+
const [providerName, modelName] = modelId.split(":", 2);
|
|
57
|
+
const knownProviders = ["openai", "anthropic", "google", "ollama"];
|
|
58
|
+
if (knownProviders.includes(providerName)) {
|
|
59
|
+
console.log(`[ModelRouter] Model "${modelId}" not in registry — using dynamic passthrough`);
|
|
60
|
+
meta = {
|
|
61
|
+
provider: providerName,
|
|
62
|
+
model: modelName,
|
|
63
|
+
contextWindow: 128_000,
|
|
64
|
+
compactAt: 90_000,
|
|
65
|
+
costPer1kInput: 0.001,
|
|
66
|
+
costPer1kOutput: 0.004,
|
|
67
|
+
capabilities: ["text", "tools"],
|
|
68
|
+
tier: "standard",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
if (!meta) {
|
|
53
74
|
throw new Error(`Unknown model: ${modelId}. Available: ${Object.keys(models).join(", ")}`);
|
|
54
75
|
}
|
|
@@ -138,6 +159,51 @@ export function listAvailableModels() {
|
|
|
138
159
|
return available;
|
|
139
160
|
}
|
|
140
161
|
|
|
162
|
+
// ── Thinking Level Resolution ──────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
const _thinkingAliases = { on: "low", min: "minimal", max: "high" };
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve thinking config for a model + level combination.
|
|
168
|
+
* Returns params to merge into the generateObject call, or null if no thinking.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} modelId - e.g. "anthropic:claude-sonnet-4-6"
|
|
171
|
+
* @param {string} level - "auto"|"off"|"minimal"|"low"|"medium"|"high"|"xhigh"
|
|
172
|
+
* @returns {{ thinkingParams: object } | null}
|
|
173
|
+
*/
|
|
174
|
+
export function resolveThinkingConfig(modelId, level = "auto") {
|
|
175
|
+
const normalized = _thinkingAliases[level] || level;
|
|
176
|
+
if (normalized === "off" || normalized === "auto") return null;
|
|
177
|
+
|
|
178
|
+
const provider = modelId.split(":")[0];
|
|
179
|
+
|
|
180
|
+
// Anthropic: thinking.budget_tokens
|
|
181
|
+
if (provider === "anthropic") {
|
|
182
|
+
const budgetMap = { minimal: 1024, low: 2048, medium: 4096, high: 8192, xhigh: 16384 };
|
|
183
|
+
const budget = budgetMap[normalized];
|
|
184
|
+
if (!budget) return null;
|
|
185
|
+
return { thinkingParams: { thinking: { type: "enabled", budgetTokens: budget } } };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// OpenAI: reasoning.effort (only for o-series models)
|
|
189
|
+
if (provider === "openai") {
|
|
190
|
+
const effortMap = { minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "high" };
|
|
191
|
+
const effort = effortMap[normalized];
|
|
192
|
+
if (!effort) return null;
|
|
193
|
+
return { thinkingParams: { reasoning: { effort } } };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Google: thinkingConfig.thinkingBudget
|
|
197
|
+
if (provider === "google") {
|
|
198
|
+
const budgetMap = { minimal: 1024, low: 2048, medium: 4096, high: 8192, xhigh: 16384 };
|
|
199
|
+
const budget = budgetMap[normalized];
|
|
200
|
+
if (!budget) return null;
|
|
201
|
+
return { thinkingParams: { thinkingConfig: { thinkingBudget: budget } } };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
141
207
|
// ── Task-Type Model Routing ────────────────────────────────────────────────────
|
|
142
208
|
|
|
143
209
|
const _profileEnvMap = {
|
|
@@ -152,11 +218,12 @@ const _profileEnvMap = {
|
|
|
152
218
|
* 1. explicitModel (caller override)
|
|
153
219
|
* 2. Per-tenant modelRoutes[profile]
|
|
154
220
|
* 3. Global CODE_MODEL / RESEARCH_MODEL / WRITER_MODEL / ANALYST_MODEL env vars
|
|
155
|
-
* 4.
|
|
156
|
-
* 5.
|
|
221
|
+
* 4. SUB_AGENT_MODEL (global sub-agent default)
|
|
222
|
+
* 5. Per-tenant general model override
|
|
223
|
+
* 6. DEFAULT_MODEL env var / hardcoded default
|
|
157
224
|
*
|
|
158
225
|
* @param {string|null} profile - e.g. "coder", "researcher", "writer", "analyst"
|
|
159
|
-
* @param {object} tenantConfig - resolvedConfig from TenantManager (may have .modelRoutes, .model)
|
|
226
|
+
* @param {object} tenantConfig - resolvedConfig from TenantManager (may have .modelRoutes, .model, .subAgentModel)
|
|
160
227
|
* @param {string|null} explicitModel - Caller-supplied model override (highest priority)
|
|
161
228
|
* @returns {string} Resolved model ID
|
|
162
229
|
*/
|
|
@@ -164,6 +231,9 @@ export function resolveModelForProfile(profile, tenantConfig = {}, explicitModel
|
|
|
164
231
|
if (explicitModel) return explicitModel;
|
|
165
232
|
if (profile && tenantConfig.modelRoutes?.[profile]) return tenantConfig.modelRoutes[profile];
|
|
166
233
|
if (profile && process.env[_profileEnvMap[profile]]) return process.env[_profileEnvMap[profile]];
|
|
234
|
+
// Sub-agent model: tenant-level > env-level
|
|
235
|
+
if (tenantConfig.subAgentModel) return tenantConfig.subAgentModel;
|
|
236
|
+
if (process.env.SUB_AGENT_MODEL) return process.env.SUB_AGENT_MODEL;
|
|
167
237
|
if (tenantConfig.model) return tenantConfig.model;
|
|
168
238
|
return process.env.DEFAULT_MODEL || "openai:gpt-4.1-mini";
|
|
169
239
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Sandbox — run commands inside Docker containers for kernel-level isolation.
|
|
3
|
+
*
|
|
4
|
+
* Config:
|
|
5
|
+
* SANDBOX_MODE=docker — enable Docker isolation
|
|
6
|
+
* DOCKER_IMAGE=node:22-slim — base image
|
|
7
|
+
* DOCKER_MEMORY=512m — memory limit
|
|
8
|
+
* DOCKER_CPUS=0.5 — CPU limit
|
|
9
|
+
* DOCKER_NETWORK=none — network mode (none = no network)
|
|
10
|
+
* DOCKER_SCOPE=session — "session" (per session) | "shared" (one for all)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
14
|
+
import { config } from "../config/default.js";
|
|
15
|
+
|
|
16
|
+
// containerId → { scope, createdAt, lastUsedAt }
|
|
17
|
+
const _containers = new Map();
|
|
18
|
+
const CLEANUP_AFTER = 10 * 60 * 1000; // 10 min inactivity
|
|
19
|
+
|
|
20
|
+
class DockerSandbox {
|
|
21
|
+
constructor() {
|
|
22
|
+
this._available = null; // cached Docker availability check
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if Docker is available.
|
|
27
|
+
*/
|
|
28
|
+
isAvailable() {
|
|
29
|
+
if (this._available !== null) return this._available;
|
|
30
|
+
try {
|
|
31
|
+
execSync("docker info", { stdio: "pipe", timeout: 5000 });
|
|
32
|
+
this._available = true;
|
|
33
|
+
} catch {
|
|
34
|
+
this._available = false;
|
|
35
|
+
}
|
|
36
|
+
return this._available;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure a container exists for the given scope.
|
|
41
|
+
* @param {string} scopeId — session ID or "shared"
|
|
42
|
+
* @returns {string} containerId
|
|
43
|
+
*/
|
|
44
|
+
ensureContainer(scopeId = "shared") {
|
|
45
|
+
const existing = _containers.get(scopeId);
|
|
46
|
+
if (existing) {
|
|
47
|
+
// Check if container is still running
|
|
48
|
+
try {
|
|
49
|
+
const status = execSync(`docker inspect -f '{{.State.Running}}' ${existing.containerId}`, {
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
}).trim();
|
|
54
|
+
if (status === "true") {
|
|
55
|
+
existing.lastUsedAt = Date.now();
|
|
56
|
+
return existing.containerId;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Container gone — remove from map and create new
|
|
60
|
+
_containers.delete(scopeId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sandbox = config.sandbox || {};
|
|
65
|
+
const image = sandbox.dockerImage || "node:22-slim";
|
|
66
|
+
const memory = sandbox.dockerMemory || "512m";
|
|
67
|
+
const cpus = sandbox.dockerCpus || "0.5";
|
|
68
|
+
const network = sandbox.dockerNetwork || "none";
|
|
69
|
+
|
|
70
|
+
// Create container with security constraints
|
|
71
|
+
const containerName = `daemora-sandbox-${scopeId.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 30)}-${Date.now()}`;
|
|
72
|
+
|
|
73
|
+
const args = [
|
|
74
|
+
"docker", "run", "-d",
|
|
75
|
+
"--name", containerName,
|
|
76
|
+
"--memory", memory,
|
|
77
|
+
"--cpus", cpus,
|
|
78
|
+
"--network", network,
|
|
79
|
+
"--read-only",
|
|
80
|
+
"--cap-drop", "ALL",
|
|
81
|
+
"--tmpfs", "/tmp:rw,noexec,nosuid,size=100m",
|
|
82
|
+
"--tmpfs", "/workspace:rw,size=500m",
|
|
83
|
+
"-w", "/workspace",
|
|
84
|
+
image,
|
|
85
|
+
"tail", "-f", "/dev/null", // Keep container alive
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const containerId = execSync(args.join(" "), {
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
timeout: 30000,
|
|
92
|
+
}).trim();
|
|
93
|
+
|
|
94
|
+
_containers.set(scopeId, {
|
|
95
|
+
containerId,
|
|
96
|
+
containerName,
|
|
97
|
+
createdAt: Date.now(),
|
|
98
|
+
lastUsedAt: Date.now(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log(`[DockerSandbox] Created container ${containerName} (${containerId.slice(0, 12)}) for scope: ${scopeId}`);
|
|
102
|
+
return containerId;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new Error(`Failed to create Docker container: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Execute a command inside a container.
|
|
110
|
+
* @param {string} scopeId
|
|
111
|
+
* @param {string} command
|
|
112
|
+
* @param {object} opts — { timeout, cwd }
|
|
113
|
+
* @returns {string} command output
|
|
114
|
+
*/
|
|
115
|
+
exec(scopeId, command, opts = {}) {
|
|
116
|
+
const containerId = this.ensureContainer(scopeId);
|
|
117
|
+
const timeout = opts.timeout || 120_000;
|
|
118
|
+
const cwd = opts.cwd || "/workspace";
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = execSync(`docker exec -w "${cwd}" ${containerId} sh -c ${JSON.stringify(command)}`, {
|
|
122
|
+
encoding: "utf-8",
|
|
123
|
+
timeout,
|
|
124
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
125
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const entry = _containers.get(scopeId);
|
|
129
|
+
if (entry) entry.lastUsedAt = Date.now();
|
|
130
|
+
|
|
131
|
+
return result || "(command completed with no output)";
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error.killed) {
|
|
134
|
+
return `Command timed out after ${timeout / 1000}s inside Docker container.`;
|
|
135
|
+
}
|
|
136
|
+
const parts = [];
|
|
137
|
+
if (error.stdout) parts.push(`stdout:\n${error.stdout.slice(0, 2000)}`);
|
|
138
|
+
if (error.stderr) parts.push(`stderr:\n${error.stderr.slice(0, 2000)}`);
|
|
139
|
+
const exitMsg = error.status !== undefined ? ` (exit code: ${error.status})` : "";
|
|
140
|
+
if (parts.length > 0) return `Command failed${exitMsg}:\n${parts.join("\n---\n")}`;
|
|
141
|
+
return `Command failed${exitMsg}: ${error.message}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Copy workspace files into the container.
|
|
147
|
+
*/
|
|
148
|
+
copyToContainer(scopeId, hostPath, containerPath = "/workspace") {
|
|
149
|
+
const containerId = this.ensureContainer(scopeId);
|
|
150
|
+
try {
|
|
151
|
+
execSync(`docker cp "${hostPath}/." ${containerId}:${containerPath}`, {
|
|
152
|
+
timeout: 30000,
|
|
153
|
+
stdio: "pipe",
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.log(`[DockerSandbox] Copy failed: ${error.message}`);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Remove a container.
|
|
164
|
+
*/
|
|
165
|
+
removeContainer(scopeId) {
|
|
166
|
+
const entry = _containers.get(scopeId);
|
|
167
|
+
if (!entry) return;
|
|
168
|
+
try {
|
|
169
|
+
execSync(`docker rm -f ${entry.containerId}`, { stdio: "pipe", timeout: 10000 });
|
|
170
|
+
console.log(`[DockerSandbox] Removed container for scope: ${scopeId}`);
|
|
171
|
+
} catch {}
|
|
172
|
+
_containers.delete(scopeId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Cleanup inactive containers.
|
|
177
|
+
*/
|
|
178
|
+
cleanup() {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
for (const [scopeId, entry] of _containers.entries()) {
|
|
181
|
+
if (now - entry.lastUsedAt > CLEANUP_AFTER) {
|
|
182
|
+
this.removeContainer(scopeId);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Remove all containers.
|
|
189
|
+
*/
|
|
190
|
+
removeAll() {
|
|
191
|
+
for (const scopeId of [..._containers.keys()]) {
|
|
192
|
+
this.removeContainer(scopeId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* List active containers.
|
|
198
|
+
*/
|
|
199
|
+
list() {
|
|
200
|
+
return [..._containers.entries()].map(([scopeId, entry]) => ({
|
|
201
|
+
scopeId,
|
|
202
|
+
containerId: entry.containerId.slice(0, 12),
|
|
203
|
+
containerName: entry.containerName,
|
|
204
|
+
createdAt: new Date(entry.createdAt).toISOString(),
|
|
205
|
+
lastUsedAt: new Date(entry.lastUsedAt).toISOString(),
|
|
206
|
+
idleMs: Date.now() - entry.lastUsedAt,
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const dockerSandbox = new DockerSandbox();
|
|
212
|
+
export default dockerSandbox;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exec Approval Manager — interactive approval gates for dangerous commands.
|
|
3
|
+
*
|
|
4
|
+
* When approval mode is enabled and a command matches dangerous patterns,
|
|
5
|
+
* the agent loop pauses and waits for user approval via the API.
|
|
6
|
+
*
|
|
7
|
+
* Config: approval.mode = "off" | "dangerous-only" | "all"
|
|
8
|
+
* API:
|
|
9
|
+
* GET /api/approvals — list pending approvals
|
|
10
|
+
* POST /api/approvals/:id — approve/deny { decision: "allow" | "allow-once" | "deny" }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { v4 as uuidv4 } from "uuid";
|
|
14
|
+
|
|
15
|
+
// Dangerous command patterns
|
|
16
|
+
const DANGEROUS_PATTERNS = [
|
|
17
|
+
/\brm\s+(-rf?|--recursive)\b/i,
|
|
18
|
+
/\bsudo\b/,
|
|
19
|
+
/\bdrop\s+(table|database)\b/i,
|
|
20
|
+
/\bkill\s+-9\b/,
|
|
21
|
+
/\bshutdown\b/,
|
|
22
|
+
/\breboot\b/,
|
|
23
|
+
/\bmkfs\b/,
|
|
24
|
+
/\bdd\s+if=/,
|
|
25
|
+
/\bcurl\b.*\|\s*(sh|bash|zsh)\b/,
|
|
26
|
+
/\bwget\b.*\|\s*(sh|bash|zsh)\b/,
|
|
27
|
+
/\bnpm\s+publish\b/,
|
|
28
|
+
/\bgit\s+push\s+.*--force\b/,
|
|
29
|
+
/\bgit\s+reset\s+--hard\b/,
|
|
30
|
+
/\bdocker\s+rm\b/,
|
|
31
|
+
/\bdocker\s+system\s+prune\b/,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
class ExecApprovalManager {
|
|
35
|
+
constructor() {
|
|
36
|
+
// approvalId → { command, taskId, createdAt, resolve, timer }
|
|
37
|
+
this._pending = new Map();
|
|
38
|
+
this._timeoutMs = 60_000; // 60 seconds default
|
|
39
|
+
this._mode = process.env.APPROVAL_MODE || "off"; // "off" | "dangerous-only" | "all"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get mode() {
|
|
43
|
+
return this._mode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a command needs approval.
|
|
48
|
+
*/
|
|
49
|
+
needsApproval(command) {
|
|
50
|
+
if (this._mode === "off") return false;
|
|
51
|
+
if (this._mode === "all") return true;
|
|
52
|
+
// "dangerous-only" — check against patterns
|
|
53
|
+
return DANGEROUS_PATTERNS.some(p => p.test(command));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Request approval for a command. Returns a Promise that resolves with the decision.
|
|
58
|
+
* @param {string} command
|
|
59
|
+
* @param {string} taskId
|
|
60
|
+
* @returns {Promise<"allow"|"deny">}
|
|
61
|
+
*/
|
|
62
|
+
requestApproval(command, taskId) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const approvalId = uuidv4().slice(0, 12);
|
|
65
|
+
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
this._pending.delete(approvalId);
|
|
68
|
+
console.log(`[ExecApproval] Timeout for ${approvalId} — denying`);
|
|
69
|
+
resolve("deny");
|
|
70
|
+
}, this._timeoutMs);
|
|
71
|
+
|
|
72
|
+
this._pending.set(approvalId, {
|
|
73
|
+
command,
|
|
74
|
+
taskId,
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
resolve,
|
|
77
|
+
timer,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(`[ExecApproval] Waiting for approval ${approvalId}: "${command.slice(0, 80)}"`);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a pending approval.
|
|
86
|
+
* @param {string} approvalId
|
|
87
|
+
* @param {"allow"|"allow-once"|"deny"} decision
|
|
88
|
+
* @returns {boolean} true if found and resolved
|
|
89
|
+
*/
|
|
90
|
+
resolveApproval(approvalId, decision) {
|
|
91
|
+
const entry = this._pending.get(approvalId);
|
|
92
|
+
if (!entry) return false;
|
|
93
|
+
|
|
94
|
+
clearTimeout(entry.timer);
|
|
95
|
+
this._pending.delete(approvalId);
|
|
96
|
+
|
|
97
|
+
const effective = decision === "allow-once" ? "allow" : decision;
|
|
98
|
+
console.log(`[ExecApproval] ${approvalId} → ${decision}`);
|
|
99
|
+
entry.resolve(effective);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List all pending approvals (for API).
|
|
105
|
+
*/
|
|
106
|
+
listPending() {
|
|
107
|
+
return [...this._pending.entries()].map(([id, entry]) => ({
|
|
108
|
+
id,
|
|
109
|
+
command: entry.command,
|
|
110
|
+
taskId: entry.taskId,
|
|
111
|
+
createdAt: entry.createdAt,
|
|
112
|
+
expiresIn: Math.max(0, this._timeoutMs - (Date.now() - new Date(entry.createdAt).getTime())),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const execApproval = new ExecApprovalManager();
|
|
118
|
+
export default execApproval;
|