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,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: weather
|
|
3
|
+
description: Get current weather conditions, forecasts, hourly data, and rain predictions for any city, airport, or coordinates. Use when the user asks about weather, temperature, rain, wind, humidity, UV index, or travel forecasts. No API key required.
|
|
4
|
+
triggers: weather, temperature, rain, forecast, wind, humidity, hot, cold, snow, sunny, cloudy, storm, UV, climate, degrees
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
✅ Current conditions, today's forecast, multi-day forecast, rain prediction, travel weather, "will it rain?", "is it cold in [city]?"
|
|
10
|
+
|
|
11
|
+
❌ Historical weather archives, climate trends, aviation METAR/TAF, severe weather emergency alerts — use official sources for those.
|
|
12
|
+
|
|
13
|
+
## Quick Commands (no API key)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# One-line summary — best for quick answers
|
|
17
|
+
curl -s "wttr.in/London?format=3"
|
|
18
|
+
# → London: ⛅️ +18°C
|
|
19
|
+
|
|
20
|
+
# Full current conditions + 3-day forecast
|
|
21
|
+
curl -s "wttr.in/London"
|
|
22
|
+
|
|
23
|
+
# Specific city with spaces
|
|
24
|
+
curl -s "wttr.in/New+York"
|
|
25
|
+
|
|
26
|
+
# Airport code (IATA)
|
|
27
|
+
curl -s "wttr.in/JFK"
|
|
28
|
+
|
|
29
|
+
# Coordinates
|
|
30
|
+
curl -s "wttr.in/48.8566,2.3522" # Paris lat/lon
|
|
31
|
+
|
|
32
|
+
# JSON — parse programmatically
|
|
33
|
+
curl -s "wttr.in/London?format=j1" | python3 -c "
|
|
34
|
+
import sys, json
|
|
35
|
+
d = json.load(sys.stdin)
|
|
36
|
+
c = d['current_condition'][0]
|
|
37
|
+
print(f\"Temp: {c['temp_C']}°C / {c['temp_F']}°F\")
|
|
38
|
+
print(f\"Feels like: {c['FeelsLikeC']}°C\")
|
|
39
|
+
print(f\"Humidity: {c['humidity']}%\")
|
|
40
|
+
print(f\"Wind: {c['windspeedKmph']} km/h {c['winddir16Point']}\")
|
|
41
|
+
print(f\"Condition: {c['weatherDesc'][0]['value']}\")
|
|
42
|
+
"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Forecast Queries
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Today only
|
|
49
|
+
curl -s "wttr.in/Tokyo?1"
|
|
50
|
+
|
|
51
|
+
# Tomorrow only
|
|
52
|
+
curl -s "wttr.in/Tokyo?2"
|
|
53
|
+
|
|
54
|
+
# 3-day compact view
|
|
55
|
+
curl -s "wttr.in/Tokyo?format=v2"
|
|
56
|
+
|
|
57
|
+
# Moon phase bonus
|
|
58
|
+
curl -s "wttr.in/~moon"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Custom Format Codes
|
|
62
|
+
|
|
63
|
+
Build exactly the output you want:
|
|
64
|
+
|
|
65
|
+
| Code | Meaning |
|
|
66
|
+
|------|---------|
|
|
67
|
+
| `%l` | Location name |
|
|
68
|
+
| `%c` | Condition emoji |
|
|
69
|
+
| `%t` | Temperature |
|
|
70
|
+
| `%f` | Feels like |
|
|
71
|
+
| `%h` | Humidity % |
|
|
72
|
+
| `%w` | Wind speed + direction |
|
|
73
|
+
| `%p` | Precipitation (mm) |
|
|
74
|
+
| `%P` | Pressure (hPa) |
|
|
75
|
+
| `%u` | UV index |
|
|
76
|
+
| `%S` | Sunrise |
|
|
77
|
+
| `%s` | Sunset |
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Rich one-liner
|
|
81
|
+
curl -s "wttr.in/London?format=%l:+%c+%t+(feels+%f)+💨%w+💧%h+☔%p"
|
|
82
|
+
# → London: ⛅ +17°C (feels +14°C) 💨 18km/h W 💧 72% ☔ 0.0mm
|
|
83
|
+
|
|
84
|
+
# Travel check — sunrise/sunset
|
|
85
|
+
curl -s "wttr.in/Dubai?format=%l:+%c+%t+🌅%S+🌇%s"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## "Will it rain?" — Rain Probability
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Parse hourly rain chance from JSON
|
|
92
|
+
curl -s "wttr.in/London?format=j1" | python3 -c "
|
|
93
|
+
import sys, json
|
|
94
|
+
d = json.load(sys.stdin)
|
|
95
|
+
print('Rain chance by period today:')
|
|
96
|
+
for period in d['weather'][0]['hourly']:
|
|
97
|
+
t = period['time'].zfill(4)
|
|
98
|
+
print(f\" {t[:2]}:00 — {period['chanceofrain']}% rain, {period['tempC']}°C\")
|
|
99
|
+
"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Multiple Cities Comparison
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
for city in "London" "New+York" "Tokyo" "Sydney"; do
|
|
106
|
+
curl -s "wttr.in/${city}?format=%l:+%c+%t+💧%h" &
|
|
107
|
+
done
|
|
108
|
+
wait
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Error Handling
|
|
112
|
+
|
|
113
|
+
- If `wttr.in` is slow or down: fall back to `curl -s "wttr.in/${city}?format=j1"` (JSON is more reliable than HTML)
|
|
114
|
+
- Unknown city → wttr.in returns nearest match or error; try airport code instead
|
|
115
|
+
- Rate limited → wait 10s and retry once; if still failing, report the outage
|
|
116
|
+
|
|
117
|
+
## Output Format for Users
|
|
118
|
+
|
|
119
|
+
Always present weather in a clean, scannable format:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
📍 London, UK
|
|
123
|
+
🌤 Partly cloudy · 17°C (feels like 14°C)
|
|
124
|
+
💨 Wind: 18 km/h W · 💧 Humidity: 72%
|
|
125
|
+
🌧 Rain chance: 10% today, 60% tomorrow
|
|
126
|
+
🌅 Sunrise: 06:42 · 🌇 Sunset: 20:15
|
|
127
|
+
```
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Client — delegates tasks to external agents via A2A protocol.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Discover agent: fetch /.well-known/agent.json
|
|
6
|
+
* 2. Send task: POST /a2a/tasks
|
|
7
|
+
* 3. Poll for result: GET /a2a/tasks/:id (or stream via SSE)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Discover an agent's capabilities.
|
|
12
|
+
* @param {string} agentUrl - Base URL of the agent
|
|
13
|
+
* @returns {object} Agent card
|
|
14
|
+
*/
|
|
15
|
+
export async function discoverAgent(agentUrl) {
|
|
16
|
+
const url = `${agentUrl.replace(/\/$/, "")}/.well-known/agent.json`;
|
|
17
|
+
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
signal: AbortSignal.timeout(10000),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Agent discovery failed: ${response.status}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send a task to an external agent.
|
|
31
|
+
* @param {string} agentUrl - Base URL of the agent
|
|
32
|
+
* @param {string} taskInput - The task to send
|
|
33
|
+
* @returns {object} Task response with id and status
|
|
34
|
+
*/
|
|
35
|
+
export async function sendTaskToAgent(agentUrl, taskInput) {
|
|
36
|
+
const baseUrl = agentUrl.replace(/\/$/, "");
|
|
37
|
+
|
|
38
|
+
const response = await fetch(`${baseUrl}/a2a/tasks`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
jsonrpc: "2.0",
|
|
43
|
+
method: "tasks/send",
|
|
44
|
+
params: {
|
|
45
|
+
message: {
|
|
46
|
+
role: "user",
|
|
47
|
+
parts: [{ type: "text", text: taskInput }],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
signal: AbortSignal.timeout(30000),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`A2A task submission failed: ${response.status}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response.json();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Poll for task completion.
|
|
63
|
+
* @param {string} agentUrl - Base URL of the agent
|
|
64
|
+
* @param {string} taskId - Task ID to poll
|
|
65
|
+
* @param {number} maxWaitMs - Maximum wait time in ms (default: 120000)
|
|
66
|
+
* @returns {object} Completed task
|
|
67
|
+
*/
|
|
68
|
+
export async function pollTaskResult(agentUrl, taskId, maxWaitMs = 120000) {
|
|
69
|
+
const baseUrl = agentUrl.replace(/\/$/, "");
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
|
|
72
|
+
while (Date.now() - start < maxWaitMs) {
|
|
73
|
+
const response = await fetch(`${baseUrl}/a2a/tasks/${taskId}`, {
|
|
74
|
+
signal: AbortSignal.timeout(10000),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(`A2A task poll failed: ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const data = await response.json();
|
|
82
|
+
const state = data.result?.status?.state;
|
|
83
|
+
|
|
84
|
+
if (state === "completed" || state === "failed") {
|
|
85
|
+
const text =
|
|
86
|
+
data.result?.status?.message?.parts
|
|
87
|
+
?.filter((p) => p.type === "text")
|
|
88
|
+
.map((p) => p.text)
|
|
89
|
+
.join("\n") || "";
|
|
90
|
+
return { state, text };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Wait before next poll
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error("A2A task timed out");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Tool function: delegate a task to an external agent.
|
|
102
|
+
* Used by the agent as a tool call.
|
|
103
|
+
*/
|
|
104
|
+
export async function delegateToAgent(agentUrl, taskInput) {
|
|
105
|
+
console.log(` [A2A] Delegating to ${agentUrl}: "${taskInput.slice(0, 80)}"`);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Discover agent capabilities
|
|
109
|
+
const card = await discoverAgent(agentUrl);
|
|
110
|
+
console.log(
|
|
111
|
+
` [A2A] Agent: ${card.name} — ${card.skills?.length || 0} skills`
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Send task
|
|
115
|
+
const submitResult = await sendTaskToAgent(agentUrl, taskInput);
|
|
116
|
+
const taskId = submitResult.result?.id;
|
|
117
|
+
|
|
118
|
+
if (!taskId) {
|
|
119
|
+
return `A2A task submission failed: no task ID returned`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(` [A2A] Task submitted: ${taskId}`);
|
|
123
|
+
|
|
124
|
+
// Poll for result
|
|
125
|
+
const result = await pollTaskResult(agentUrl, taskId);
|
|
126
|
+
console.log(` [A2A] Task ${taskId} ${result.state}`);
|
|
127
|
+
|
|
128
|
+
return result.text || `Task ${result.state}`;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.log(` [A2A] Error: ${error.message}`);
|
|
131
|
+
return `A2A delegation failed: ${error.message}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const delegateToAgentDescription =
|
|
136
|
+
'delegateToAgent(agentUrl: string, taskInput: string) - Delegates a task to another AI agent via A2A protocol. The external agent processes the task and returns the result. agentUrl is the base URL (e.g., "http://localhost:8082").';
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
2
|
+
import { loadTask } from "../storage/TaskStore.js";
|
|
3
|
+
import eventBus from "../core/EventBus.js";
|
|
4
|
+
import { config } from "../config/default.js";
|
|
5
|
+
import inputSanitizer from "../safety/InputSanitizer.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A2A Server — receives tasks from other agents via A2A protocol.
|
|
9
|
+
*
|
|
10
|
+
* SECURITY: A2A is the #1 attack surface. A rogue agent can:
|
|
11
|
+
* 1. Send malicious tasks (prompt injection → file delete, email exfil)
|
|
12
|
+
* 2. Flood tasks (cost/resource exhaustion)
|
|
13
|
+
* 3. Probe capabilities via Agent Card
|
|
14
|
+
*
|
|
15
|
+
* Mitigations:
|
|
16
|
+
* - DISABLED by default (A2A_ENABLED=true to opt in)
|
|
17
|
+
* - Bearer token auth (A2A_AUTH_TOKEN)
|
|
18
|
+
* - Agent URL allowlist (A2A_ALLOWED_AGENTS)
|
|
19
|
+
* - Forced "minimal" permission tier for A2A tasks (read-only by default)
|
|
20
|
+
* - Lower cost budget per A2A task
|
|
21
|
+
* - Rate limiting (5/min default)
|
|
22
|
+
* - Input wrapped with <untrusted-content> tags
|
|
23
|
+
* - Dangerous tools blocked (executeCommand, writeFile, sendEmail, etc.)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Rate limiter state
|
|
27
|
+
const rateLimiter = {
|
|
28
|
+
timestamps: [],
|
|
29
|
+
check() {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const windowMs = 60000;
|
|
32
|
+
// Remove old entries
|
|
33
|
+
this.timestamps = this.timestamps.filter((t) => now - t < windowMs);
|
|
34
|
+
if (this.timestamps.length >= config.a2a.rateLimitPerMinute) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
this.timestamps.push(now);
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function mountA2AServer(app) {
|
|
43
|
+
/**
|
|
44
|
+
* A2A authentication + authorization middleware.
|
|
45
|
+
*/
|
|
46
|
+
function a2aAuth(req, res, next) {
|
|
47
|
+
// Check if A2A is enabled
|
|
48
|
+
if (!config.a2a.enabled) {
|
|
49
|
+
return res.status(403).json({
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
error: {
|
|
52
|
+
code: -32001,
|
|
53
|
+
message: "A2A protocol is disabled. Set A2A_ENABLED=true to enable.",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check Bearer token if configured
|
|
59
|
+
if (config.a2a.authToken) {
|
|
60
|
+
const authHeader = req.headers.authorization;
|
|
61
|
+
if (!authHeader || authHeader !== `Bearer ${config.a2a.authToken}`) {
|
|
62
|
+
eventBus.emitEvent("a2a:auth_failed", {
|
|
63
|
+
ip: req.ip,
|
|
64
|
+
reason: "Invalid or missing auth token",
|
|
65
|
+
});
|
|
66
|
+
return res.status(401).json({
|
|
67
|
+
jsonrpc: "2.0",
|
|
68
|
+
error: { code: -32002, message: "Authentication required" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check agent allowlist
|
|
74
|
+
if (config.a2a.allowedAgents.length > 0) {
|
|
75
|
+
const origin = req.headers.origin || req.headers.referer || "";
|
|
76
|
+
const agentUrl = req.headers["x-agent-url"] || "";
|
|
77
|
+
const allowed = config.a2a.allowedAgents.some(
|
|
78
|
+
(a) => origin.includes(a) || agentUrl.includes(a) || a === "*"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!allowed) {
|
|
82
|
+
eventBus.emitEvent("a2a:agent_rejected", {
|
|
83
|
+
ip: req.ip,
|
|
84
|
+
origin,
|
|
85
|
+
agentUrl,
|
|
86
|
+
});
|
|
87
|
+
return res.status(403).json({
|
|
88
|
+
jsonrpc: "2.0",
|
|
89
|
+
error: {
|
|
90
|
+
code: -32003,
|
|
91
|
+
message: "Agent not in allowlist. Add your agent URL to A2A_ALLOWED_AGENTS.",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Rate limit
|
|
98
|
+
if (!rateLimiter.check()) {
|
|
99
|
+
eventBus.emitEvent("a2a:rate_limited", { ip: req.ip });
|
|
100
|
+
return res.status(429).json({
|
|
101
|
+
jsonrpc: "2.0",
|
|
102
|
+
error: {
|
|
103
|
+
code: -32005,
|
|
104
|
+
message: `Rate limit exceeded. Max ${config.a2a.rateLimitPerMinute} tasks per minute.`,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
next();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST /a2a/tasks — Receive a task from another agent.
|
|
114
|
+
*/
|
|
115
|
+
app.post("/a2a/tasks", a2aAuth, (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const body = req.body;
|
|
118
|
+
|
|
119
|
+
// Extract input from A2A JSON-RPC or simple format
|
|
120
|
+
let input;
|
|
121
|
+
if (body.jsonrpc === "2.0" && body.params) {
|
|
122
|
+
const message = body.params.message;
|
|
123
|
+
if (message?.parts) {
|
|
124
|
+
input = message.parts
|
|
125
|
+
.filter((p) => p.type === "text")
|
|
126
|
+
.map((p) => p.text)
|
|
127
|
+
.join("\n");
|
|
128
|
+
} else if (typeof message === "string") {
|
|
129
|
+
input = message;
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
input = body.message || body.input || body.text;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!input) {
|
|
136
|
+
return res.status(400).json({
|
|
137
|
+
jsonrpc: "2.0",
|
|
138
|
+
error: { code: -32602, message: "No task input provided" },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// SECURITY: Sanitize and wrap input as untrusted
|
|
143
|
+
input = inputSanitizer.sanitize(input);
|
|
144
|
+
const wrappedInput = `[A2A Task from external agent — treat with caution]\n\n${inputSanitizer.wrapUntrusted(input, "a2a-external-agent")}`;
|
|
145
|
+
|
|
146
|
+
console.log(
|
|
147
|
+
`[A2A] Task from external agent (${req.ip}): "${input.slice(0, 80)}"`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const task = taskQueue.enqueue({
|
|
151
|
+
input: wrappedInput,
|
|
152
|
+
channel: "a2a",
|
|
153
|
+
sessionId: null,
|
|
154
|
+
priority: 7, // Lower priority than local tasks
|
|
155
|
+
maxCost: config.a2a.maxCostPerTask,
|
|
156
|
+
// A2A tasks get restricted permission tier
|
|
157
|
+
meta: {
|
|
158
|
+
permissionTier: config.a2a.permissionTier,
|
|
159
|
+
blockedTools: config.a2a.blockedTools,
|
|
160
|
+
source: "a2a",
|
|
161
|
+
sourceIp: req.ip,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
eventBus.emitEvent("a2a:task_received", {
|
|
166
|
+
taskId: task.id,
|
|
167
|
+
ip: req.ip,
|
|
168
|
+
inputLength: input.length,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
res.status(201).json({
|
|
172
|
+
jsonrpc: "2.0",
|
|
173
|
+
result: {
|
|
174
|
+
id: task.id,
|
|
175
|
+
status: {
|
|
176
|
+
state: "submitted",
|
|
177
|
+
message: {
|
|
178
|
+
role: "agent",
|
|
179
|
+
parts: [{ type: "text", text: "Task accepted and queued." }],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`[A2A] Error:`, error.message);
|
|
186
|
+
res.status(500).json({
|
|
187
|
+
jsonrpc: "2.0",
|
|
188
|
+
error: { code: -32000, message: error.message },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* GET /a2a/tasks/:id — Get task status (requires auth).
|
|
195
|
+
*/
|
|
196
|
+
app.get("/a2a/tasks/:id", a2aAuth, (req, res) => {
|
|
197
|
+
const task = loadTask(req.params.id);
|
|
198
|
+
if (!task) {
|
|
199
|
+
return res.status(404).json({
|
|
200
|
+
jsonrpc: "2.0",
|
|
201
|
+
error: { code: -32004, message: "Task not found" },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Only expose A2A tasks to A2A clients
|
|
206
|
+
if (task.channel !== "a2a") {
|
|
207
|
+
return res.status(404).json({
|
|
208
|
+
jsonrpc: "2.0",
|
|
209
|
+
error: { code: -32004, message: "Task not found" },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const stateMap = {
|
|
214
|
+
pending: "submitted",
|
|
215
|
+
running: "working",
|
|
216
|
+
completed: "completed",
|
|
217
|
+
failed: "failed",
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = {
|
|
221
|
+
id: task.id,
|
|
222
|
+
status: { state: stateMap[task.status] || task.status },
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (task.status === "completed" && task.result) {
|
|
226
|
+
result.status.message = {
|
|
227
|
+
role: "agent",
|
|
228
|
+
parts: [{ type: "text", text: task.result }],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (task.status === "failed" && task.error) {
|
|
233
|
+
result.status.message = {
|
|
234
|
+
role: "agent",
|
|
235
|
+
parts: [{ type: "text", text: `Error: ${task.error}` }],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
res.json({ jsonrpc: "2.0", result });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* GET /a2a/tasks/:id/stream — SSE stream of task progress (requires auth).
|
|
244
|
+
*/
|
|
245
|
+
app.get("/a2a/tasks/:id/stream", a2aAuth, (req, res) => {
|
|
246
|
+
const taskId = req.params.id;
|
|
247
|
+
const task = loadTask(taskId);
|
|
248
|
+
|
|
249
|
+
if (!task || task.channel !== "a2a") {
|
|
250
|
+
return res.status(404).json({ error: "Task not found" });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
res.writeHead(200, {
|
|
254
|
+
"Content-Type": "text/event-stream",
|
|
255
|
+
"Cache-Control": "no-cache",
|
|
256
|
+
Connection: "keep-alive",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
res.write(
|
|
260
|
+
`data: ${JSON.stringify({ type: "status", state: task.status })}\n\n`
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (task.status === "completed" || task.status === "failed") {
|
|
264
|
+
res.write(
|
|
265
|
+
`data: ${JSON.stringify({
|
|
266
|
+
type: "result",
|
|
267
|
+
state: task.status,
|
|
268
|
+
text: task.result || task.error,
|
|
269
|
+
})}\n\n`
|
|
270
|
+
);
|
|
271
|
+
res.end();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const onCompleted = (data) => {
|
|
276
|
+
if (data.taskId === taskId) {
|
|
277
|
+
res.write(
|
|
278
|
+
`data: ${JSON.stringify({
|
|
279
|
+
type: "result",
|
|
280
|
+
state: "completed",
|
|
281
|
+
text: data.result,
|
|
282
|
+
})}\n\n`
|
|
283
|
+
);
|
|
284
|
+
cleanup();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const onFailed = (data) => {
|
|
289
|
+
if (data.taskId === taskId) {
|
|
290
|
+
res.write(
|
|
291
|
+
`data: ${JSON.stringify({
|
|
292
|
+
type: "result",
|
|
293
|
+
state: "failed",
|
|
294
|
+
text: data.error,
|
|
295
|
+
})}\n\n`
|
|
296
|
+
);
|
|
297
|
+
cleanup();
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
eventBus.on("task:completed", onCompleted);
|
|
302
|
+
eventBus.on("task:failed", onFailed);
|
|
303
|
+
|
|
304
|
+
const cleanup = () => {
|
|
305
|
+
eventBus.off("task:completed", onCompleted);
|
|
306
|
+
eventBus.off("task:failed", onFailed);
|
|
307
|
+
res.end();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
req.on("close", cleanup);
|
|
311
|
+
setTimeout(cleanup, 300000);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const status = config.a2a.enabled ? "ENABLED" : "DISABLED (set A2A_ENABLED=true)";
|
|
315
|
+
console.log(`[A2A] Server endpoints mounted — ${status}`);
|
|
316
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { config } from "../config/default.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A2A Agent Card — serves agent capabilities at /.well-known/agent.json
|
|
5
|
+
*
|
|
6
|
+
* SECURITY: Only serves the card when A2A is enabled.
|
|
7
|
+
* Does NOT expose internal tools or file system capabilities.
|
|
8
|
+
*/
|
|
9
|
+
export function getAgentCard() {
|
|
10
|
+
return {
|
|
11
|
+
name: "Daemora",
|
|
12
|
+
description:
|
|
13
|
+
"A multi-agent digital worker. Handles research, analysis, and general tasks.",
|
|
14
|
+
url: `http://localhost:${config.port}`,
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
protocol: "a2a/1.0",
|
|
17
|
+
|
|
18
|
+
capabilities: {
|
|
19
|
+
streaming: true,
|
|
20
|
+
pushNotifications: false,
|
|
21
|
+
stateTransitions: true,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Only expose safe, high-level skill categories — NOT internal tools
|
|
25
|
+
skills: [
|
|
26
|
+
{
|
|
27
|
+
id: "research",
|
|
28
|
+
name: "Web Research",
|
|
29
|
+
description: "Search the web and synthesize information.",
|
|
30
|
+
tags: ["research", "search", "web"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "analysis",
|
|
34
|
+
name: "Text Analysis",
|
|
35
|
+
description: "Analyze text, summarize, answer questions.",
|
|
36
|
+
tags: ["analysis", "summary", "qa"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "documents",
|
|
40
|
+
name: "Document Creation",
|
|
41
|
+
description: "Create markdown documents and reports.",
|
|
42
|
+
tags: ["documents", "markdown"],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
endpoints: {
|
|
47
|
+
tasks: `/a2a/tasks`,
|
|
48
|
+
taskStatus: `/a2a/tasks/:id`,
|
|
49
|
+
stream: `/a2a/tasks/:id/stream`,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
authentication: {
|
|
53
|
+
type: config.a2a.authToken ? "bearer" : "none",
|
|
54
|
+
description: config.a2a.authToken
|
|
55
|
+
? "Include Authorization: Bearer <token> header"
|
|
56
|
+
: "No authentication required",
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
security: {
|
|
60
|
+
permissionTier: config.a2a.permissionTier,
|
|
61
|
+
rateLimitPerMinute: config.a2a.rateLimitPerMinute,
|
|
62
|
+
note: "A2A tasks run with restricted permissions. Dangerous tools are blocked.",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Mount A2A discovery endpoint on Express app.
|
|
69
|
+
*/
|
|
70
|
+
export function mountAgentCard(app) {
|
|
71
|
+
app.get("/.well-known/agent.json", (req, res) => {
|
|
72
|
+
if (!config.a2a.enabled) {
|
|
73
|
+
return res.status(404).json({
|
|
74
|
+
error: "A2A protocol is not enabled on this agent.",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
res.json(getAgentCard());
|
|
78
|
+
});
|
|
79
|
+
}
|