claude-flow 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/LICENSE +21 -0
- package/README.md +612 -0
- package/bin/claude-flow +0 -0
- package/bin/claude-flow-simple +0 -0
- package/bin/claude-flow-typecheck +0 -0
- package/deno.json +84 -0
- package/package.json +45 -0
- package/scripts/check-links.ts +274 -0
- package/scripts/check-performance-regression.ts +168 -0
- package/scripts/claude-sparc.sh +562 -0
- package/scripts/coverage-report.ts +692 -0
- package/scripts/demo-task-system.ts +224 -0
- package/scripts/install.js +72 -0
- package/scripts/test-batch-tasks.ts +29 -0
- package/scripts/test-coordination-features.ts +238 -0
- package/scripts/test-mcp.ts +251 -0
- package/scripts/test-runner.ts +571 -0
- package/scripts/validate-examples.ts +288 -0
- package/src/cli/cli-core.ts +273 -0
- package/src/cli/commands/agent.ts +83 -0
- package/src/cli/commands/config.ts +442 -0
- package/src/cli/commands/help.ts +765 -0
- package/src/cli/commands/index.ts +963 -0
- package/src/cli/commands/mcp.ts +191 -0
- package/src/cli/commands/memory.ts +74 -0
- package/src/cli/commands/monitor.ts +403 -0
- package/src/cli/commands/session.ts +595 -0
- package/src/cli/commands/start.ts +156 -0
- package/src/cli/commands/status.ts +345 -0
- package/src/cli/commands/task.ts +79 -0
- package/src/cli/commands/workflow.ts +763 -0
- package/src/cli/completion.ts +553 -0
- package/src/cli/formatter.ts +310 -0
- package/src/cli/index.ts +211 -0
- package/src/cli/main.ts +23 -0
- package/src/cli/repl.ts +1050 -0
- package/src/cli/simple-cli.js +211 -0
- package/src/cli/simple-cli.ts +211 -0
- package/src/coordination/README.md +400 -0
- package/src/coordination/advanced-scheduler.ts +487 -0
- package/src/coordination/circuit-breaker.ts +366 -0
- package/src/coordination/conflict-resolution.ts +490 -0
- package/src/coordination/dependency-graph.ts +475 -0
- package/src/coordination/index.ts +63 -0
- package/src/coordination/manager.ts +460 -0
- package/src/coordination/messaging.ts +290 -0
- package/src/coordination/metrics.ts +585 -0
- package/src/coordination/resources.ts +322 -0
- package/src/coordination/scheduler.ts +390 -0
- package/src/coordination/work-stealing.ts +224 -0
- package/src/core/config.ts +627 -0
- package/src/core/event-bus.ts +186 -0
- package/src/core/json-persistence.ts +183 -0
- package/src/core/logger.ts +262 -0
- package/src/core/orchestrator-fixed.ts +312 -0
- package/src/core/orchestrator.ts +1234 -0
- package/src/core/persistence.ts +276 -0
- package/src/mcp/auth.ts +438 -0
- package/src/mcp/claude-flow-tools.ts +1280 -0
- package/src/mcp/load-balancer.ts +510 -0
- package/src/mcp/router.ts +240 -0
- package/src/mcp/server.ts +548 -0
- package/src/mcp/session-manager.ts +418 -0
- package/src/mcp/tools.ts +180 -0
- package/src/mcp/transports/base.ts +21 -0
- package/src/mcp/transports/http.ts +457 -0
- package/src/mcp/transports/stdio.ts +254 -0
- package/src/memory/backends/base.ts +22 -0
- package/src/memory/backends/markdown.ts +283 -0
- package/src/memory/backends/sqlite.ts +329 -0
- package/src/memory/cache.ts +238 -0
- package/src/memory/indexer.ts +238 -0
- package/src/memory/manager.ts +572 -0
- package/src/terminal/adapters/base.ts +29 -0
- package/src/terminal/adapters/native.ts +504 -0
- package/src/terminal/adapters/vscode.ts +340 -0
- package/src/terminal/manager.ts +308 -0
- package/src/terminal/pool.ts +271 -0
- package/src/terminal/session.ts +250 -0
- package/src/terminal/vscode-bridge.ts +242 -0
- package/src/utils/errors.ts +231 -0
- package/src/utils/helpers.ts +476 -0
- package/src/utils/types.ts +493 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for Claude-Flow using SQLite
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DB } from "https://deno.land/x/sqlite@v3.8/mod.ts";
|
|
6
|
+
import { join } from "https://deno.land/std@0.224.0/path/mod.ts";
|
|
7
|
+
import { ensureDir } from "https://deno.land/std@0.224.0/fs/mod.ts";
|
|
8
|
+
|
|
9
|
+
export interface PersistedAgent {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
status: string;
|
|
14
|
+
capabilities: string;
|
|
15
|
+
systemPrompt: string;
|
|
16
|
+
maxConcurrentTasks: number;
|
|
17
|
+
priority: number;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PersistedTask {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
description: string;
|
|
25
|
+
status: string;
|
|
26
|
+
priority: number;
|
|
27
|
+
dependencies: string;
|
|
28
|
+
metadata: string;
|
|
29
|
+
assignedAgent?: string;
|
|
30
|
+
progress: number;
|
|
31
|
+
error?: string;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
completedAt?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class PersistenceManager {
|
|
37
|
+
private db: DB;
|
|
38
|
+
private dbPath: string;
|
|
39
|
+
|
|
40
|
+
constructor(dataDir: string = "./memory") {
|
|
41
|
+
this.dbPath = join(dataDir, "claude-flow.db");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async initialize(): Promise<void> {
|
|
45
|
+
// Ensure directory exists
|
|
46
|
+
await ensureDir(join(this.dbPath, ".."));
|
|
47
|
+
|
|
48
|
+
// Open database
|
|
49
|
+
this.db = new DB(this.dbPath);
|
|
50
|
+
|
|
51
|
+
// Create tables if they don't exist
|
|
52
|
+
this.createTables();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private createTables(): void {
|
|
56
|
+
// Agents table
|
|
57
|
+
this.db.execute(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
type TEXT NOT NULL,
|
|
61
|
+
name TEXT NOT NULL,
|
|
62
|
+
status TEXT NOT NULL,
|
|
63
|
+
capabilities TEXT NOT NULL,
|
|
64
|
+
system_prompt TEXT NOT NULL,
|
|
65
|
+
max_concurrent_tasks INTEGER NOT NULL,
|
|
66
|
+
priority INTEGER NOT NULL,
|
|
67
|
+
created_at INTEGER NOT NULL
|
|
68
|
+
)
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
// Tasks table
|
|
72
|
+
this.db.execute(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
type TEXT NOT NULL,
|
|
76
|
+
description TEXT NOT NULL,
|
|
77
|
+
status TEXT NOT NULL,
|
|
78
|
+
priority INTEGER NOT NULL,
|
|
79
|
+
dependencies TEXT NOT NULL,
|
|
80
|
+
metadata TEXT NOT NULL,
|
|
81
|
+
assigned_agent TEXT,
|
|
82
|
+
progress INTEGER DEFAULT 0,
|
|
83
|
+
error TEXT,
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
completed_at INTEGER
|
|
86
|
+
)
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
// Sessions table for terminal sessions
|
|
90
|
+
this.db.execute(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
92
|
+
id TEXT PRIMARY KEY,
|
|
93
|
+
agent_id TEXT NOT NULL,
|
|
94
|
+
terminal_id TEXT NOT NULL,
|
|
95
|
+
status TEXT NOT NULL,
|
|
96
|
+
created_at INTEGER NOT NULL,
|
|
97
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
98
|
+
)
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Agent operations
|
|
103
|
+
async saveAgent(agent: PersistedAgent): Promise<void> {
|
|
104
|
+
this.db.query(
|
|
105
|
+
`INSERT OR REPLACE INTO agents
|
|
106
|
+
(id, type, name, status, capabilities, system_prompt, max_concurrent_tasks, priority, created_at)
|
|
107
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
108
|
+
[
|
|
109
|
+
agent.id,
|
|
110
|
+
agent.type,
|
|
111
|
+
agent.name,
|
|
112
|
+
agent.status,
|
|
113
|
+
agent.capabilities,
|
|
114
|
+
agent.systemPrompt,
|
|
115
|
+
agent.maxConcurrentTasks,
|
|
116
|
+
agent.priority,
|
|
117
|
+
agent.createdAt,
|
|
118
|
+
]
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getAgent(id: string): Promise<PersistedAgent | null> {
|
|
123
|
+
const rows = this.db.query<[string, string, string, string, string, string, number, number, number]>(
|
|
124
|
+
"SELECT * FROM agents WHERE id = ?",
|
|
125
|
+
[id]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (rows.length === 0) return null;
|
|
129
|
+
|
|
130
|
+
const [id_, type, name, status, capabilities, systemPrompt, maxConcurrentTasks, priority, createdAt] = rows[0];
|
|
131
|
+
return {
|
|
132
|
+
id: id_,
|
|
133
|
+
type,
|
|
134
|
+
name,
|
|
135
|
+
status,
|
|
136
|
+
capabilities,
|
|
137
|
+
systemPrompt,
|
|
138
|
+
maxConcurrentTasks,
|
|
139
|
+
priority,
|
|
140
|
+
createdAt,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getActiveAgents(): Promise<PersistedAgent[]> {
|
|
145
|
+
const rows = this.db.query<[string, string, string, string, string, string, number, number, number]>(
|
|
146
|
+
"SELECT * FROM agents WHERE status IN ('active', 'idle') ORDER BY created_at DESC"
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return rows.map(([id, type, name, status, capabilities, systemPrompt, maxConcurrentTasks, priority, createdAt]) => ({
|
|
150
|
+
id,
|
|
151
|
+
type,
|
|
152
|
+
name,
|
|
153
|
+
status,
|
|
154
|
+
capabilities,
|
|
155
|
+
systemPrompt,
|
|
156
|
+
maxConcurrentTasks,
|
|
157
|
+
priority,
|
|
158
|
+
createdAt,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async updateAgentStatus(id: string, status: string): Promise<void> {
|
|
163
|
+
this.db.query("UPDATE agents SET status = ? WHERE id = ?", [status, id]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Task operations
|
|
167
|
+
async saveTask(task: PersistedTask): Promise<void> {
|
|
168
|
+
this.db.query(
|
|
169
|
+
`INSERT OR REPLACE INTO tasks
|
|
170
|
+
(id, type, description, status, priority, dependencies, metadata, assigned_agent, progress, error, created_at, completed_at)
|
|
171
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
172
|
+
[
|
|
173
|
+
task.id,
|
|
174
|
+
task.type,
|
|
175
|
+
task.description,
|
|
176
|
+
task.status,
|
|
177
|
+
task.priority,
|
|
178
|
+
task.dependencies,
|
|
179
|
+
task.metadata,
|
|
180
|
+
task.assignedAgent || null,
|
|
181
|
+
task.progress,
|
|
182
|
+
task.error || null,
|
|
183
|
+
task.createdAt,
|
|
184
|
+
task.completedAt || null,
|
|
185
|
+
]
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getTask(id: string): Promise<PersistedTask | null> {
|
|
190
|
+
const rows = this.db.query<[string, string, string, string, number, string, string, string | null, number, string | null, number, number | null]>(
|
|
191
|
+
"SELECT * FROM tasks WHERE id = ?",
|
|
192
|
+
[id]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (rows.length === 0) return null;
|
|
196
|
+
|
|
197
|
+
const [id_, type, description, status, priority, dependencies, metadata, assignedAgent, progress, error, createdAt, completedAt] = rows[0];
|
|
198
|
+
return {
|
|
199
|
+
id: id_,
|
|
200
|
+
type,
|
|
201
|
+
description,
|
|
202
|
+
status,
|
|
203
|
+
priority,
|
|
204
|
+
dependencies,
|
|
205
|
+
metadata,
|
|
206
|
+
assignedAgent: assignedAgent || undefined,
|
|
207
|
+
progress,
|
|
208
|
+
error: error || undefined,
|
|
209
|
+
createdAt,
|
|
210
|
+
completedAt: completedAt || undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getActiveTasks(): Promise<PersistedTask[]> {
|
|
215
|
+
const rows = this.db.query<[string, string, string, string, number, string, string, string | null, number, string | null, number, number | null]>(
|
|
216
|
+
"SELECT * FROM tasks WHERE status IN ('pending', 'in_progress', 'assigned') ORDER BY priority DESC, created_at ASC"
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return rows.map(([id, type, description, status, priority, dependencies, metadata, assignedAgent, progress, error, createdAt, completedAt]) => ({
|
|
220
|
+
id,
|
|
221
|
+
type,
|
|
222
|
+
description,
|
|
223
|
+
status,
|
|
224
|
+
priority,
|
|
225
|
+
dependencies,
|
|
226
|
+
metadata,
|
|
227
|
+
assignedAgent: assignedAgent || undefined,
|
|
228
|
+
progress,
|
|
229
|
+
error: error || undefined,
|
|
230
|
+
createdAt,
|
|
231
|
+
completedAt: completedAt || undefined,
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async updateTaskStatus(id: string, status: string, assignedAgent?: string): Promise<void> {
|
|
236
|
+
if (assignedAgent) {
|
|
237
|
+
this.db.query(
|
|
238
|
+
"UPDATE tasks SET status = ?, assigned_agent = ? WHERE id = ?",
|
|
239
|
+
[status, assignedAgent, id]
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
this.db.query("UPDATE tasks SET status = ? WHERE id = ?", [status, id]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async updateTaskProgress(id: string, progress: number): Promise<void> {
|
|
247
|
+
this.db.query("UPDATE tasks SET progress = ? WHERE id = ?", [progress, id]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Statistics
|
|
251
|
+
async getStats(): Promise<{
|
|
252
|
+
totalAgents: number;
|
|
253
|
+
activeAgents: number;
|
|
254
|
+
totalTasks: number;
|
|
255
|
+
pendingTasks: number;
|
|
256
|
+
completedTasks: number;
|
|
257
|
+
}> {
|
|
258
|
+
const [totalAgents] = this.db.query<[number]>("SELECT COUNT(*) FROM agents")[0];
|
|
259
|
+
const [activeAgents] = this.db.query<[number]>("SELECT COUNT(*) FROM agents WHERE status IN ('active', 'idle')")[0];
|
|
260
|
+
const [totalTasks] = this.db.query<[number]>("SELECT COUNT(*) FROM tasks")[0];
|
|
261
|
+
const [pendingTasks] = this.db.query<[number]>("SELECT COUNT(*) FROM tasks WHERE status IN ('pending', 'in_progress', 'assigned')")[0];
|
|
262
|
+
const [completedTasks] = this.db.query<[number]>("SELECT COUNT(*) FROM tasks WHERE status = 'completed'")[0];
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
totalAgents,
|
|
266
|
+
activeAgents,
|
|
267
|
+
totalTasks,
|
|
268
|
+
pendingTasks,
|
|
269
|
+
completedTasks,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
close(): void {
|
|
274
|
+
this.db.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/mcp/auth.ts
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication and authorization for MCP
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MCPAuthConfig, MCPSession } from '../utils/types.ts';
|
|
6
|
+
import { ILogger } from '../core/logger.ts';
|
|
7
|
+
import { MCPError } from '../utils/errors.ts';
|
|
8
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
export interface IAuthManager {
|
|
11
|
+
authenticate(credentials: unknown): Promise<AuthResult>;
|
|
12
|
+
authorize(session: MCPSession, permission: string): boolean;
|
|
13
|
+
validateToken(token: string): Promise<TokenValidation>;
|
|
14
|
+
generateToken(userId: string, permissions: string[]): Promise<string>;
|
|
15
|
+
revokeToken(token: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AuthResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
user?: string;
|
|
21
|
+
permissions?: string[];
|
|
22
|
+
token?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TokenValidation {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
user?: string;
|
|
29
|
+
permissions?: string[];
|
|
30
|
+
expiresAt?: Date;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Authentication manager implementation
|
|
36
|
+
*/
|
|
37
|
+
export class AuthManager implements IAuthManager {
|
|
38
|
+
private revokedTokens = new Set<string>();
|
|
39
|
+
private tokenStore = new Map<string, {
|
|
40
|
+
user: string;
|
|
41
|
+
permissions: string[];
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
expiresAt: Date;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private config: MCPAuthConfig,
|
|
48
|
+
private logger: ILogger,
|
|
49
|
+
) {
|
|
50
|
+
// Start token cleanup timer
|
|
51
|
+
if (config.enabled) {
|
|
52
|
+
setInterval(() => {
|
|
53
|
+
this.cleanupExpiredTokens();
|
|
54
|
+
}, 300000); // Clean up every 5 minutes
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async authenticate(credentials: unknown): Promise<AuthResult> {
|
|
59
|
+
if (!this.config.enabled) {
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
user: 'anonymous',
|
|
63
|
+
permissions: ['*'],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.logger.debug('Authenticating credentials', {
|
|
68
|
+
method: this.config.method,
|
|
69
|
+
hasCredentials: !!credentials,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
switch (this.config.method) {
|
|
74
|
+
case 'token':
|
|
75
|
+
return await this.authenticateToken(credentials);
|
|
76
|
+
case 'basic':
|
|
77
|
+
return await this.authenticateBasic(credentials);
|
|
78
|
+
case 'oauth':
|
|
79
|
+
return await this.authenticateOAuth(credentials);
|
|
80
|
+
default:
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: `Unsupported authentication method: ${this.config.method}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.error('Authentication error', error);
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: error instanceof Error ? error.message : 'Authentication failed',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
authorize(session: MCPSession, permission: string): boolean {
|
|
96
|
+
if (!this.config.enabled || !session.authenticated) {
|
|
97
|
+
return !this.config.enabled; // If auth disabled, allow all
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const permissions = session.authData?.permissions || [];
|
|
101
|
+
|
|
102
|
+
// Check for wildcard permission
|
|
103
|
+
if (permissions.includes('*')) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for exact permission match
|
|
108
|
+
if (permissions.includes(permission)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check for prefix-based permissions (e.g., "tools.*" matches "tools.list")
|
|
113
|
+
for (const perm of permissions) {
|
|
114
|
+
if (perm.endsWith('*') && permission.startsWith(perm.slice(0, -1))) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.logger.warn('Authorization denied', {
|
|
120
|
+
sessionId: session.id,
|
|
121
|
+
user: session.authData?.user,
|
|
122
|
+
permission,
|
|
123
|
+
userPermissions: permissions,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async validateToken(token: string): Promise<TokenValidation> {
|
|
130
|
+
if (this.revokedTokens.has(token)) {
|
|
131
|
+
return {
|
|
132
|
+
valid: false,
|
|
133
|
+
error: 'Token has been revoked',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tokenData = this.tokenStore.get(token);
|
|
138
|
+
if (!tokenData) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
error: 'Invalid token',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (tokenData.expiresAt < new Date()) {
|
|
146
|
+
this.tokenStore.delete(token);
|
|
147
|
+
return {
|
|
148
|
+
valid: false,
|
|
149
|
+
error: 'Token has expired',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
valid: true,
|
|
155
|
+
user: tokenData.user,
|
|
156
|
+
permissions: tokenData.permissions,
|
|
157
|
+
expiresAt: tokenData.expiresAt,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async generateToken(userId: string, permissions: string[]): Promise<string> {
|
|
162
|
+
const token = this.createSecureToken();
|
|
163
|
+
const now = new Date();
|
|
164
|
+
const expiresAt = new Date(now.getTime() + (this.config.sessionTimeout || 3600000));
|
|
165
|
+
|
|
166
|
+
this.tokenStore.set(token, {
|
|
167
|
+
user: userId,
|
|
168
|
+
permissions,
|
|
169
|
+
createdAt: now,
|
|
170
|
+
expiresAt,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.logger.info('Token generated', {
|
|
174
|
+
userId,
|
|
175
|
+
permissions,
|
|
176
|
+
expiresAt,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return token;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async revokeToken(token: string): Promise<void> {
|
|
183
|
+
this.revokedTokens.add(token);
|
|
184
|
+
this.tokenStore.delete(token);
|
|
185
|
+
this.logger.info('Token revoked', { token: token.substring(0, 8) + '...' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async authenticateToken(credentials: unknown): Promise<AuthResult> {
|
|
189
|
+
const token = this.extractToken(credentials);
|
|
190
|
+
if (!token) {
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
error: 'Token not provided',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check if it's a stored token (generated by us)
|
|
198
|
+
const validation = await this.validateToken(token);
|
|
199
|
+
if (validation.valid) {
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
user: validation.user!,
|
|
203
|
+
permissions: validation.permissions!,
|
|
204
|
+
token,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check against configured static tokens
|
|
209
|
+
if (this.config.tokens && this.config.tokens.length > 0) {
|
|
210
|
+
const isValid = this.config.tokens.some((validToken) => {
|
|
211
|
+
return this.timingSafeEqual(token, validToken);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (isValid) {
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
user: 'token-user',
|
|
218
|
+
permissions: ['*'], // Static tokens get all permissions
|
|
219
|
+
token,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
error: 'Invalid token',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async authenticateBasic(credentials: unknown): Promise<AuthResult> {
|
|
231
|
+
const { username, password } = this.extractBasicAuth(credentials);
|
|
232
|
+
if (!username || !password) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: 'Username and password required',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!this.config.users || this.config.users.length === 0) {
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: 'No users configured',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const user = this.config.users.find((u) => u.username === username);
|
|
247
|
+
if (!user) {
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
error: 'Invalid username or password',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Verify password
|
|
255
|
+
const isValidPassword = this.verifyPassword(password, user.password);
|
|
256
|
+
if (!isValidPassword) {
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
error: 'Invalid username or password',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Generate a session token
|
|
264
|
+
const token = await this.generateToken(username, user.permissions);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
success: true,
|
|
268
|
+
user: username,
|
|
269
|
+
permissions: user.permissions,
|
|
270
|
+
token,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async authenticateOAuth(credentials: unknown): Promise<AuthResult> {
|
|
275
|
+
// TODO: Implement OAuth authentication
|
|
276
|
+
// This would typically involve:
|
|
277
|
+
// 1. Validating JWT tokens
|
|
278
|
+
// 2. Checking token expiration
|
|
279
|
+
// 3. Extracting user info and permissions from token claims
|
|
280
|
+
|
|
281
|
+
this.logger.warn('OAuth authentication not yet implemented');
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: 'OAuth authentication not implemented',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private extractToken(credentials: unknown): string | null {
|
|
289
|
+
if (typeof credentials === 'string') {
|
|
290
|
+
return credentials;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (typeof credentials === 'object' && credentials !== null) {
|
|
294
|
+
const creds = credentials as Record<string, unknown>;
|
|
295
|
+
|
|
296
|
+
if (typeof creds.token === 'string') {
|
|
297
|
+
return creds.token;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof creds.authorization === 'string') {
|
|
301
|
+
const match = creds.authorization.match(/^Bearer\s+(.+)$/i);
|
|
302
|
+
return match ? match[1] : null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private extractBasicAuth(credentials: unknown): { username?: string; password?: string } {
|
|
310
|
+
if (typeof credentials === 'object' && credentials !== null) {
|
|
311
|
+
const creds = credentials as Record<string, unknown>;
|
|
312
|
+
|
|
313
|
+
if (typeof creds.username === 'string' && typeof creds.password === 'string') {
|
|
314
|
+
return {
|
|
315
|
+
username: creds.username,
|
|
316
|
+
password: creds.password,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (typeof creds.authorization === 'string') {
|
|
321
|
+
const match = creds.authorization.match(/^Basic\s+(.+)$/i);
|
|
322
|
+
if (match) {
|
|
323
|
+
try {
|
|
324
|
+
const decoded = atob(match[1]);
|
|
325
|
+
const colonIndex = decoded.indexOf(':');
|
|
326
|
+
if (colonIndex >= 0) {
|
|
327
|
+
return {
|
|
328
|
+
username: decoded.substring(0, colonIndex),
|
|
329
|
+
password: decoded.substring(colonIndex + 1),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// Invalid base64
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private verifyPassword(providedPassword: string, storedPassword: string): boolean {
|
|
343
|
+
// For now, using simple hash comparison
|
|
344
|
+
// In production, use proper password hashing like bcrypt
|
|
345
|
+
const hashedProvided = this.hashPassword(providedPassword);
|
|
346
|
+
const hashedStored = this.hashPassword(storedPassword);
|
|
347
|
+
|
|
348
|
+
return this.timingSafeEqual(hashedProvided, hashedStored);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private hashPassword(password: string): string {
|
|
352
|
+
return createHash('sha256').update(password).digest('hex');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private timingSafeEqual(a: string, b: string): boolean {
|
|
356
|
+
const encoder = new TextEncoder();
|
|
357
|
+
const bufferA = encoder.encode(a);
|
|
358
|
+
const bufferB = encoder.encode(b);
|
|
359
|
+
|
|
360
|
+
if (bufferA.length !== bufferB.length) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return timingSafeEqual(bufferA, bufferB);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private createSecureToken(): string {
|
|
368
|
+
// Generate a secure random token
|
|
369
|
+
const timestamp = Date.now().toString(36);
|
|
370
|
+
const random1 = Math.random().toString(36).substring(2, 15);
|
|
371
|
+
const random2 = Math.random().toString(36).substring(2, 15);
|
|
372
|
+
const hash = createHash('sha256')
|
|
373
|
+
.update(`${timestamp}${random1}${random2}`)
|
|
374
|
+
.digest('hex')
|
|
375
|
+
.substring(0, 32);
|
|
376
|
+
|
|
377
|
+
return `mcp_${timestamp}_${hash}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private cleanupExpiredTokens(): void {
|
|
381
|
+
const now = new Date();
|
|
382
|
+
let cleaned = 0;
|
|
383
|
+
|
|
384
|
+
for (const [token, data] of this.tokenStore.entries()) {
|
|
385
|
+
if (data.expiresAt < now) {
|
|
386
|
+
this.tokenStore.delete(token);
|
|
387
|
+
cleaned++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (cleaned > 0) {
|
|
392
|
+
this.logger.debug('Cleaned up expired tokens', { count: cleaned });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Permission constants for common operations
|
|
399
|
+
*/
|
|
400
|
+
export const Permissions = {
|
|
401
|
+
// System operations
|
|
402
|
+
SYSTEM_INFO: 'system.info',
|
|
403
|
+
SYSTEM_HEALTH: 'system.health',
|
|
404
|
+
SYSTEM_METRICS: 'system.metrics',
|
|
405
|
+
|
|
406
|
+
// Tool operations
|
|
407
|
+
TOOLS_LIST: 'tools.list',
|
|
408
|
+
TOOLS_INVOKE: 'tools.invoke',
|
|
409
|
+
TOOLS_DESCRIBE: 'tools.describe',
|
|
410
|
+
|
|
411
|
+
// Agent operations
|
|
412
|
+
AGENTS_LIST: 'agents.list',
|
|
413
|
+
AGENTS_SPAWN: 'agents.spawn',
|
|
414
|
+
AGENTS_TERMINATE: 'agents.terminate',
|
|
415
|
+
AGENTS_INFO: 'agents.info',
|
|
416
|
+
|
|
417
|
+
// Task operations
|
|
418
|
+
TASKS_LIST: 'tasks.list',
|
|
419
|
+
TASKS_CREATE: 'tasks.create',
|
|
420
|
+
TASKS_CANCEL: 'tasks.cancel',
|
|
421
|
+
TASKS_STATUS: 'tasks.status',
|
|
422
|
+
|
|
423
|
+
// Memory operations
|
|
424
|
+
MEMORY_READ: 'memory.read',
|
|
425
|
+
MEMORY_WRITE: 'memory.write',
|
|
426
|
+
MEMORY_QUERY: 'memory.query',
|
|
427
|
+
MEMORY_DELETE: 'memory.delete',
|
|
428
|
+
|
|
429
|
+
// Administrative operations
|
|
430
|
+
ADMIN_CONFIG: 'admin.config',
|
|
431
|
+
ADMIN_LOGS: 'admin.logs',
|
|
432
|
+
ADMIN_SESSIONS: 'admin.sessions',
|
|
433
|
+
|
|
434
|
+
// Wildcard permission
|
|
435
|
+
ALL: '*',
|
|
436
|
+
} as const;
|
|
437
|
+
|
|
438
|
+
export type Permission = typeof Permissions[keyof typeof Permissions];
|