claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
387 lines (377 loc) • 15.7 kB
JavaScript
/**
* Agent Token Management CLI
* Generate, register, and manage agent tokens for MCP authentication
*/ const crypto = require('crypto');
const Redis = require('redis');
const fs = require('fs').promises;
const path = require('path');
let AgentTokenManager = class AgentTokenManager {
constructor(options = {}){
// Support CFN standard variables with fallback to legacy MCP_REDIS_URL
// FIX: Default to 'localhost' for CLI mode (host execution), not 'cfn-redis' (Docker)
const redisHost = process.env.CFN_REDIS_HOST || 'localhost';
const redisPort = process.env.CFN_REDIS_PORT || '6379';
const defaultUrl = `redis://${redisHost}:${redisPort}`;
this.redisUrl = options.redisUrl || process.env.CFN_REDIS_URL || process.env.MCP_REDIS_URL || defaultUrl;
this.redis = null;
this.agentConfigPath = options.agentConfigPath || './config/agent-whitelist.json';
this.defaultExpiry = options.defaultExpiry || '24h';
}
async initialize() {
try {
this.redis = Redis.createClient({
url: this.redisUrl
});
await this.redis.connect();
console.log('Connected to Redis for token management');
} catch (error) {
console.error('Failed to connect to Redis:', error);
throw error;
}
}
async loadAgentConfig() {
try {
const configPath = path.resolve(this.agentConfigPath);
const config = await fs.readFile(configPath, 'utf8');
return JSON.parse(config);
} catch (error) {
console.error('Failed to load agent config:', error);
throw error;
}
}
generateToken() {
return crypto.randomBytes(32).toString('hex');
}
parseExpiry(expiry) {
if (typeof expiry === 'number') {
return expiry;
}
const match = expiry.match(/^(\d+)([smhd])$/);
if (!match) {
return 86400; // Default to 24 hours
}
const value = parseInt(match[1]);
const unit = match[2];
const multipliers = {
s: 1,
m: 60,
h: 3600,
d: 86400
};
return value * (multipliers[unit] || 86400);
}
async registerAgentToken(agentType, options = {}) {
try {
const config = await this.loadAgentConfig();
const agentConfig = config.agents.find((a)=>a.type === agentType);
if (!agentConfig) {
throw new Error(`Unknown agent type: ${agentType}. Available types: ${config.agents.map((a)=>a.type).join(', ')}`);
}
const token = this.generateToken();
const expiresIn = options.expiresIn || this.defaultExpiry;
const expiresAt = Date.now() + this.parseExpiry(expiresIn) * 1000;
const tokenData = {
token,
agentType,
displayName: agentConfig.displayName,
skills: agentConfig.skills,
allowedMcpServers: agentConfig.allowedMcpServers,
resourceLimits: agentConfig.resourceLimits,
expiresAt,
createdAt: Date.now(),
createdBy: options.createdBy || 'cli',
description: options.description || `Token for ${agentConfig.displayName}`
};
// Store in Redis
const key = `mcp:agent:${agentType}:${token}`;
const value = JSON.stringify(tokenData);
const ttlSeconds = this.parseExpiry(expiresIn);
await this.redis.setEx(key, ttlSeconds, value);
console.log(`✅ Token registered successfully!`);
console.log(` Agent Type: ${agentConfig.displayName} (${agentType})`);
console.log(` Token: ${token}`);
console.log(` Skills: ${agentConfig.skills.join(', ')}`);
console.log(` Allowed MCP Servers: ${agentConfig.allowedMcpServers.join(', ')}`);
console.log(` Expires: ${new Date(expiresAt).toISOString()}`);
console.log(` Memory Limit: ${agentConfig.resourceLimits.maxMemoryMB}MB`);
console.log(` Rate Limit: ${agentConfig.resourceLimits.maxRequestsPerMinute}/min`);
return tokenData;
} catch (error) {
console.error('❌ Failed to register token:', error.message);
throw error;
}
}
async listActiveTokens(agentType = null) {
try {
const pattern = agentType ? `mcp:agent:${agentType}:*` : 'mcp:agent:*';
const keys = await this.redis.keys(pattern);
const tokens = [];
for (const key of keys){
const value = await this.redis.get(key);
if (value) {
const tokenData = JSON.parse(value);
tokens.push({
...tokenData,
key,
status: tokenData.expiresAt > Date.now() ? 'active' : 'expired'
});
}
}
if (tokens.length === 0) {
console.log('No active tokens found');
return [];
}
console.log(`\nActive Tokens (${tokens.length}):\n`);
console.log('Agent Type | Token | Expires At | Status');
console.log('-------------------|------------------------------------|------------------------------|--------');
for (const token of tokens){
const expiresAt = new Date(token.expiresAt).toISOString();
const tokenShort = token.token.substring(0, 32);
console.log(`${token.agentType.padEnd(17)} | ${tokenShort} | ${expiresAt} | ${token.status}`);
}
return tokens;
} catch (error) {
console.error('❌ Failed to list tokens:', error.message);
throw error;
}
}
async revokeToken(agentType, token) {
try {
const key = `mcp:agent:${agentType}:${token}`;
const exists = await this.redis.exists(key);
if (!exists) {
throw new Error(`Token not found for agent ${agentType}`);
}
await this.redis.del(key);
console.log(`✅ Token revoked successfully for agent ${agentType}`);
console.log(` Token: ${token}`);
} catch (error) {
console.error('❌ Failed to revoke token:', error.message);
throw error;
}
}
async revokeAllTokens(agentType = null) {
try {
const pattern = agentType ? `mcp:agent:${agentType}:*` : 'mcp:agent:*';
const keys = await this.redis.keys(pattern);
if (keys.length === 0) {
console.log('No tokens found to revoke');
return 0;
}
await this.redis.del(keys);
console.log(`✅ Revoked ${keys.length} tokens${agentType ? ` for agent ${agentType}` : ''}`);
return keys.length;
} catch (error) {
console.error('❌ Failed to revoke tokens:', error.message);
throw error;
}
}
async validateToken(agentType, token) {
try {
const key = `mcp:agent:${agentType}:${token}`;
const value = await this.redis.get(key);
if (!value) {
return {
valid: false,
reason: 'Token not found'
};
}
const tokenData = JSON.parse(value);
if (Date.now() > tokenData.expiresAt) {
await this.redis.del(key);
return {
valid: false,
reason: 'Token expired'
};
}
return {
valid: true,
tokenData
};
} catch (error) {
return {
valid: false,
reason: error.message
};
}
}
async getAgentInfo(agentType) {
try {
const config = await this.loadAgentConfig();
const agentConfig = config.agents.find((a)=>a.type === agentType);
if (!agentConfig) {
throw new Error(`Unknown agent type: ${agentType}`);
}
console.log(`\nAgent Information: ${agentConfig.displayName}`);
console.log('='.repeat(50));
console.log(`Type: ${agentConfig.type}`);
console.log(`Skills: ${agentConfig.skills.join(', ')}`);
console.log(`Allowed MCP Servers: ${agentConfig.allowedMcpServers.join(', ')}`);
console.log(`Memory Limit: ${agentConfig.resourceLimits.maxMemoryMB}MB`);
console.log(`Rate Limit: ${agentConfig.resourceLimits.maxRequestsPerMinute}/min`);
console.log(`Max Concurrent: ${agentConfig.resourceLimits.maxConcurrentRequests}`);
console.log(`Description: ${agentConfig.description}`);
return agentConfig;
} catch (error) {
console.error('❌ Failed to get agent info:', error.message);
throw error;
}
}
async listAgentTypes() {
try {
const config = await this.loadAgentConfig();
console.log(`\nAvailable Agent Types (${config.agents.length}):\n`);
console.log('Agent Type | Display Name | Skills Count');
console.log('------------------------|--------------------------------|-------------');
for (const agent of config.agents){
const type = agent.type.padEnd(22);
const name = agent.displayName.padEnd(32);
const skills = agent.skills.length;
console.log(`${type} | ${name} | ${skills}`);
}
return config.agents.map((a)=>a.type);
} catch (error) {
console.error('❌ Failed to list agent types:', error.message);
throw error;
}
}
async shutdown() {
if (this.redis) {
await this.redis.quit();
}
}
};
// CLI interface
async function main() {
const args = process.argv.slice(2);
const command = args[0];
if (!command) {
console.log(`
Agent Token Management CLI
Usage: node agent-token-manager.js <command> [options]
Commands:
register <agent-type> [options] Register a new token for an agent
list [agent-type] List active tokens
revoke <agent-type> <token> Revoke a specific token
revoke-all [agent-type] Revoke all tokens (optionally for specific agent)
validate <agent-type> <token> Validate a token
info <agent-type> Show agent information
types List available agent types
Options for 'register' command:
--expires-in <duration> Token expiry (e.g., 1h, 30m, 7d) [default: 24h]
--description <text> Token description
--created-by <identifier> Creator identifier
Examples:
# Register token for frontend engineer
node agent-token-manager.js register react-frontend-engineer
# Register token with custom expiry
node agent-token-manager.js register backend-developer --expires-in 2h
# List all active tokens
node agent-token-manager.js list
# List tokens for specific agent
node agent-token-manager.js list react-frontend-engineer
# Revoke a specific token
node agent-token-manager.js revoke react-frontend-engineer abc123...
# Get agent information
node agent-token-manager.js info security-specialist
`);
process.exit(0);
}
const tokenManager = new AgentTokenManager();
try {
await tokenManager.initialize();
switch(command){
case 'register':
{
const agentType = args[1];
if (!agentType) {
throw new Error('Agent type is required for register command');
}
const options = {};
for(let i = 2; i < args.length; i++){
if (args[i] === '--expires-in') {
options.expiresIn = args[++i];
} else if (args[i] === '--description') {
options.description = args[++i];
} else if (args[i] === '--created-by') {
options.createdBy = args[++i];
}
}
await tokenManager.registerAgentToken(agentType, options);
break;
}
case 'list':
{
const agentType = args[1];
await tokenManager.listActiveTokens(agentType);
break;
}
case 'revoke':
{
const agentType = args[1];
const token = args[2];
if (!agentType || !token) {
throw new Error('Agent type and token are required for revoke command');
}
await tokenManager.revokeToken(agentType, token);
break;
}
case 'revoke-all':
{
const agentType = args[1];
const count = await tokenManager.revokeAllTokens(agentType);
console.log(`Revoked ${count} tokens`);
break;
}
case 'validate':
{
const agentType = args[1];
const token = args[2];
if (!agentType || !token) {
throw new Error('Agent type and token are required for validate command');
}
const result = await tokenManager.validateToken(agentType, token);
if (result.valid) {
console.log('✅ Token is valid');
console.log(` Agent Type: ${result.tokenData.agentType}`);
console.log(` Skills: ${result.tokenData.skills.join(', ')}`);
console.log(` Expires: ${new Date(result.tokenData.expiresAt).toISOString()}`);
} else {
console.log('❌ Token is invalid');
console.log(` Reason: ${result.reason}`);
}
break;
}
case 'info':
{
const agentType = args[1];
if (!agentType) {
throw new Error('Agent type is required for info command');
}
await tokenManager.getAgentInfo(agentType);
break;
}
case 'types':
{
await tokenManager.listAgentTypes();
break;
}
default:
throw new Error(`Unknown command: ${command}`);
}
} catch (error) {
console.error('❌ Error:', error.message);
process.exit(1);
} finally{
await tokenManager.shutdown();
}
}
if (require.main === module) {
main().catch((error)=>{
console.error('Fatal error:', error);
process.exit(1);
});
}
module.exports = AgentTokenManager;
//# sourceMappingURL=agent-token-manager.js.map