network-ai 3.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/LICENSE +21 -0
- package/QUICKSTART.md +260 -0
- package/README.md +604 -0
- package/SKILL.md +568 -0
- package/dist/adapters/adapter-registry.d.ts +94 -0
- package/dist/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/adapters/adapter-registry.js +355 -0
- package/dist/adapters/adapter-registry.js.map +1 -0
- package/dist/adapters/agno-adapter.d.ts +112 -0
- package/dist/adapters/agno-adapter.d.ts.map +1 -0
- package/dist/adapters/agno-adapter.js +140 -0
- package/dist/adapters/agno-adapter.js.map +1 -0
- package/dist/adapters/autogen-adapter.d.ts +67 -0
- package/dist/adapters/autogen-adapter.d.ts.map +1 -0
- package/dist/adapters/autogen-adapter.js +141 -0
- package/dist/adapters/autogen-adapter.js.map +1 -0
- package/dist/adapters/base-adapter.d.ts +51 -0
- package/dist/adapters/base-adapter.d.ts.map +1 -0
- package/dist/adapters/base-adapter.js +103 -0
- package/dist/adapters/base-adapter.js.map +1 -0
- package/dist/adapters/crewai-adapter.d.ts +72 -0
- package/dist/adapters/crewai-adapter.d.ts.map +1 -0
- package/dist/adapters/crewai-adapter.js +148 -0
- package/dist/adapters/crewai-adapter.js.map +1 -0
- package/dist/adapters/custom-adapter.d.ts +74 -0
- package/dist/adapters/custom-adapter.d.ts.map +1 -0
- package/dist/adapters/custom-adapter.js +142 -0
- package/dist/adapters/custom-adapter.js.map +1 -0
- package/dist/adapters/dspy-adapter.d.ts +70 -0
- package/dist/adapters/dspy-adapter.d.ts.map +1 -0
- package/dist/adapters/dspy-adapter.js +127 -0
- package/dist/adapters/dspy-adapter.js.map +1 -0
- package/dist/adapters/haystack-adapter.d.ts +83 -0
- package/dist/adapters/haystack-adapter.d.ts.map +1 -0
- package/dist/adapters/haystack-adapter.js +149 -0
- package/dist/adapters/haystack-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +56 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/langchain-adapter.d.ts +51 -0
- package/dist/adapters/langchain-adapter.d.ts.map +1 -0
- package/dist/adapters/langchain-adapter.js +134 -0
- package/dist/adapters/langchain-adapter.js.map +1 -0
- package/dist/adapters/llamaindex-adapter.d.ts +89 -0
- package/dist/adapters/llamaindex-adapter.d.ts.map +1 -0
- package/dist/adapters/llamaindex-adapter.js +135 -0
- package/dist/adapters/llamaindex-adapter.js.map +1 -0
- package/dist/adapters/mcp-adapter.d.ts +90 -0
- package/dist/adapters/mcp-adapter.d.ts.map +1 -0
- package/dist/adapters/mcp-adapter.js +200 -0
- package/dist/adapters/mcp-adapter.js.map +1 -0
- package/dist/adapters/openai-assistants-adapter.d.ts +94 -0
- package/dist/adapters/openai-assistants-adapter.d.ts.map +1 -0
- package/dist/adapters/openai-assistants-adapter.js +130 -0
- package/dist/adapters/openai-assistants-adapter.js.map +1 -0
- package/dist/adapters/openclaw-adapter.d.ts +21 -0
- package/dist/adapters/openclaw-adapter.d.ts.map +1 -0
- package/dist/adapters/openclaw-adapter.js +140 -0
- package/dist/adapters/openclaw-adapter.js.map +1 -0
- package/dist/adapters/semantic-kernel-adapter.d.ts +73 -0
- package/dist/adapters/semantic-kernel-adapter.d.ts.map +1 -0
- package/dist/adapters/semantic-kernel-adapter.js +123 -0
- package/dist/adapters/semantic-kernel-adapter.js.map +1 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1428 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/blackboard-validator.d.ts +205 -0
- package/dist/lib/blackboard-validator.d.ts.map +1 -0
- package/dist/lib/blackboard-validator.js +756 -0
- package/dist/lib/blackboard-validator.js.map +1 -0
- package/dist/lib/locked-blackboard.d.ts +174 -0
- package/dist/lib/locked-blackboard.d.ts.map +1 -0
- package/dist/lib/locked-blackboard.js +654 -0
- package/dist/lib/locked-blackboard.js.map +1 -0
- package/dist/lib/swarm-utils.d.ts +136 -0
- package/dist/lib/swarm-utils.d.ts.map +1 -0
- package/dist/lib/swarm-utils.js +510 -0
- package/dist/lib/swarm-utils.js.map +1 -0
- package/dist/security.d.ts +269 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +713 -0
- package/dist/security.js.map +1 -0
- package/package.json +84 -0
- package/scripts/blackboard.py +819 -0
- package/scripts/check_permission.py +331 -0
- package/scripts/revoke_token.py +243 -0
- package/scripts/swarm_guard.py +1140 -0
- package/scripts/validate_token.py +97 -0
- package/types/agent-adapter.d.ts +244 -0
- package/types/openclaw-core.d.ts +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SwarmOrchestrator - Multi-Agent Swarm Orchestration Skill
|
|
4
|
+
*
|
|
5
|
+
* This module implements the core logic for agent-to-agent communication,
|
|
6
|
+
* task decomposition, permission management, and shared blackboard coordination.
|
|
7
|
+
*
|
|
8
|
+
* @module SwarmOrchestrator
|
|
9
|
+
* @version 3.0.0
|
|
10
|
+
* @license MIT
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.CustomAdapter = exports.MCPAdapter = exports.CrewAIAdapter = exports.AutoGenAdapter = exports.LangChainAdapter = exports.OpenClawAdapter = exports.BaseAdapter = exports.AdapterRegistry = exports.QualityGateAgent = exports.BlackboardValidator = exports.TaskDecomposer = exports.AuthGuardian = exports.SharedBlackboard = exports.SwarmOrchestrator = void 0;
|
|
14
|
+
exports.createSwarmOrchestrator = createSwarmOrchestrator;
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = require("path");
|
|
17
|
+
const crypto_1 = require("crypto");
|
|
18
|
+
const adapter_registry_1 = require("./adapters/adapter-registry");
|
|
19
|
+
const security_1 = require("./security");
|
|
20
|
+
const locked_blackboard_1 = require("./lib/locked-blackboard");
|
|
21
|
+
const blackboard_validator_1 = require("./lib/blackboard-validator");
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// CONFIGURATION
|
|
24
|
+
// ============================================================================
|
|
25
|
+
const CONFIG = {
|
|
26
|
+
blackboardPath: './swarm-blackboard.md',
|
|
27
|
+
maxParallelAgents: 3,
|
|
28
|
+
defaultTimeout: 30000,
|
|
29
|
+
enableTracing: true,
|
|
30
|
+
grantTokenTTL: 300000, // 5 minutes in milliseconds
|
|
31
|
+
maxBlackboardValueSize: 1024 * 1024, // 1 MB max per entry
|
|
32
|
+
auditLogPath: './data/audit_log.jsonl',
|
|
33
|
+
trustConfigPath: './data/trust_levels.json',
|
|
34
|
+
};
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// DEFAULT RESOURCE PROFILES -- Universal, domain-agnostic
|
|
37
|
+
// Users can override/extend these for any domain (coding, finance, devops, etc.)
|
|
38
|
+
// ============================================================================
|
|
39
|
+
const DEFAULT_RESOURCE_PROFILES = {
|
|
40
|
+
// --- Financial / Enterprise ---
|
|
41
|
+
SAP_API: { baseRisk: 0.5, defaultRestrictions: ['read_only', 'max_records:100'], description: 'SAP enterprise API' },
|
|
42
|
+
FINANCIAL_API: { baseRisk: 0.7, defaultRestrictions: ['read_only', 'no_pii_fields', 'audit_required'], description: 'Financial data API' },
|
|
43
|
+
DATA_EXPORT: { baseRisk: 0.6, defaultRestrictions: ['anonymize_pii', 'local_only'], description: 'Data export operations' },
|
|
44
|
+
// --- Coding / Development ---
|
|
45
|
+
FILE_SYSTEM: { baseRisk: 0.5, defaultRestrictions: ['workspace_only', 'no_system_dirs', 'max_file_size:10mb'], description: 'Read/write files in workspace' },
|
|
46
|
+
SHELL_EXEC: { baseRisk: 0.8, defaultRestrictions: ['sandbox_only', 'no_sudo', 'timeout:30s', 'audit_required'], description: 'Execute shell commands' },
|
|
47
|
+
GIT: { baseRisk: 0.4, defaultRestrictions: ['local_repo_only', 'no_force_push'], description: 'Git operations' },
|
|
48
|
+
PACKAGE_MANAGER: { baseRisk: 0.6, defaultRestrictions: ['audit_required', 'no_global_install', 'lockfile_required'], description: 'npm/pip/cargo package management' },
|
|
49
|
+
BUILD_TOOL: { baseRisk: 0.5, defaultRestrictions: ['workspace_only', 'timeout:120s'], description: 'Build and compilation' },
|
|
50
|
+
// --- Infrastructure / DevOps ---
|
|
51
|
+
DOCKER: { baseRisk: 0.7, defaultRestrictions: ['no_privileged', 'no_host_network', 'audit_required'], description: 'Container operations' },
|
|
52
|
+
CLOUD_DEPLOY: { baseRisk: 0.9, defaultRestrictions: ['staging_only', 'approval_required', 'rollback_ready'], description: 'Cloud deployment' },
|
|
53
|
+
DATABASE: { baseRisk: 0.6, defaultRestrictions: ['read_only', 'max_records:1000', 'no_schema_changes'], description: 'Database access' },
|
|
54
|
+
// --- Communication / External ---
|
|
55
|
+
EXTERNAL_SERVICE: { baseRisk: 0.4, defaultRestrictions: ['rate_limit:10_per_minute'], description: 'External API calls' },
|
|
56
|
+
EMAIL: { baseRisk: 0.5, defaultRestrictions: ['rate_limit:5_per_minute', 'no_attachments'], description: 'Email sending' },
|
|
57
|
+
WEBHOOK: { baseRisk: 0.4, defaultRestrictions: ['allowed_domains_only', 'no_credentials'], description: 'Webhook dispatch' },
|
|
58
|
+
};
|
|
59
|
+
const DEFAULT_AGENT_TRUST = [
|
|
60
|
+
{ agentId: 'orchestrator', trustLevel: 0.9, allowedNamespaces: ['*'], allowedResources: ['*'] },
|
|
61
|
+
{ agentId: 'data_analyst', trustLevel: 0.8, allowedNamespaces: ['task:', 'analytics:', 'agent:'], allowedResources: ['SAP_API', 'DATABASE', 'DATA_EXPORT', 'EXTERNAL_SERVICE'] },
|
|
62
|
+
{ agentId: 'strategy_advisor', trustLevel: 0.7, allowedNamespaces: ['task:', 'strategy:'], allowedResources: ['EXTERNAL_SERVICE', 'DATA_EXPORT'] },
|
|
63
|
+
{ agentId: 'risk_assessor', trustLevel: 0.85, allowedNamespaces: ['task:', 'risk:', 'analytics:'], allowedResources: ['EXTERNAL_SERVICE', 'DATABASE'] },
|
|
64
|
+
// Coding agents
|
|
65
|
+
{ agentId: 'code_writer', trustLevel: 0.75, allowedNamespaces: ['task:', 'code:', 'build:'], allowedResources: ['FILE_SYSTEM', 'GIT', 'BUILD_TOOL', 'PACKAGE_MANAGER'] },
|
|
66
|
+
{ agentId: 'code_reviewer', trustLevel: 0.8, allowedNamespaces: ['task:', 'code:', 'review:'], allowedResources: ['FILE_SYSTEM', 'GIT'] },
|
|
67
|
+
{ agentId: 'test_runner', trustLevel: 0.75, allowedNamespaces: ['task:', 'test:', 'build:'], allowedResources: ['FILE_SYSTEM', 'SHELL_EXEC', 'BUILD_TOOL'] },
|
|
68
|
+
{ agentId: 'devops_agent', trustLevel: 0.7, allowedNamespaces: ['task:', 'deploy:', 'infra:'], allowedResources: ['DOCKER', 'SHELL_EXEC', 'CLOUD_DEPLOY', 'GIT'] },
|
|
69
|
+
];
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// BLACKBOARD MANAGEMENT -- Secured with LockedBlackboard, identity verification,
|
|
72
|
+
// namespace scoping, value validation, and input sanitization
|
|
73
|
+
// ============================================================================
|
|
74
|
+
class SharedBlackboard {
|
|
75
|
+
backend;
|
|
76
|
+
agentTokens = new Map(); // agentId -> verified token
|
|
77
|
+
agentNamespaces = new Map(); // agentId -> allowed prefixes
|
|
78
|
+
constructor(basePath) {
|
|
79
|
+
this.backend = new locked_blackboard_1.LockedBlackboard(basePath);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Register a verified agent identity. Only agents with registered tokens
|
|
83
|
+
* can write to the blackboard. The orchestrator registers agents after
|
|
84
|
+
* verifying their identity through the AuthGuardian.
|
|
85
|
+
*/
|
|
86
|
+
registerAgent(agentId, verificationToken, allowedNamespaces = ['*']) {
|
|
87
|
+
this.agentTokens.set(agentId, verificationToken);
|
|
88
|
+
this.agentNamespaces.set(agentId, allowedNamespaces);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if an agent is allowed to access a key based on namespace rules.
|
|
92
|
+
*/
|
|
93
|
+
canAccessKey(agentId, key) {
|
|
94
|
+
const namespaces = this.agentNamespaces.get(agentId);
|
|
95
|
+
if (!namespaces)
|
|
96
|
+
return false;
|
|
97
|
+
if (namespaces.includes('*'))
|
|
98
|
+
return true;
|
|
99
|
+
return namespaces.some(ns => key.startsWith(ns));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Verify that the calling agent is who they claim to be.
|
|
103
|
+
*/
|
|
104
|
+
verifyAgent(agentId, token) {
|
|
105
|
+
const registeredToken = this.agentTokens.get(agentId);
|
|
106
|
+
// If no token system is configured for this agent, allow (backward compat)
|
|
107
|
+
if (!registeredToken)
|
|
108
|
+
return true;
|
|
109
|
+
return token === registeredToken;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Validate value size and structure before writing.
|
|
113
|
+
* Prevents DoS via oversized writes and circular data.
|
|
114
|
+
*/
|
|
115
|
+
validateValue(value) {
|
|
116
|
+
try {
|
|
117
|
+
const serialized = JSON.stringify(value);
|
|
118
|
+
if (serialized.length > CONFIG.maxBlackboardValueSize) {
|
|
119
|
+
return { valid: false, reason: `Value exceeds max size (${serialized.length} > ${CONFIG.maxBlackboardValueSize} bytes)` };
|
|
120
|
+
}
|
|
121
|
+
return { valid: true };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return { valid: false, reason: 'Value cannot be serialized (circular reference or invalid structure)' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Sanitize a key to prevent markdown injection.
|
|
129
|
+
*/
|
|
130
|
+
sanitizeKey(key) {
|
|
131
|
+
// Keys must be safe for markdown headings -- no #, newlines, or markdown syntax
|
|
132
|
+
return key.replace(/[#\n\r|`]/g, '_').slice(0, 256);
|
|
133
|
+
}
|
|
134
|
+
read(key) {
|
|
135
|
+
const entry = this.backend.read(key);
|
|
136
|
+
if (!entry)
|
|
137
|
+
return null;
|
|
138
|
+
// Normalize field name for backward compatibility
|
|
139
|
+
return {
|
|
140
|
+
key: entry.key,
|
|
141
|
+
value: entry.value,
|
|
142
|
+
sourceAgent: entry.source_agent ?? entry.sourceAgent ?? 'unknown',
|
|
143
|
+
timestamp: entry.timestamp,
|
|
144
|
+
ttl: entry.ttl,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Write to the blackboard with identity verification, namespace checks,
|
|
149
|
+
* value validation, and input sanitization. Uses LockedBlackboard for
|
|
150
|
+
* atomic file-system writes.
|
|
151
|
+
*
|
|
152
|
+
* @param key - The key to write
|
|
153
|
+
* @param value - The value (will be sanitized and size-checked)
|
|
154
|
+
* @param sourceAgent - Agent claiming to write (verified against registered token)
|
|
155
|
+
* @param ttl - Optional TTL in seconds
|
|
156
|
+
* @param agentToken - Optional verification token for identity check
|
|
157
|
+
*/
|
|
158
|
+
write(key, value, sourceAgent, ttl, agentToken) {
|
|
159
|
+
// 1. Verify agent identity
|
|
160
|
+
if (!this.verifyAgent(sourceAgent, agentToken)) {
|
|
161
|
+
throw new Error(`[Blackboard] Identity verification failed for agent '${sourceAgent}'`);
|
|
162
|
+
}
|
|
163
|
+
// 2. Namespace check
|
|
164
|
+
if (!this.canAccessKey(sourceAgent, key)) {
|
|
165
|
+
throw new Error(`[Blackboard] Agent '${sourceAgent}' not allowed to write to key '${key}'`);
|
|
166
|
+
}
|
|
167
|
+
// 3. Sanitize key
|
|
168
|
+
const safeKey = this.sanitizeKey(key);
|
|
169
|
+
// 4. Validate value size/structure
|
|
170
|
+
const validation = this.validateValue(value);
|
|
171
|
+
if (!validation.valid) {
|
|
172
|
+
throw new Error(`[Blackboard] Value validation failed: ${validation.reason}`);
|
|
173
|
+
}
|
|
174
|
+
// 5. Sanitize value -- strip injection payloads from string content
|
|
175
|
+
let sanitizedValue;
|
|
176
|
+
try {
|
|
177
|
+
sanitizedValue = security_1.InputSanitizer.sanitizeObject(value);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
sanitizedValue = value; // Fall back to raw if sanitization can't handle it
|
|
181
|
+
}
|
|
182
|
+
// 6. Write through LockedBlackboard (atomic, file-locked)
|
|
183
|
+
const entry = this.backend.write(safeKey, sanitizedValue, sourceAgent, ttl);
|
|
184
|
+
// Normalize for backward compat
|
|
185
|
+
return {
|
|
186
|
+
key: entry.key,
|
|
187
|
+
value: entry.value,
|
|
188
|
+
sourceAgent: entry.source_agent ?? sourceAgent,
|
|
189
|
+
timestamp: entry.timestamp,
|
|
190
|
+
ttl: entry.ttl,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
exists(key) {
|
|
194
|
+
return this.read(key) !== null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get a full snapshot of all blackboard entries.
|
|
198
|
+
*/
|
|
199
|
+
getSnapshot() {
|
|
200
|
+
const raw = this.backend.getSnapshot();
|
|
201
|
+
const normalized = {};
|
|
202
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
203
|
+
normalized[key] = {
|
|
204
|
+
key: entry.key,
|
|
205
|
+
value: entry.value,
|
|
206
|
+
sourceAgent: entry.source_agent ?? entry.sourceAgent ?? 'unknown',
|
|
207
|
+
timestamp: entry.timestamp,
|
|
208
|
+
ttl: entry.ttl,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return normalized;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get a namespace-scoped snapshot -- only returns keys an agent is allowed to see.
|
|
215
|
+
* Prevents data leakage between agents.
|
|
216
|
+
*/
|
|
217
|
+
getScopedSnapshot(agentId) {
|
|
218
|
+
const full = this.getSnapshot();
|
|
219
|
+
const scoped = {};
|
|
220
|
+
for (const [key, entry] of Object.entries(full)) {
|
|
221
|
+
if (this.canAccessKey(agentId, key)) {
|
|
222
|
+
scoped[key] = entry;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return scoped;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Clear all entries (for testing).
|
|
229
|
+
*/
|
|
230
|
+
clear() {
|
|
231
|
+
// Write an empty state through locked backend
|
|
232
|
+
const keys = this.backend.listKeys();
|
|
233
|
+
for (const key of keys) {
|
|
234
|
+
this.backend.delete(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
exports.SharedBlackboard = SharedBlackboard;
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// AUTH GUARDIAN - UNIVERSAL PERMISSION WALL IMPLEMENTATION
|
|
241
|
+
// Now domain-agnostic: resource types, risk profiles, trust levels, and
|
|
242
|
+
// restrictions are all configurable. Works for coding, finance, devops, etc.
|
|
243
|
+
// Integrates with SecureSwarmGateway for HMAC tokens, rate limiting,
|
|
244
|
+
// input sanitization, and cryptographic audit logs.
|
|
245
|
+
// ============================================================================
|
|
246
|
+
class AuthGuardian {
|
|
247
|
+
activeGrants = new Map();
|
|
248
|
+
agentTrustLevels = new Map();
|
|
249
|
+
agentTrustConfigs = new Map();
|
|
250
|
+
resourceProfiles = new Map();
|
|
251
|
+
auditLog = [];
|
|
252
|
+
auditLogPath;
|
|
253
|
+
trustConfigPath;
|
|
254
|
+
constructor(options) {
|
|
255
|
+
this.auditLogPath = options?.auditLogPath ?? CONFIG.auditLogPath;
|
|
256
|
+
this.trustConfigPath = options?.trustConfigPath ?? CONFIG.trustConfigPath;
|
|
257
|
+
// Load resource profiles (user-provided + defaults)
|
|
258
|
+
const profiles = { ...DEFAULT_RESOURCE_PROFILES, ...(options?.resourceProfiles ?? {}) };
|
|
259
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
260
|
+
this.resourceProfiles.set(name, profile);
|
|
261
|
+
}
|
|
262
|
+
// Load trust levels (try disk first, then user-provided, then defaults)
|
|
263
|
+
const trustConfigs = options?.trustLevels ?? this.loadTrustFromDisk() ?? DEFAULT_AGENT_TRUST;
|
|
264
|
+
for (const config of trustConfigs) {
|
|
265
|
+
this.agentTrustLevels.set(config.agentId, config.trustLevel);
|
|
266
|
+
this.agentTrustConfigs.set(config.agentId, config);
|
|
267
|
+
}
|
|
268
|
+
// Load existing audit log from disk
|
|
269
|
+
this.loadAuditFromDisk();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Register a new resource type at runtime.
|
|
273
|
+
* Makes the system extensible for any domain.
|
|
274
|
+
*/
|
|
275
|
+
registerResourceType(name, profile) {
|
|
276
|
+
this.resourceProfiles.set(name, profile);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Register or update an agent's trust configuration at runtime.
|
|
280
|
+
*/
|
|
281
|
+
registerAgentTrust(config) {
|
|
282
|
+
this.agentTrustLevels.set(config.agentId, config.trustLevel);
|
|
283
|
+
this.agentTrustConfigs.set(config.agentId, config);
|
|
284
|
+
this.persistTrustToDisk();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Request permission to access a resource.
|
|
288
|
+
* resourceType is now a free string -- validated against registered profiles.
|
|
289
|
+
*/
|
|
290
|
+
async requestPermission(agentId, resourceType, justification, scope) {
|
|
291
|
+
// Sanitize inputs
|
|
292
|
+
let safeAgentId;
|
|
293
|
+
let safeJustification;
|
|
294
|
+
try {
|
|
295
|
+
safeAgentId = security_1.InputSanitizer.sanitizeAgentId(agentId);
|
|
296
|
+
safeJustification = security_1.InputSanitizer.sanitizeString(justification, 2000);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
safeAgentId = agentId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'unknown';
|
|
300
|
+
safeJustification = justification.slice(0, 2000);
|
|
301
|
+
}
|
|
302
|
+
this.log('permission_request', { agentId: safeAgentId, resourceType, justification: safeJustification, scope });
|
|
303
|
+
// Check if agent is allowed to access this resource type
|
|
304
|
+
const agentConfig = this.agentTrustConfigs.get(safeAgentId);
|
|
305
|
+
if (agentConfig && agentConfig.allowedResources && !agentConfig.allowedResources.includes('*')) {
|
|
306
|
+
if (!agentConfig.allowedResources.includes(resourceType)) {
|
|
307
|
+
this.log('permission_denied', { agentId: safeAgentId, resourceType, reason: 'resource_not_in_allowlist' });
|
|
308
|
+
return {
|
|
309
|
+
granted: false,
|
|
310
|
+
grantToken: null,
|
|
311
|
+
expiresAt: null,
|
|
312
|
+
restrictions: [],
|
|
313
|
+
reason: `Agent '${safeAgentId}' is not authorized to access '${resourceType}'. Allowed: ${agentConfig.allowedResources.join(', ')}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Evaluate the permission request
|
|
318
|
+
const evaluation = this.evaluateRequest(safeAgentId, resourceType, safeJustification, scope);
|
|
319
|
+
if (!evaluation.approved) {
|
|
320
|
+
this.log('permission_denied', { agentId: safeAgentId, resourceType, reason: evaluation.reason });
|
|
321
|
+
return {
|
|
322
|
+
granted: false,
|
|
323
|
+
grantToken: null,
|
|
324
|
+
expiresAt: null,
|
|
325
|
+
restrictions: [],
|
|
326
|
+
reason: evaluation.reason,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
// Generate grant token
|
|
330
|
+
const grantToken = this.generateGrantToken();
|
|
331
|
+
const expiresAt = new Date(Date.now() + CONFIG.grantTokenTTL).toISOString();
|
|
332
|
+
const grant = {
|
|
333
|
+
grantToken,
|
|
334
|
+
resourceType,
|
|
335
|
+
agentId: safeAgentId,
|
|
336
|
+
expiresAt,
|
|
337
|
+
restrictions: evaluation.restrictions,
|
|
338
|
+
scope,
|
|
339
|
+
};
|
|
340
|
+
this.activeGrants.set(grantToken, grant);
|
|
341
|
+
this.log('permission_granted', { grantToken, agentId: safeAgentId, resourceType, expiresAt, restrictions: evaluation.restrictions });
|
|
342
|
+
return {
|
|
343
|
+
granted: true,
|
|
344
|
+
grantToken,
|
|
345
|
+
expiresAt,
|
|
346
|
+
restrictions: evaluation.restrictions,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
validateToken(token) {
|
|
350
|
+
const grant = this.activeGrants.get(token);
|
|
351
|
+
if (!grant)
|
|
352
|
+
return false;
|
|
353
|
+
if (new Date(grant.expiresAt) < new Date()) {
|
|
354
|
+
this.activeGrants.delete(token);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Validate a token and return the bound restrictions and scope.
|
|
361
|
+
* Used to enforce restrictions at the point of use.
|
|
362
|
+
*/
|
|
363
|
+
validateTokenWithGrant(token) {
|
|
364
|
+
const grant = this.activeGrants.get(token);
|
|
365
|
+
if (!grant)
|
|
366
|
+
return null;
|
|
367
|
+
if (new Date(grant.expiresAt) < new Date()) {
|
|
368
|
+
this.activeGrants.delete(token);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
return grant;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Enforce restrictions on an operation. Returns an error string if
|
|
375
|
+
* the operation violates any restriction, or null if allowed.
|
|
376
|
+
*/
|
|
377
|
+
enforceRestrictions(grantToken, operation) {
|
|
378
|
+
const grant = this.validateTokenWithGrant(grantToken);
|
|
379
|
+
if (!grant)
|
|
380
|
+
return 'Invalid or expired grant token';
|
|
381
|
+
for (const restriction of grant.restrictions) {
|
|
382
|
+
// Enforce read_only
|
|
383
|
+
if (restriction === 'read_only' && operation.type && operation.type !== 'read') {
|
|
384
|
+
return `Restriction 'read_only' violated: attempted '${operation.type}'`;
|
|
385
|
+
}
|
|
386
|
+
// Enforce max_records
|
|
387
|
+
const maxRecordsMatch = restriction.match(/^max_records:(\d+)$/);
|
|
388
|
+
if (maxRecordsMatch && operation.recordCount) {
|
|
389
|
+
const max = parseInt(maxRecordsMatch[1], 10);
|
|
390
|
+
if (operation.recordCount > max) {
|
|
391
|
+
return `Restriction '${restriction}' violated: requested ${operation.recordCount} records`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Enforce sandbox_only
|
|
395
|
+
if (restriction === 'sandbox_only' && operation.targetPath) {
|
|
396
|
+
if (/^\/|^[A-Z]:\\(?:Windows|Program)/i.test(operation.targetPath)) {
|
|
397
|
+
return `Restriction 'sandbox_only' violated: path '${operation.targetPath}' is outside sandbox`;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Enforce no_sudo
|
|
401
|
+
if (restriction === 'no_sudo' && operation.command) {
|
|
402
|
+
if (/\bsudo\b/i.test(operation.command)) {
|
|
403
|
+
return `Restriction 'no_sudo' violated: command contains sudo`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Enforce workspace_only
|
|
407
|
+
if (restriction === 'workspace_only' && operation.targetPath) {
|
|
408
|
+
if (/\.\.[/\\]/.test(operation.targetPath)) {
|
|
409
|
+
return `Restriction 'workspace_only' violated: path traversal detected`;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Enforce no_system_dirs
|
|
413
|
+
if (restriction === 'no_system_dirs' && operation.targetPath) {
|
|
414
|
+
if (/(?:\/etc|\/usr|\/var|\\Windows|\\System32)/i.test(operation.targetPath)) {
|
|
415
|
+
return `Restriction 'no_system_dirs' violated: system directory access`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Enforce no_attachments
|
|
419
|
+
if (restriction === 'no_attachments' && operation.hasAttachments) {
|
|
420
|
+
return `Restriction 'no_attachments' violated`;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return null; // All restrictions passed
|
|
424
|
+
}
|
|
425
|
+
revokeToken(token) {
|
|
426
|
+
this.activeGrants.delete(token);
|
|
427
|
+
this.log('permission_revoked', { token });
|
|
428
|
+
}
|
|
429
|
+
evaluateRequest(agentId, resourceType, justification, scope) {
|
|
430
|
+
// 1. Justification Quality (40% weight) -- now includes resource-relevance
|
|
431
|
+
const justificationScore = this.scoreJustification(justification, resourceType);
|
|
432
|
+
if (justificationScore < 0.3) {
|
|
433
|
+
return {
|
|
434
|
+
approved: false,
|
|
435
|
+
reason: 'Justification is insufficient. Please provide specific task context.',
|
|
436
|
+
restrictions: [],
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// 2. Agent Trust Level (30% weight)
|
|
440
|
+
const trustLevel = this.agentTrustLevels.get(agentId) ?? 0.5;
|
|
441
|
+
if (trustLevel < 0.4) {
|
|
442
|
+
return {
|
|
443
|
+
approved: false,
|
|
444
|
+
reason: 'Agent trust level is below threshold. Escalate to human operator.',
|
|
445
|
+
restrictions: [],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
// 3. Risk Assessment (30% weight)
|
|
449
|
+
const riskScore = this.assessRisk(resourceType, scope);
|
|
450
|
+
if (riskScore > 0.8) {
|
|
451
|
+
return {
|
|
452
|
+
approved: false,
|
|
453
|
+
reason: 'Risk assessment exceeds acceptable threshold. Narrow the requested scope.',
|
|
454
|
+
restrictions: [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
// Get restrictions from resource profile (data-driven, not hardcoded)
|
|
458
|
+
const profile = this.resourceProfiles.get(resourceType);
|
|
459
|
+
const restrictions = profile
|
|
460
|
+
? [...profile.defaultRestrictions]
|
|
461
|
+
: ['audit_required']; // Unknown resources get audited by default
|
|
462
|
+
// Calculate weighted approval
|
|
463
|
+
const weightedScore = (justificationScore * 0.4) + (trustLevel * 0.3) + ((1 - riskScore) * 0.3);
|
|
464
|
+
const approved = weightedScore >= 0.5;
|
|
465
|
+
return {
|
|
466
|
+
approved,
|
|
467
|
+
reason: approved ? undefined : 'Combined evaluation score below threshold.',
|
|
468
|
+
restrictions,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Improved justification scoring with resource-relevance checking.
|
|
473
|
+
* Prevents trivial gaming by verifying the justification mentions
|
|
474
|
+
* concepts relevant to the requested resource.
|
|
475
|
+
*/
|
|
476
|
+
scoreJustification(justification, resourceType) {
|
|
477
|
+
let score = 0;
|
|
478
|
+
// Length scoring
|
|
479
|
+
if (justification.length > 20)
|
|
480
|
+
score += 0.15;
|
|
481
|
+
if (justification.length > 50)
|
|
482
|
+
score += 0.15;
|
|
483
|
+
// Intent keywords
|
|
484
|
+
if (/task|purpose|need|require|generate|analyze|process|build|deploy|test|review/i.test(justification))
|
|
485
|
+
score += 0.15;
|
|
486
|
+
// Specificity keywords
|
|
487
|
+
if (/specific|particular|exact|for\s+the|in\s+order\s+to|because|so\s+that/i.test(justification))
|
|
488
|
+
score += 0.15;
|
|
489
|
+
// Penalty for vague/test phrasing
|
|
490
|
+
if (/^test$|^debug$|^try$|^just\s+testing/i.test(justification.trim()))
|
|
491
|
+
score -= 0.3;
|
|
492
|
+
// Resource-relevance check: does the justification mention anything related
|
|
493
|
+
// to the requested resource? (+0.2 bonus for relevant context)
|
|
494
|
+
if (resourceType) {
|
|
495
|
+
const relevancePatterns = {
|
|
496
|
+
SAP_API: /sap|erp|invoice|procurement|purchase|material|vendor/i,
|
|
497
|
+
FINANCIAL_API: /financ|revenue|budget|accounting|payment|ledger|balance/i,
|
|
498
|
+
DATA_EXPORT: /export|report|csv|download|extract|migrate/i,
|
|
499
|
+
FILE_SYSTEM: /file|read|write|save|load|path|directory|workspace/i,
|
|
500
|
+
SHELL_EXEC: /command|script|compile|build|run|execute|terminal/i,
|
|
501
|
+
GIT: /git|commit|branch|merge|pull|push|repository|diff/i,
|
|
502
|
+
PACKAGE_MANAGER: /package|install|dependency|npm|pip|cargo|module/i,
|
|
503
|
+
BUILD_TOOL: /build|compile|webpack|tsc|make|gradle|cargo/i,
|
|
504
|
+
DOCKER: /container|docker|image|deploy|service|compose/i,
|
|
505
|
+
CLOUD_DEPLOY: /deploy|cloud|staging|production|release|infrastructure/i,
|
|
506
|
+
DATABASE: /database|query|sql|table|record|schema|migration/i,
|
|
507
|
+
EXTERNAL_SERVICE: /api|service|endpoint|webhook|request|fetch/i,
|
|
508
|
+
EMAIL: /email|mail|send|notification|alert|message/i,
|
|
509
|
+
WEBHOOK: /webhook|callback|notification|event|dispatch/i,
|
|
510
|
+
};
|
|
511
|
+
const pattern = relevancePatterns[resourceType];
|
|
512
|
+
if (pattern && pattern.test(justification)) {
|
|
513
|
+
score += 0.2;
|
|
514
|
+
}
|
|
515
|
+
else if (pattern && !pattern.test(justification)) {
|
|
516
|
+
// Justification doesn't mention anything relevant -- small penalty
|
|
517
|
+
score -= 0.1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// Bonus for mentioning a task/ticket ID
|
|
521
|
+
if (/(?:task|ticket|issue|jira|pr|bug)[_\-#]?\s*\d+/i.test(justification))
|
|
522
|
+
score += 0.1;
|
|
523
|
+
return Math.max(0, Math.min(score, 1));
|
|
524
|
+
}
|
|
525
|
+
assessRisk(resourceType, scope) {
|
|
526
|
+
// Look up base risk from registered profile (not hardcoded)
|
|
527
|
+
const profile = this.resourceProfiles.get(resourceType);
|
|
528
|
+
let risk = profile?.baseRisk ?? 0.5; // Unknown resources get medium risk
|
|
529
|
+
// Broad scopes increase risk
|
|
530
|
+
if (!scope || scope === '*' || scope === 'all') {
|
|
531
|
+
risk += 0.2;
|
|
532
|
+
}
|
|
533
|
+
// Write/delete operations increase risk
|
|
534
|
+
if (scope && /write|delete|update|modify|execute|deploy/i.test(scope)) {
|
|
535
|
+
risk += 0.2;
|
|
536
|
+
}
|
|
537
|
+
return Math.min(risk, 1);
|
|
538
|
+
}
|
|
539
|
+
generateGrantToken() {
|
|
540
|
+
return `grant_${(0, crypto_1.randomUUID)().replace(/-/g, '')}`;
|
|
541
|
+
}
|
|
542
|
+
log(action, details) {
|
|
543
|
+
const entry = {
|
|
544
|
+
timestamp: new Date().toISOString(),
|
|
545
|
+
action,
|
|
546
|
+
details,
|
|
547
|
+
};
|
|
548
|
+
this.auditLog.push(entry);
|
|
549
|
+
// Persist to disk
|
|
550
|
+
try {
|
|
551
|
+
const dir = (0, path_1.join)('.', 'data');
|
|
552
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
553
|
+
require('fs').mkdirSync(dir, { recursive: true });
|
|
554
|
+
}
|
|
555
|
+
(0, fs_1.appendFileSync)(this.auditLogPath, JSON.stringify(entry) + '\n');
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Non-fatal -- log is also in memory
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
getActiveGrants() {
|
|
562
|
+
// Clean expired grants
|
|
563
|
+
const now = new Date();
|
|
564
|
+
for (const [token, grant] of this.activeGrants.entries()) {
|
|
565
|
+
if (new Date(grant.expiresAt) < now) {
|
|
566
|
+
this.activeGrants.delete(token);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return Array.from(this.activeGrants.values());
|
|
570
|
+
}
|
|
571
|
+
getAuditLog() {
|
|
572
|
+
return [...this.auditLog];
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Get all registered resource profiles.
|
|
576
|
+
*/
|
|
577
|
+
getResourceProfiles() {
|
|
578
|
+
return Object.fromEntries(this.resourceProfiles);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get the allowed namespaces for an agent (used by blackboard scoping).
|
|
582
|
+
*/
|
|
583
|
+
getAgentNamespaces(agentId) {
|
|
584
|
+
const config = this.agentTrustConfigs.get(agentId);
|
|
585
|
+
return config?.allowedNamespaces ?? ['task:'];
|
|
586
|
+
}
|
|
587
|
+
// ---- Persistence helpers ----
|
|
588
|
+
loadTrustFromDisk() {
|
|
589
|
+
try {
|
|
590
|
+
if ((0, fs_1.existsSync)(this.trustConfigPath)) {
|
|
591
|
+
const raw = (0, fs_1.readFileSync)(this.trustConfigPath, 'utf-8');
|
|
592
|
+
return JSON.parse(raw);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch { /* ignore */ }
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
persistTrustToDisk() {
|
|
599
|
+
try {
|
|
600
|
+
const dir = (0, path_1.join)('.', 'data');
|
|
601
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
602
|
+
require('fs').mkdirSync(dir, { recursive: true });
|
|
603
|
+
}
|
|
604
|
+
const configs = Array.from(this.agentTrustConfigs.values());
|
|
605
|
+
(0, fs_1.writeFileSync)(this.trustConfigPath, JSON.stringify(configs, null, 2));
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
// Non-fatal
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
loadAuditFromDisk() {
|
|
612
|
+
try {
|
|
613
|
+
if ((0, fs_1.existsSync)(this.auditLogPath)) {
|
|
614
|
+
const raw = (0, fs_1.readFileSync)(this.auditLogPath, 'utf-8');
|
|
615
|
+
const lines = raw.trim().split('\n').filter(l => l);
|
|
616
|
+
for (const line of lines) {
|
|
617
|
+
try {
|
|
618
|
+
this.auditLog.push(JSON.parse(line));
|
|
619
|
+
}
|
|
620
|
+
catch { /* skip malformed */ }
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch { /* ignore */ }
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
exports.AuthGuardian = AuthGuardian;
|
|
628
|
+
// ============================================================================
|
|
629
|
+
// TASK DECOMPOSITION ENGINE
|
|
630
|
+
// ============================================================================
|
|
631
|
+
class TaskDecomposer {
|
|
632
|
+
blackboard;
|
|
633
|
+
authGuardian;
|
|
634
|
+
adapterRegistry;
|
|
635
|
+
constructor(blackboard, authGuardian, adapterRegistry) {
|
|
636
|
+
this.blackboard = blackboard;
|
|
637
|
+
this.authGuardian = authGuardian;
|
|
638
|
+
this.adapterRegistry = adapterRegistry;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Decomposes a complex task into parallel sub-agent calls
|
|
642
|
+
* This is the "Wall Breaker" - transforms impossible monolithic tasks
|
|
643
|
+
* into manageable parallel executions
|
|
644
|
+
*/
|
|
645
|
+
async executeParallel(tasks, synthesisStrategy = 'merge', context) {
|
|
646
|
+
// Enforce maximum parallel agent limit
|
|
647
|
+
if (tasks.length > CONFIG.maxParallelAgents) {
|
|
648
|
+
throw new Error(`Cannot spawn ${tasks.length} agents. Maximum is ${CONFIG.maxParallelAgents}. ` +
|
|
649
|
+
`Decompose further or use 'chain' strategy.`);
|
|
650
|
+
}
|
|
651
|
+
const startTime = Date.now();
|
|
652
|
+
const individualResults = [];
|
|
653
|
+
// Check blackboard for cached results first
|
|
654
|
+
const cachedTasks = [];
|
|
655
|
+
const uncachedTasks = [];
|
|
656
|
+
for (const task of tasks) {
|
|
657
|
+
const cacheKey = `task:${task.agentType}:${this.hashPayload(task.taskPayload)}`;
|
|
658
|
+
const cached = this.blackboard.read(cacheKey);
|
|
659
|
+
if (cached) {
|
|
660
|
+
individualResults.push({
|
|
661
|
+
agentType: task.agentType,
|
|
662
|
+
success: true,
|
|
663
|
+
result: cached.value,
|
|
664
|
+
executionTime: 0, // From cache
|
|
665
|
+
});
|
|
666
|
+
cachedTasks.push(task);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
uncachedTasks.push(task);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Execute uncached tasks in parallel using Promise.all
|
|
673
|
+
if (uncachedTasks.length > 0) {
|
|
674
|
+
const parallelPromises = uncachedTasks.map(task => this.executeSingleTask(task, context));
|
|
675
|
+
const results = await Promise.all(parallelPromises);
|
|
676
|
+
for (let i = 0; i < results.length; i++) {
|
|
677
|
+
const task = uncachedTasks[i];
|
|
678
|
+
const result = results[i];
|
|
679
|
+
individualResults.push(result);
|
|
680
|
+
// Cache successful results
|
|
681
|
+
if (result.success) {
|
|
682
|
+
const cacheKey = `task:${task.agentType}:${this.hashPayload(task.taskPayload)}`;
|
|
683
|
+
this.blackboard.write(cacheKey, result.result, context.agentId, 3600); // 1 hour TTL
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Synthesize results based on strategy
|
|
688
|
+
const synthesizedResult = this.synthesize(individualResults, synthesisStrategy);
|
|
689
|
+
const totalTime = Date.now() - startTime;
|
|
690
|
+
const successCount = individualResults.filter(r => r.success).length;
|
|
691
|
+
return {
|
|
692
|
+
synthesizedResult,
|
|
693
|
+
individualResults,
|
|
694
|
+
executionMetrics: {
|
|
695
|
+
totalTime,
|
|
696
|
+
successRate: successCount / individualResults.length,
|
|
697
|
+
synthesisStrategy,
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async executeSingleTask(task, context) {
|
|
702
|
+
const taskStart = Date.now();
|
|
703
|
+
try {
|
|
704
|
+
// Build the handoff message
|
|
705
|
+
const handoff = {
|
|
706
|
+
handoffId: (0, crypto_1.randomUUID)(),
|
|
707
|
+
sourceAgent: context.agentId,
|
|
708
|
+
targetAgent: task.agentType,
|
|
709
|
+
taskType: 'delegate',
|
|
710
|
+
payload: task.taskPayload,
|
|
711
|
+
metadata: {
|
|
712
|
+
priority: 1,
|
|
713
|
+
deadline: Date.now() + CONFIG.defaultTimeout,
|
|
714
|
+
parentTaskId: context.taskId ?? null,
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
// Sanitize the instruction before sending to adapter
|
|
718
|
+
let sanitizedInstruction = task.taskPayload.instruction;
|
|
719
|
+
try {
|
|
720
|
+
sanitizedInstruction = security_1.InputSanitizer.sanitizeString(task.taskPayload.instruction, 10000);
|
|
721
|
+
}
|
|
722
|
+
catch { /* use original if sanitization fails */ }
|
|
723
|
+
// Use namespace-scoped snapshot -- target agent only sees keys it's allowed to see
|
|
724
|
+
const scopedSnapshot = this.blackboard.getScopedSnapshot(task.agentType);
|
|
725
|
+
// Route through the adapter registry (framework-agnostic)
|
|
726
|
+
const agentPayload = {
|
|
727
|
+
action: 'execute',
|
|
728
|
+
params: {},
|
|
729
|
+
handoff: {
|
|
730
|
+
handoffId: handoff.handoffId,
|
|
731
|
+
sourceAgent: handoff.sourceAgent,
|
|
732
|
+
targetAgent: handoff.targetAgent,
|
|
733
|
+
taskType: handoff.taskType,
|
|
734
|
+
instruction: sanitizedInstruction,
|
|
735
|
+
context: handoff.payload.context,
|
|
736
|
+
constraints: handoff.payload.constraints,
|
|
737
|
+
expectedOutput: handoff.payload.expectedOutput,
|
|
738
|
+
metadata: handoff.metadata,
|
|
739
|
+
},
|
|
740
|
+
blackboardSnapshot: scopedSnapshot,
|
|
741
|
+
};
|
|
742
|
+
const agentContext = {
|
|
743
|
+
agentId: context.agentId,
|
|
744
|
+
taskId: context.taskId,
|
|
745
|
+
sessionId: context.sessionId,
|
|
746
|
+
};
|
|
747
|
+
const result = await this.adapterRegistry.executeAgent(task.agentType, agentPayload, agentContext);
|
|
748
|
+
// Sanitize adapter output before returning/caching
|
|
749
|
+
let sanitizedData = result.data;
|
|
750
|
+
try {
|
|
751
|
+
sanitizedData = security_1.InputSanitizer.sanitizeObject(result.data);
|
|
752
|
+
}
|
|
753
|
+
catch { /* use raw if sanitization fails */ }
|
|
754
|
+
return {
|
|
755
|
+
agentType: task.agentType,
|
|
756
|
+
success: true,
|
|
757
|
+
result: sanitizedData,
|
|
758
|
+
executionTime: Date.now() - taskStart,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
return {
|
|
763
|
+
agentType: task.agentType,
|
|
764
|
+
success: false,
|
|
765
|
+
result: {
|
|
766
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
767
|
+
recoverable: true,
|
|
768
|
+
},
|
|
769
|
+
executionTime: Date.now() - taskStart,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
synthesize(results, strategy) {
|
|
774
|
+
const successfulResults = results.filter(r => r.success);
|
|
775
|
+
if (successfulResults.length === 0) {
|
|
776
|
+
return {
|
|
777
|
+
error: 'All parallel tasks failed',
|
|
778
|
+
individualErrors: results.map(r => ({
|
|
779
|
+
agent: r.agentType,
|
|
780
|
+
error: r.result,
|
|
781
|
+
})),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
switch (strategy) {
|
|
785
|
+
case 'merge':
|
|
786
|
+
// Combine all results into a unified object
|
|
787
|
+
return {
|
|
788
|
+
merged: true,
|
|
789
|
+
contributions: successfulResults.map(r => ({
|
|
790
|
+
source: r.agentType,
|
|
791
|
+
data: r.result,
|
|
792
|
+
})),
|
|
793
|
+
summary: this.generateMergeSummary(successfulResults),
|
|
794
|
+
};
|
|
795
|
+
case 'vote':
|
|
796
|
+
// Return the result with highest "confidence" (simplified: most data)
|
|
797
|
+
const scored = successfulResults.map(r => ({
|
|
798
|
+
result: r,
|
|
799
|
+
score: JSON.stringify(r.result).length,
|
|
800
|
+
}));
|
|
801
|
+
scored.sort((a, b) => b.score - a.score);
|
|
802
|
+
return {
|
|
803
|
+
voted: true,
|
|
804
|
+
winner: scored[0].result.agentType,
|
|
805
|
+
result: scored[0].result.result,
|
|
806
|
+
};
|
|
807
|
+
case 'chain':
|
|
808
|
+
// Results should already be ordered; return the final one
|
|
809
|
+
return {
|
|
810
|
+
chained: true,
|
|
811
|
+
finalResult: successfulResults[successfulResults.length - 1].result,
|
|
812
|
+
chainLength: successfulResults.length,
|
|
813
|
+
};
|
|
814
|
+
case 'first-success':
|
|
815
|
+
// Return the first successful result
|
|
816
|
+
return {
|
|
817
|
+
firstSuccess: true,
|
|
818
|
+
source: successfulResults[0].agentType,
|
|
819
|
+
result: successfulResults[0].result,
|
|
820
|
+
};
|
|
821
|
+
default:
|
|
822
|
+
return successfulResults.map(r => r.result);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
generateMergeSummary(results) {
|
|
826
|
+
const agents = results.map(r => r.agentType).join(', ');
|
|
827
|
+
return `Synthesized from ${results.length} agents: ${agents}`;
|
|
828
|
+
}
|
|
829
|
+
hashPayload(payload) {
|
|
830
|
+
// Simple hash for cache key generation
|
|
831
|
+
const str = JSON.stringify(payload);
|
|
832
|
+
let hash = 0;
|
|
833
|
+
for (let i = 0; i < str.length; i++) {
|
|
834
|
+
const char = str.charCodeAt(i);
|
|
835
|
+
hash = ((hash << 5) - hash) + char;
|
|
836
|
+
hash = hash & hash;
|
|
837
|
+
}
|
|
838
|
+
return Math.abs(hash).toString(16);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
exports.TaskDecomposer = TaskDecomposer;
|
|
842
|
+
// ============================================================================
|
|
843
|
+
// SWARM ORCHESTRATOR - MAIN SKILL IMPLEMENTATION
|
|
844
|
+
// ============================================================================
|
|
845
|
+
class SwarmOrchestrator {
|
|
846
|
+
name = 'SwarmOrchestrator';
|
|
847
|
+
version = '3.0.0';
|
|
848
|
+
blackboard;
|
|
849
|
+
authGuardian;
|
|
850
|
+
taskDecomposer;
|
|
851
|
+
agentRegistry = new Map();
|
|
852
|
+
gateway;
|
|
853
|
+
qualityGate;
|
|
854
|
+
/** The adapter registry -- routes requests to the right agent framework */
|
|
855
|
+
adapters;
|
|
856
|
+
constructor(workspacePath = process.cwd(), adapterRegistry, options) {
|
|
857
|
+
this.blackboard = new SharedBlackboard(workspacePath);
|
|
858
|
+
this.authGuardian = new AuthGuardian({
|
|
859
|
+
trustLevels: options?.trustLevels,
|
|
860
|
+
resourceProfiles: options?.resourceProfiles,
|
|
861
|
+
});
|
|
862
|
+
this.adapters = adapterRegistry ?? new adapter_registry_1.AdapterRegistry();
|
|
863
|
+
this.taskDecomposer = new TaskDecomposer(this.blackboard, this.authGuardian, this.adapters);
|
|
864
|
+
this.gateway = new security_1.SecureSwarmGateway();
|
|
865
|
+
this.qualityGate = new blackboard_validator_1.QualityGateAgent({
|
|
866
|
+
validationConfig: options?.validationConfig,
|
|
867
|
+
qualityThreshold: options?.qualityThreshold,
|
|
868
|
+
aiReviewCallback: options?.aiReviewCallback,
|
|
869
|
+
});
|
|
870
|
+
// Register the orchestrator agent on the blackboard with full access
|
|
871
|
+
this.blackboard.registerAgent('orchestrator', 'system-orchestrator-token', ['*']);
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Add an agent framework adapter (LangChain, AutoGen, CrewAI, MCP, custom, etc.)
|
|
875
|
+
* This is the plug-and-play entry point.
|
|
876
|
+
*/
|
|
877
|
+
async addAdapter(adapter, config = {}) {
|
|
878
|
+
await this.adapters.addAdapter(adapter, config);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Main entry point for the skill.
|
|
882
|
+
* Now integrates SecureSwarmGateway: every request flows through
|
|
883
|
+
* input sanitization, rate limiting, and agent ID validation.
|
|
884
|
+
*/
|
|
885
|
+
async execute(action, params, context) {
|
|
886
|
+
const traceId = (0, crypto_1.randomUUID)();
|
|
887
|
+
// P0: Route through SecureSwarmGateway -- sanitization + rate limiting
|
|
888
|
+
const gatewayResult = await this.gateway.handleSecureRequest(context.agentId, action, params);
|
|
889
|
+
if (!gatewayResult.allowed) {
|
|
890
|
+
return {
|
|
891
|
+
success: false,
|
|
892
|
+
error: {
|
|
893
|
+
code: 'GATEWAY_DENIED',
|
|
894
|
+
message: `Security gateway denied request: ${gatewayResult.reason}`,
|
|
895
|
+
recoverable: true,
|
|
896
|
+
suggestedAction: 'Check agent ID, rate limits, or input format',
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
// Use sanitized params from gateway
|
|
901
|
+
const safeParams = gatewayResult.sanitizedParams ?? params;
|
|
902
|
+
if (CONFIG.enableTracing) {
|
|
903
|
+
try {
|
|
904
|
+
this.blackboard.write(`trace:${traceId}`, {
|
|
905
|
+
action,
|
|
906
|
+
startTime: new Date().toISOString(),
|
|
907
|
+
}, context.agentId);
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
// Non-fatal -- tracing failure shouldn't block execution
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
switch (action) {
|
|
915
|
+
case 'delegate_task':
|
|
916
|
+
return await this.delegateTask(safeParams, context);
|
|
917
|
+
case 'query_swarm_state':
|
|
918
|
+
return await this.querySwarmState(safeParams, context);
|
|
919
|
+
case 'spawn_parallel_agents':
|
|
920
|
+
return await this.spawnParallelAgents(safeParams, context);
|
|
921
|
+
case 'request_permission':
|
|
922
|
+
return await this.handlePermissionRequest(safeParams, context);
|
|
923
|
+
case 'update_blackboard':
|
|
924
|
+
return await this.handleBlackboardUpdate(safeParams, context);
|
|
925
|
+
case 'quality_gate_status':
|
|
926
|
+
return this.handleQualityGateStatus();
|
|
927
|
+
case 'review_quarantine':
|
|
928
|
+
return this.handleQuarantineReview(safeParams);
|
|
929
|
+
default:
|
|
930
|
+
return {
|
|
931
|
+
success: false,
|
|
932
|
+
error: {
|
|
933
|
+
code: 'UNKNOWN_ACTION',
|
|
934
|
+
message: `Unknown action: ${action}`,
|
|
935
|
+
recoverable: false,
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
error: {
|
|
944
|
+
code: 'EXECUTION_ERROR',
|
|
945
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
946
|
+
recoverable: true,
|
|
947
|
+
trace: { traceId, action },
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
// -------------------------------------------------------------------------
|
|
953
|
+
// CAPABILITY: delegate_task
|
|
954
|
+
// -------------------------------------------------------------------------
|
|
955
|
+
async delegateTask(params, context) {
|
|
956
|
+
const targetAgent = params.targetAgent;
|
|
957
|
+
const taskPayload = params.taskPayload;
|
|
958
|
+
const priority = params.priority ?? 'normal';
|
|
959
|
+
const timeout = params.timeout ?? CONFIG.defaultTimeout;
|
|
960
|
+
const requiresAuth = params.requiresAuth ?? false;
|
|
961
|
+
const resourceType = params.resourceType ?? 'EXTERNAL_SERVICE';
|
|
962
|
+
// Check permission wall if required -- now returns bound restrictions
|
|
963
|
+
let grantToken = null;
|
|
964
|
+
if (requiresAuth) {
|
|
965
|
+
const authResult = await this.authGuardian.requestPermission(context.agentId, resourceType, `Delegating task to ${targetAgent}: ${taskPayload.instruction}`, 'delegate');
|
|
966
|
+
if (!authResult.granted) {
|
|
967
|
+
return {
|
|
968
|
+
success: false,
|
|
969
|
+
error: {
|
|
970
|
+
code: 'AUTH_DENIED',
|
|
971
|
+
message: `Permission denied: ${authResult.reason}`,
|
|
972
|
+
recoverable: true,
|
|
973
|
+
suggestedAction: 'Provide more specific justification or narrow scope',
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
grantToken = authResult.grantToken;
|
|
978
|
+
// Enforce restrictions at point of use
|
|
979
|
+
if (grantToken) {
|
|
980
|
+
const restrictionViolation = this.authGuardian.enforceRestrictions(grantToken, {
|
|
981
|
+
type: 'execute',
|
|
982
|
+
});
|
|
983
|
+
if (restrictionViolation) {
|
|
984
|
+
return {
|
|
985
|
+
success: false,
|
|
986
|
+
error: {
|
|
987
|
+
code: 'RESTRICTION_VIOLATED',
|
|
988
|
+
message: restrictionViolation,
|
|
989
|
+
recoverable: true,
|
|
990
|
+
suggestedAction: 'Request a grant with broader scope',
|
|
991
|
+
},
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// Check blackboard for existing work
|
|
997
|
+
const cacheKey = `task:${targetAgent}:${JSON.stringify(taskPayload).slice(0, 50)}`;
|
|
998
|
+
const existingWork = this.blackboard.read(cacheKey);
|
|
999
|
+
if (existingWork) {
|
|
1000
|
+
return {
|
|
1001
|
+
success: true,
|
|
1002
|
+
data: {
|
|
1003
|
+
taskId: 'cached',
|
|
1004
|
+
status: 'completed',
|
|
1005
|
+
result: existingWork.value,
|
|
1006
|
+
agentTrace: ['blackboard-cache'],
|
|
1007
|
+
fromCache: true,
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
// Build handoff message
|
|
1012
|
+
const handoff = {
|
|
1013
|
+
handoffId: (0, crypto_1.randomUUID)(),
|
|
1014
|
+
sourceAgent: context.agentId,
|
|
1015
|
+
targetAgent,
|
|
1016
|
+
taskType: 'delegate',
|
|
1017
|
+
payload: taskPayload,
|
|
1018
|
+
metadata: {
|
|
1019
|
+
priority: this.priorityToNumber(priority),
|
|
1020
|
+
deadline: Date.now() + timeout,
|
|
1021
|
+
parentTaskId: context.taskId ?? null,
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
// Execute via adapter registry (routes to the right framework)
|
|
1025
|
+
try {
|
|
1026
|
+
// Sanitize instruction before sending to adapter
|
|
1027
|
+
let sanitizedInstruction = taskPayload.instruction;
|
|
1028
|
+
try {
|
|
1029
|
+
sanitizedInstruction = security_1.InputSanitizer.sanitizeString(taskPayload.instruction, 10000);
|
|
1030
|
+
}
|
|
1031
|
+
catch { /* use original if sanitization fails */ }
|
|
1032
|
+
// P1: Namespace-scoped snapshot -- target agent only sees keys it's allowed to see
|
|
1033
|
+
const scopedSnapshot = this.blackboard.getScopedSnapshot(targetAgent);
|
|
1034
|
+
const agentPayload = {
|
|
1035
|
+
action: 'execute',
|
|
1036
|
+
params: {},
|
|
1037
|
+
handoff: {
|
|
1038
|
+
handoffId: handoff.handoffId,
|
|
1039
|
+
sourceAgent: handoff.sourceAgent,
|
|
1040
|
+
targetAgent: handoff.targetAgent,
|
|
1041
|
+
taskType: handoff.taskType,
|
|
1042
|
+
instruction: sanitizedInstruction,
|
|
1043
|
+
context: taskPayload.context,
|
|
1044
|
+
constraints: taskPayload.constraints,
|
|
1045
|
+
expectedOutput: taskPayload.expectedOutput,
|
|
1046
|
+
metadata: handoff.metadata,
|
|
1047
|
+
},
|
|
1048
|
+
blackboardSnapshot: scopedSnapshot,
|
|
1049
|
+
};
|
|
1050
|
+
const agentContext = {
|
|
1051
|
+
agentId: context.agentId,
|
|
1052
|
+
taskId: context.taskId,
|
|
1053
|
+
sessionId: context.sessionId,
|
|
1054
|
+
};
|
|
1055
|
+
const result = await Promise.race([
|
|
1056
|
+
this.adapters.executeAgent(targetAgent, agentPayload, agentContext),
|
|
1057
|
+
this.timeoutPromise(timeout),
|
|
1058
|
+
]);
|
|
1059
|
+
// P1: Sanitize adapter output before caching
|
|
1060
|
+
let sanitizedResult = result;
|
|
1061
|
+
try {
|
|
1062
|
+
sanitizedResult = security_1.InputSanitizer.sanitizeObject(result);
|
|
1063
|
+
}
|
|
1064
|
+
catch { /* use raw if sanitization fails */ }
|
|
1065
|
+
// Quality gate: validate result before committing to blackboard
|
|
1066
|
+
const gateResult = await this.qualityGate.gate(cacheKey, sanitizedResult, targetAgent, {
|
|
1067
|
+
taskInstruction: taskPayload.instruction,
|
|
1068
|
+
expectedOutput: taskPayload.expectedOutput,
|
|
1069
|
+
});
|
|
1070
|
+
if (gateResult.decision === 'reject') {
|
|
1071
|
+
return {
|
|
1072
|
+
success: false,
|
|
1073
|
+
error: {
|
|
1074
|
+
code: 'QUALITY_REJECTED',
|
|
1075
|
+
message: `Result from ${targetAgent} failed quality validation: ${gateResult.validation.issues.filter(i => i.severity === 'error').map(i => i.message).join('; ')}`,
|
|
1076
|
+
recoverable: gateResult.validation.recoverable,
|
|
1077
|
+
suggestedAction: gateResult.validation.issues.find(i => i.suggestion)?.suggestion,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
if (gateResult.decision === 'quarantine') {
|
|
1082
|
+
// Still return the result but flag it
|
|
1083
|
+
return {
|
|
1084
|
+
success: true,
|
|
1085
|
+
data: {
|
|
1086
|
+
taskId: handoff.handoffId,
|
|
1087
|
+
status: 'quarantined',
|
|
1088
|
+
result: sanitizedResult,
|
|
1089
|
+
agentTrace: [context.agentId, targetAgent],
|
|
1090
|
+
qualityGate: {
|
|
1091
|
+
decision: 'quarantine',
|
|
1092
|
+
quarantineKey: gateResult.quarantineKey,
|
|
1093
|
+
score: gateResult.validation.score,
|
|
1094
|
+
issues: gateResult.validation.issues,
|
|
1095
|
+
reviewNotes: gateResult.reviewNotes,
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
// Approved -- cache result
|
|
1101
|
+
this.blackboard.write(cacheKey, sanitizedResult, context.agentId, 1800); // 30 min TTL
|
|
1102
|
+
return {
|
|
1103
|
+
success: true,
|
|
1104
|
+
data: {
|
|
1105
|
+
taskId: handoff.handoffId,
|
|
1106
|
+
status: 'completed',
|
|
1107
|
+
result: sanitizedResult,
|
|
1108
|
+
agentTrace: [context.agentId, targetAgent],
|
|
1109
|
+
qualityGate: {
|
|
1110
|
+
decision: 'approve',
|
|
1111
|
+
score: gateResult.validation.score,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
catch (error) {
|
|
1117
|
+
return {
|
|
1118
|
+
success: false,
|
|
1119
|
+
error: {
|
|
1120
|
+
code: 'DELEGATION_FAILED',
|
|
1121
|
+
message: error instanceof Error ? error.message : 'Task delegation failed',
|
|
1122
|
+
recoverable: true,
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// -------------------------------------------------------------------------
|
|
1128
|
+
// CAPABILITY: query_swarm_state
|
|
1129
|
+
// -------------------------------------------------------------------------
|
|
1130
|
+
async querySwarmState(params, context) {
|
|
1131
|
+
const scope = params.scope ?? 'all';
|
|
1132
|
+
const agentFilter = params.agentFilter;
|
|
1133
|
+
const includeHistory = params.includeHistory ?? false;
|
|
1134
|
+
const state = {
|
|
1135
|
+
timestamp: new Date().toISOString(),
|
|
1136
|
+
};
|
|
1137
|
+
if (scope === 'all' || scope === 'agents') {
|
|
1138
|
+
let agents = Array.from(this.agentRegistry.values());
|
|
1139
|
+
if (agentFilter) {
|
|
1140
|
+
agents = agents.filter(a => agentFilter.includes(a.agentId));
|
|
1141
|
+
}
|
|
1142
|
+
state.activeAgents = agents;
|
|
1143
|
+
}
|
|
1144
|
+
if (scope === 'all' || scope === 'blackboard') {
|
|
1145
|
+
// P1: Namespace-scoped -- agent only sees keys it's allowed to access
|
|
1146
|
+
state.blackboardSnapshot = this.blackboard.getScopedSnapshot(context.agentId);
|
|
1147
|
+
}
|
|
1148
|
+
if (scope === 'all' || scope === 'permissions') {
|
|
1149
|
+
state.permissionGrants = this.authGuardian.getActiveGrants();
|
|
1150
|
+
}
|
|
1151
|
+
if (scope === 'all' || scope === 'tasks') {
|
|
1152
|
+
// Extract tasks from scoped blackboard
|
|
1153
|
+
const snapshot = this.blackboard.getScopedSnapshot(context.agentId);
|
|
1154
|
+
state.pendingTasks = Object.entries(snapshot)
|
|
1155
|
+
.filter(([key]) => key.startsWith('task:'))
|
|
1156
|
+
.map(([, entry]) => ({
|
|
1157
|
+
taskId: entry.key,
|
|
1158
|
+
agentId: entry.sourceAgent,
|
|
1159
|
+
status: 'in_progress',
|
|
1160
|
+
startedAt: entry.timestamp,
|
|
1161
|
+
description: String(entry.value),
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
return {
|
|
1165
|
+
success: true,
|
|
1166
|
+
data: state,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
// -------------------------------------------------------------------------
|
|
1170
|
+
// CAPABILITY: spawn_parallel_agents
|
|
1171
|
+
// -------------------------------------------------------------------------
|
|
1172
|
+
async spawnParallelAgents(params, context) {
|
|
1173
|
+
const tasks = params.tasks;
|
|
1174
|
+
const synthesisStrategy = params.synthesisStrategy ?? 'merge';
|
|
1175
|
+
if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
|
|
1176
|
+
return {
|
|
1177
|
+
success: false,
|
|
1178
|
+
error: {
|
|
1179
|
+
code: 'INVALID_PARAMS',
|
|
1180
|
+
message: 'Tasks array is required and must not be empty',
|
|
1181
|
+
recoverable: false,
|
|
1182
|
+
},
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
const result = await this.taskDecomposer.executeParallel(tasks, synthesisStrategy, context);
|
|
1187
|
+
return {
|
|
1188
|
+
success: true,
|
|
1189
|
+
data: result,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
catch (error) {
|
|
1193
|
+
return {
|
|
1194
|
+
success: false,
|
|
1195
|
+
error: {
|
|
1196
|
+
code: 'PARALLEL_EXECUTION_FAILED',
|
|
1197
|
+
message: error instanceof Error ? error.message : 'Parallel execution failed',
|
|
1198
|
+
recoverable: true,
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
// -------------------------------------------------------------------------
|
|
1204
|
+
// CAPABILITY: request_permission
|
|
1205
|
+
// -------------------------------------------------------------------------
|
|
1206
|
+
async handlePermissionRequest(params, context) {
|
|
1207
|
+
const resourceType = params.resourceType;
|
|
1208
|
+
const justification = params.justification;
|
|
1209
|
+
const scope = params.scope;
|
|
1210
|
+
if (!resourceType || !justification) {
|
|
1211
|
+
return {
|
|
1212
|
+
success: false,
|
|
1213
|
+
error: {
|
|
1214
|
+
code: 'INVALID_PARAMS',
|
|
1215
|
+
message: 'resourceType and justification are required',
|
|
1216
|
+
recoverable: false,
|
|
1217
|
+
},
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const grant = await this.authGuardian.requestPermission(context.agentId, resourceType, justification, scope);
|
|
1221
|
+
return {
|
|
1222
|
+
success: grant.granted,
|
|
1223
|
+
data: grant,
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
// -------------------------------------------------------------------------
|
|
1227
|
+
// CAPABILITY: update_blackboard
|
|
1228
|
+
// -------------------------------------------------------------------------
|
|
1229
|
+
async handleBlackboardUpdate(params, context) {
|
|
1230
|
+
const key = params.key;
|
|
1231
|
+
const value = params.value;
|
|
1232
|
+
const ttl = params.ttl;
|
|
1233
|
+
if (!key || value === undefined) {
|
|
1234
|
+
return {
|
|
1235
|
+
success: false,
|
|
1236
|
+
error: {
|
|
1237
|
+
code: 'INVALID_PARAMS',
|
|
1238
|
+
message: 'key and value are required',
|
|
1239
|
+
recoverable: false,
|
|
1240
|
+
},
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
const previousValue = this.blackboard.read(key)?.value ?? null;
|
|
1244
|
+
// Quality gate: validate before writing to blackboard
|
|
1245
|
+
const gateResult = await this.qualityGate.gate(key, value, context.agentId);
|
|
1246
|
+
if (gateResult.decision === 'reject') {
|
|
1247
|
+
return {
|
|
1248
|
+
success: false,
|
|
1249
|
+
error: {
|
|
1250
|
+
code: 'QUALITY_REJECTED',
|
|
1251
|
+
message: `Blackboard write rejected: ${gateResult.validation.issues.filter(i => i.severity === 'error').map(i => i.message).join('; ')}`,
|
|
1252
|
+
recoverable: gateResult.validation.recoverable,
|
|
1253
|
+
suggestedAction: gateResult.validation.issues.find(i => i.suggestion)?.suggestion,
|
|
1254
|
+
},
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
if (gateResult.decision === 'quarantine') {
|
|
1258
|
+
return {
|
|
1259
|
+
success: true,
|
|
1260
|
+
data: {
|
|
1261
|
+
success: true,
|
|
1262
|
+
quarantined: true,
|
|
1263
|
+
quarantineKey: gateResult.quarantineKey,
|
|
1264
|
+
qualityScore: gateResult.validation.score,
|
|
1265
|
+
issues: gateResult.validation.issues,
|
|
1266
|
+
previousValue,
|
|
1267
|
+
},
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
this.blackboard.write(key, value, context.agentId, ttl);
|
|
1271
|
+
return {
|
|
1272
|
+
success: true,
|
|
1273
|
+
data: {
|
|
1274
|
+
success: true,
|
|
1275
|
+
previousValue,
|
|
1276
|
+
qualityScore: gateResult.validation.score,
|
|
1277
|
+
},
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
// -------------------------------------------------------------------------
|
|
1281
|
+
// QUALITY GATE MANAGEMENT
|
|
1282
|
+
// -------------------------------------------------------------------------
|
|
1283
|
+
/** Returns quality gate metrics and quarantined entries */
|
|
1284
|
+
handleQualityGateStatus() {
|
|
1285
|
+
return {
|
|
1286
|
+
success: true,
|
|
1287
|
+
data: {
|
|
1288
|
+
metrics: this.qualityGate.getMetrics(),
|
|
1289
|
+
quarantined: this.qualityGate.getQuarantined(),
|
|
1290
|
+
},
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
/** Approve or reject a quarantined entry */
|
|
1294
|
+
handleQuarantineReview(params) {
|
|
1295
|
+
const quarantineId = params.quarantineId;
|
|
1296
|
+
const decision = params.decision;
|
|
1297
|
+
if (!quarantineId || !decision) {
|
|
1298
|
+
return {
|
|
1299
|
+
success: false,
|
|
1300
|
+
error: {
|
|
1301
|
+
code: 'INVALID_PARAMS',
|
|
1302
|
+
message: 'quarantineId and decision ("approve" or "reject") are required',
|
|
1303
|
+
recoverable: false,
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
let entry;
|
|
1308
|
+
if (decision === 'approve') {
|
|
1309
|
+
entry = this.qualityGate.approveQuarantined(quarantineId);
|
|
1310
|
+
if (entry) {
|
|
1311
|
+
// Write the approved entry to the blackboard
|
|
1312
|
+
this.blackboard.write(`approved:${quarantineId}`, entry, 'orchestrator');
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
entry = this.qualityGate.rejectQuarantined(quarantineId);
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
success: !!entry,
|
|
1320
|
+
data: entry ? { quarantineId, decision, resolved: true } : undefined,
|
|
1321
|
+
error: entry ? undefined : {
|
|
1322
|
+
code: 'NOT_FOUND',
|
|
1323
|
+
message: `Quarantine entry ${quarantineId} not found`,
|
|
1324
|
+
recoverable: false,
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
/** Expose the quality gate for external configuration */
|
|
1329
|
+
getQualityGate() {
|
|
1330
|
+
return this.qualityGate;
|
|
1331
|
+
}
|
|
1332
|
+
// -------------------------------------------------------------------------
|
|
1333
|
+
// UTILITY METHODS
|
|
1334
|
+
// -------------------------------------------------------------------------
|
|
1335
|
+
priorityToNumber(priority) {
|
|
1336
|
+
const map = {
|
|
1337
|
+
low: 0,
|
|
1338
|
+
normal: 1,
|
|
1339
|
+
high: 2,
|
|
1340
|
+
critical: 3,
|
|
1341
|
+
};
|
|
1342
|
+
return map[priority] ?? 1;
|
|
1343
|
+
}
|
|
1344
|
+
timeoutPromise(ms) {
|
|
1345
|
+
return new Promise((_, reject) => {
|
|
1346
|
+
setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms);
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Register an agent with the swarm
|
|
1351
|
+
*/
|
|
1352
|
+
registerAgent(agentId, status = 'available') {
|
|
1353
|
+
this.agentRegistry.set(agentId, {
|
|
1354
|
+
agentId,
|
|
1355
|
+
status,
|
|
1356
|
+
currentTask: null,
|
|
1357
|
+
lastHeartbeat: new Date().toISOString(),
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Update agent status
|
|
1362
|
+
*/
|
|
1363
|
+
updateAgentStatus(agentId, status, currentTask) {
|
|
1364
|
+
const existing = this.agentRegistry.get(agentId);
|
|
1365
|
+
if (existing) {
|
|
1366
|
+
existing.status = status;
|
|
1367
|
+
existing.currentTask = currentTask ?? null;
|
|
1368
|
+
existing.lastHeartbeat = new Date().toISOString();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
exports.SwarmOrchestrator = SwarmOrchestrator;
|
|
1373
|
+
// ============================================================================
|
|
1374
|
+
// EXPORTS & MODULE INITIALIZATION
|
|
1375
|
+
// ============================================================================
|
|
1376
|
+
// Default export for OpenClaw skill loader (backward compatible)
|
|
1377
|
+
exports.default = SwarmOrchestrator;
|
|
1378
|
+
// Quality gate & validation exports
|
|
1379
|
+
var blackboard_validator_2 = require("./lib/blackboard-validator");
|
|
1380
|
+
Object.defineProperty(exports, "BlackboardValidator", { enumerable: true, get: function () { return blackboard_validator_2.BlackboardValidator; } });
|
|
1381
|
+
Object.defineProperty(exports, "QualityGateAgent", { enumerable: true, get: function () { return blackboard_validator_2.QualityGateAgent; } });
|
|
1382
|
+
// Adapter system re-exports for convenience
|
|
1383
|
+
var adapter_registry_2 = require("./adapters/adapter-registry");
|
|
1384
|
+
Object.defineProperty(exports, "AdapterRegistry", { enumerable: true, get: function () { return adapter_registry_2.AdapterRegistry; } });
|
|
1385
|
+
var base_adapter_1 = require("./adapters/base-adapter");
|
|
1386
|
+
Object.defineProperty(exports, "BaseAdapter", { enumerable: true, get: function () { return base_adapter_1.BaseAdapter; } });
|
|
1387
|
+
var openclaw_adapter_1 = require("./adapters/openclaw-adapter");
|
|
1388
|
+
Object.defineProperty(exports, "OpenClawAdapter", { enumerable: true, get: function () { return openclaw_adapter_1.OpenClawAdapter; } });
|
|
1389
|
+
var langchain_adapter_1 = require("./adapters/langchain-adapter");
|
|
1390
|
+
Object.defineProperty(exports, "LangChainAdapter", { enumerable: true, get: function () { return langchain_adapter_1.LangChainAdapter; } });
|
|
1391
|
+
var autogen_adapter_1 = require("./adapters/autogen-adapter");
|
|
1392
|
+
Object.defineProperty(exports, "AutoGenAdapter", { enumerable: true, get: function () { return autogen_adapter_1.AutoGenAdapter; } });
|
|
1393
|
+
var crewai_adapter_1 = require("./adapters/crewai-adapter");
|
|
1394
|
+
Object.defineProperty(exports, "CrewAIAdapter", { enumerable: true, get: function () { return crewai_adapter_1.CrewAIAdapter; } });
|
|
1395
|
+
var mcp_adapter_1 = require("./adapters/mcp-adapter");
|
|
1396
|
+
Object.defineProperty(exports, "MCPAdapter", { enumerable: true, get: function () { return mcp_adapter_1.MCPAdapter; } });
|
|
1397
|
+
var custom_adapter_1 = require("./adapters/custom-adapter");
|
|
1398
|
+
Object.defineProperty(exports, "CustomAdapter", { enumerable: true, get: function () { return custom_adapter_1.CustomAdapter; } });
|
|
1399
|
+
/**
|
|
1400
|
+
* Factory function for creating a configured SwarmOrchestrator instance.
|
|
1401
|
+
*
|
|
1402
|
+
* For plug-and-play with other agent systems, pass adapters:
|
|
1403
|
+
*
|
|
1404
|
+
* const orchestrator = createSwarmOrchestrator({
|
|
1405
|
+
* adapters: [{ adapter: new LangChainAdapter(), config: {} }],
|
|
1406
|
+
* });
|
|
1407
|
+
*/
|
|
1408
|
+
function createSwarmOrchestrator(config) {
|
|
1409
|
+
if (config) {
|
|
1410
|
+
const { adapters: adapterList, adapterRegistry, trustLevels, resourceProfiles, validationConfig, qualityThreshold, aiReviewCallback, ...rest } = config;
|
|
1411
|
+
Object.assign(CONFIG, rest);
|
|
1412
|
+
const registry = adapterRegistry ?? new adapter_registry_1.AdapterRegistry();
|
|
1413
|
+
const orchestrator = new SwarmOrchestrator(undefined, registry, {
|
|
1414
|
+
trustLevels,
|
|
1415
|
+
resourceProfiles,
|
|
1416
|
+
validationConfig,
|
|
1417
|
+
qualityThreshold,
|
|
1418
|
+
aiReviewCallback,
|
|
1419
|
+
});
|
|
1420
|
+
// Initialize adapters if provided
|
|
1421
|
+
if (adapterList) {
|
|
1422
|
+
Promise.all(adapterList.map(({ adapter, config: adapterConfig }) => orchestrator.addAdapter(adapter, adapterConfig ?? {}))).catch(err => console.error('[SwarmOrchestrator] Adapter init error:', err));
|
|
1423
|
+
}
|
|
1424
|
+
return orchestrator;
|
|
1425
|
+
}
|
|
1426
|
+
return new SwarmOrchestrator();
|
|
1427
|
+
}
|
|
1428
|
+
//# sourceMappingURL=index.js.map
|