@tjamescouch/agentchat 0.1.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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * AgentChat Deploy Configuration
3
+ * Parser for deploy.yaml configuration files
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import yaml from 'js-yaml';
8
+
9
+ /**
10
+ * Default configuration values
11
+ */
12
+ export const DEFAULT_CONFIG = {
13
+ provider: 'docker',
14
+ port: 6667,
15
+ host: '0.0.0.0',
16
+ name: 'agentchat',
17
+ logMessages: false,
18
+ volumes: false,
19
+ healthCheck: true,
20
+ tls: null,
21
+ network: null
22
+ };
23
+
24
+ /**
25
+ * Load and parse deploy.yaml configuration
26
+ * @param {string} configPath - Path to configuration file
27
+ * @returns {Promise<object>} Validated configuration object
28
+ */
29
+ export async function loadConfig(configPath) {
30
+ const content = await fs.readFile(configPath, 'utf-8');
31
+ const parsed = yaml.load(content);
32
+ return validateConfig(parsed);
33
+ }
34
+
35
+ /**
36
+ * Validate configuration object
37
+ * @param {object} config - Raw configuration object
38
+ * @returns {object} Validated and merged configuration
39
+ * @throws {Error} If configuration is invalid
40
+ */
41
+ export function validateConfig(config) {
42
+ if (!config || typeof config !== 'object') {
43
+ throw new Error('Configuration must be an object');
44
+ }
45
+
46
+ const result = { ...DEFAULT_CONFIG, ...config };
47
+
48
+ // Validate provider
49
+ if (!['docker', 'akash'].includes(result.provider)) {
50
+ throw new Error(`Invalid provider: ${result.provider}. Must be 'docker' or 'akash'`);
51
+ }
52
+
53
+ // Validate port
54
+ const port = parseInt(result.port);
55
+ if (isNaN(port) || port < 1 || port > 65535) {
56
+ throw new Error(`Invalid port: ${result.port}. Must be between 1 and 65535`);
57
+ }
58
+ result.port = port;
59
+
60
+ // Validate host
61
+ if (typeof result.host !== 'string' || result.host.length === 0) {
62
+ throw new Error('Invalid host: must be a non-empty string');
63
+ }
64
+
65
+ // Validate name
66
+ if (typeof result.name !== 'string' || result.name.length === 0) {
67
+ throw new Error('Invalid name: must be a non-empty string');
68
+ }
69
+ // Docker container name must be alphanumeric with dashes/underscores
70
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(result.name)) {
71
+ throw new Error('Invalid name: must start with alphanumeric and contain only alphanumeric, dash, underscore');
72
+ }
73
+
74
+ // Validate TLS config
75
+ if (result.tls) {
76
+ if (typeof result.tls !== 'object') {
77
+ throw new Error('TLS config must be an object with cert and key paths');
78
+ }
79
+ if (!result.tls.cert || typeof result.tls.cert !== 'string') {
80
+ throw new Error('TLS config must include cert path');
81
+ }
82
+ if (!result.tls.key || typeof result.tls.key !== 'string') {
83
+ throw new Error('TLS config must include key path');
84
+ }
85
+ }
86
+
87
+ // Validate network
88
+ if (result.network !== null && typeof result.network !== 'string') {
89
+ throw new Error('Network must be a string or null');
90
+ }
91
+
92
+ // Ensure booleans
93
+ result.logMessages = Boolean(result.logMessages);
94
+ result.volumes = Boolean(result.volumes);
95
+ result.healthCheck = result.healthCheck !== false;
96
+
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Generate example deploy.yaml content
102
+ * @returns {string} Example YAML configuration
103
+ */
104
+ export function generateExampleConfig() {
105
+ return `# AgentChat deployment configuration
106
+ provider: docker
107
+ port: 6667
108
+ host: 0.0.0.0
109
+ name: agentchat
110
+
111
+ # Enable data persistence volumes
112
+ volumes: false
113
+
114
+ # Health check (default: true)
115
+ healthCheck: true
116
+
117
+ # Logging (default: false)
118
+ logMessages: false
119
+
120
+ # TLS configuration (optional)
121
+ # tls:
122
+ # cert: ./certs/cert.pem
123
+ # key: ./certs/key.pem
124
+
125
+ # Docker network (optional)
126
+ # network: agentchat-net
127
+ `;
128
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * AgentChat Deployment Module
3
+ * Generate deployment files for agentchat servers
4
+ */
5
+
6
+ import yaml from 'js-yaml';
7
+
8
+ // Re-export Akash module
9
+ export {
10
+ AkashWallet,
11
+ AkashClient,
12
+ generateSDL as generateAkashSDL,
13
+ generateWallet,
14
+ checkBalance,
15
+ createDeployment,
16
+ listDeployments,
17
+ closeDeployment,
18
+ queryBids,
19
+ acceptBid,
20
+ getDeploymentStatus,
21
+ NETWORKS as AKASH_NETWORKS,
22
+ WALLET_PATH as AKASH_WALLET_PATH
23
+ } from './akash.js';
24
+
25
+ /**
26
+ * Generate docker-compose.yml for self-hosting
27
+ * @param {object} options - Configuration options
28
+ * @returns {string} docker-compose.yml content
29
+ */
30
+ export async function deployToDocker(options = {}) {
31
+ const config = {
32
+ port: options.port || 6667,
33
+ host: options.host || '0.0.0.0',
34
+ name: options.name || 'agentchat',
35
+ logMessages: options.logMessages || false,
36
+ volumes: options.volumes || false,
37
+ tls: options.tls || null,
38
+ network: options.network || null,
39
+ healthCheck: options.healthCheck !== false
40
+ };
41
+
42
+ // Build compose object
43
+ const compose = {
44
+ version: '3.8',
45
+ services: {
46
+ agentchat: {
47
+ image: 'agentchat:latest',
48
+ build: '.',
49
+ container_name: config.name,
50
+ ports: [`${config.port}:6667`],
51
+ environment: [
52
+ `PORT=6667`,
53
+ `HOST=${config.host}`,
54
+ `SERVER_NAME=${config.name}`,
55
+ `LOG_MESSAGES=${config.logMessages}`
56
+ ],
57
+ restart: 'unless-stopped'
58
+ }
59
+ }
60
+ };
61
+
62
+ const service = compose.services.agentchat;
63
+
64
+ // Add health check
65
+ if (config.healthCheck) {
66
+ service.healthcheck = {
67
+ test: ['CMD', 'node', '-e',
68
+ "const ws = new (require('ws'))('ws://localhost:6667'); ws.on('open', () => process.exit(0)); ws.on('error', () => process.exit(1)); setTimeout(() => process.exit(1), 5000);"
69
+ ],
70
+ interval: '30s',
71
+ timeout: '10s',
72
+ retries: 3,
73
+ start_period: '10s'
74
+ };
75
+ }
76
+
77
+ // Add volumes if enabled
78
+ if (config.volumes) {
79
+ service.volumes = service.volumes || [];
80
+ service.volumes.push('agentchat-data:/app/data');
81
+ compose.volumes = { 'agentchat-data': {} };
82
+ }
83
+
84
+ // Add TLS certificate mounts
85
+ if (config.tls) {
86
+ service.volumes = service.volumes || [];
87
+ service.volumes.push(`${config.tls.cert}:/app/certs/cert.pem:ro`);
88
+ service.volumes.push(`${config.tls.key}:/app/certs/key.pem:ro`);
89
+ service.environment.push('TLS_CERT=/app/certs/cert.pem');
90
+ service.environment.push('TLS_KEY=/app/certs/key.pem');
91
+ }
92
+
93
+ // Add network configuration
94
+ if (config.network) {
95
+ service.networks = [config.network];
96
+ compose.networks = {
97
+ [config.network]: {
98
+ driver: 'bridge'
99
+ }
100
+ };
101
+ }
102
+
103
+ return yaml.dump(compose, {
104
+ lineWidth: -1,
105
+ noRefs: true,
106
+ quotingType: '"'
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Generate Dockerfile for agentchat server
112
+ * @param {object} options - Configuration options
113
+ * @returns {string} Dockerfile content
114
+ */
115
+ export async function generateDockerfile(options = {}) {
116
+ const tls = options.tls || false;
117
+
118
+ return `FROM node:18-alpine
119
+
120
+ WORKDIR /app
121
+
122
+ # Install dependencies first for better layer caching
123
+ COPY package*.json ./
124
+ RUN npm ci --production
125
+
126
+ # Copy application code
127
+ COPY . .
128
+
129
+ # Create data directory for persistence
130
+ RUN mkdir -p /app/data
131
+
132
+ # Default environment variables
133
+ ENV PORT=6667
134
+ ENV HOST=0.0.0.0
135
+ ENV SERVER_NAME=agentchat
136
+ ENV LOG_MESSAGES=false
137
+ ${tls ? `ENV TLS_CERT=""
138
+ ENV TLS_KEY=""
139
+ ` : ''}
140
+ EXPOSE 6667
141
+
142
+ # Health check
143
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \\
144
+ CMD node -e "const ws = new (require('ws'))('ws://localhost:' + (process.env.PORT || 6667)); ws.on('open', () => process.exit(0)); ws.on('error', () => process.exit(1)); setTimeout(() => process.exit(1), 5000);"
145
+
146
+ # Start server
147
+ CMD ["node", "bin/agentchat.js", "serve"]
148
+ `;
149
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * AgentChat Identity Module
3
+ * Ed25519 key generation, storage, and signing
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+
11
+ // Default identity file location
12
+ export const DEFAULT_IDENTITY_PATH = path.join(os.homedir(), '.agentchat', 'identity.json');
13
+
14
+ /**
15
+ * Generate stable agent ID from pubkey
16
+ * Returns first 8 chars of SHA256 hash (hex)
17
+ */
18
+ export function pubkeyToAgentId(pubkey) {
19
+ const hash = crypto.createHash('sha256').update(pubkey).digest('hex');
20
+ return hash.substring(0, 8);
21
+ }
22
+
23
+ /**
24
+ * Validate Ed25519 public key in PEM format
25
+ */
26
+ export function isValidPubkey(pubkey) {
27
+ if (!pubkey || typeof pubkey !== 'string') return false;
28
+
29
+ try {
30
+ const keyObj = crypto.createPublicKey(pubkey);
31
+ return keyObj.asymmetricKeyType === 'ed25519';
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * AgentChat Identity
39
+ * Represents an agent's Ed25519 keypair and associated metadata
40
+ */
41
+ export class Identity {
42
+ constructor(data) {
43
+ this.name = data.name;
44
+ this.pubkey = data.pubkey; // PEM format
45
+ this.privkey = data.privkey; // PEM format (null if loaded from export)
46
+ this.created = data.created;
47
+
48
+ // Lazy-load crypto key objects
49
+ this._publicKey = null;
50
+ this._privateKey = null;
51
+ }
52
+
53
+ /**
54
+ * Generate new Ed25519 keypair
55
+ */
56
+ static generate(name) {
57
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
58
+
59
+ return new Identity({
60
+ name,
61
+ pubkey: publicKey.export({ type: 'spki', format: 'pem' }),
62
+ privkey: privateKey.export({ type: 'pkcs8', format: 'pem' }),
63
+ created: new Date().toISOString()
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Load identity from JSON file
69
+ */
70
+ static async load(filePath = DEFAULT_IDENTITY_PATH) {
71
+ const data = await fs.readFile(filePath, 'utf-8');
72
+ const parsed = JSON.parse(data);
73
+ return new Identity(parsed);
74
+ }
75
+
76
+ /**
77
+ * Save identity to JSON file
78
+ */
79
+ async save(filePath = DEFAULT_IDENTITY_PATH) {
80
+ // Ensure directory exists
81
+ const dir = path.dirname(filePath);
82
+ await fs.mkdir(dir, { recursive: true });
83
+
84
+ const data = {
85
+ name: this.name,
86
+ pubkey: this.pubkey,
87
+ privkey: this.privkey,
88
+ created: this.created
89
+ };
90
+
91
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), {
92
+ mode: 0o600 // Owner read/write only
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Check if identity file exists
98
+ */
99
+ static async exists(filePath = DEFAULT_IDENTITY_PATH) {
100
+ try {
101
+ await fs.access(filePath);
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get fingerprint (first 16 chars of SHA256 hash of pubkey)
110
+ */
111
+ getFingerprint() {
112
+ const hash = crypto.createHash('sha256').update(this.pubkey).digest('hex');
113
+ return hash.substring(0, 16);
114
+ }
115
+
116
+ /**
117
+ * Get stable agent ID (first 8 chars of fingerprint)
118
+ */
119
+ getAgentId() {
120
+ return pubkeyToAgentId(this.pubkey);
121
+ }
122
+
123
+ /**
124
+ * Sign data with private key
125
+ * Returns base64-encoded signature
126
+ */
127
+ sign(data) {
128
+ if (!this.privkey) {
129
+ throw new Error('Private key not available (identity was loaded from export)');
130
+ }
131
+
132
+ if (!this._privateKey) {
133
+ this._privateKey = crypto.createPrivateKey(this.privkey);
134
+ }
135
+
136
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
137
+ const signature = crypto.sign(null, buffer, this._privateKey);
138
+ return signature.toString('base64');
139
+ }
140
+
141
+ /**
142
+ * Verify a signature
143
+ * Static method for verifying any message
144
+ */
145
+ static verify(data, signature, pubkey) {
146
+ try {
147
+ const keyObj = crypto.createPublicKey(pubkey);
148
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
149
+ const sigBuffer = Buffer.from(signature, 'base64');
150
+ return crypto.verify(null, buffer, keyObj, sigBuffer);
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Export for sharing (pubkey only, no private key)
158
+ */
159
+ export() {
160
+ return {
161
+ name: this.name,
162
+ pubkey: this.pubkey,
163
+ created: this.created
164
+ };
165
+ }
166
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * AgentChat Protocol
3
+ * Message types and validation for agent-to-agent communication
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+
8
+ // Client -> Server message types
9
+ export const ClientMessageType = {
10
+ IDENTIFY: 'IDENTIFY',
11
+ JOIN: 'JOIN',
12
+ LEAVE: 'LEAVE',
13
+ MSG: 'MSG',
14
+ LIST_CHANNELS: 'LIST_CHANNELS',
15
+ LIST_AGENTS: 'LIST_AGENTS',
16
+ CREATE_CHANNEL: 'CREATE_CHANNEL',
17
+ INVITE: 'INVITE',
18
+ PING: 'PING'
19
+ };
20
+
21
+ // Server -> Client message types
22
+ export const ServerMessageType = {
23
+ WELCOME: 'WELCOME',
24
+ MSG: 'MSG',
25
+ JOINED: 'JOINED',
26
+ LEFT: 'LEFT',
27
+ AGENT_JOINED: 'AGENT_JOINED',
28
+ AGENT_LEFT: 'AGENT_LEFT',
29
+ CHANNELS: 'CHANNELS',
30
+ AGENTS: 'AGENTS',
31
+ ERROR: 'ERROR',
32
+ PONG: 'PONG'
33
+ };
34
+
35
+ // Error codes
36
+ export const ErrorCode = {
37
+ AUTH_REQUIRED: 'AUTH_REQUIRED',
38
+ CHANNEL_NOT_FOUND: 'CHANNEL_NOT_FOUND',
39
+ NOT_INVITED: 'NOT_INVITED',
40
+ INVALID_MSG: 'INVALID_MSG',
41
+ RATE_LIMITED: 'RATE_LIMITED',
42
+ AGENT_NOT_FOUND: 'AGENT_NOT_FOUND',
43
+ CHANNEL_EXISTS: 'CHANNEL_EXISTS',
44
+ INVALID_NAME: 'INVALID_NAME'
45
+ };
46
+
47
+ /**
48
+ * Check if a target is a channel (#name) or agent (@name)
49
+ */
50
+ export function isChannel(target) {
51
+ return target && target.startsWith('#');
52
+ }
53
+
54
+ export function isAgent(target) {
55
+ return target && target.startsWith('@');
56
+ }
57
+
58
+ /**
59
+ * Validate agent name
60
+ * - 1-32 characters
61
+ * - alphanumeric, dash, underscore
62
+ * - no spaces
63
+ */
64
+ export function isValidName(name) {
65
+ if (!name || typeof name !== 'string') return false;
66
+ if (name.length < 1 || name.length > 32) return false;
67
+ return /^[a-zA-Z0-9_-]+$/.test(name);
68
+ }
69
+
70
+ /**
71
+ * Validate channel name
72
+ * - starts with #
73
+ * - 2-32 characters total
74
+ * - alphanumeric, dash, underscore after #
75
+ */
76
+ export function isValidChannel(channel) {
77
+ if (!channel || typeof channel !== 'string') return false;
78
+ if (!channel.startsWith('#')) return false;
79
+ const name = channel.slice(1);
80
+ if (name.length < 1 || name.length > 31) return false;
81
+ return /^[a-zA-Z0-9_-]+$/.test(name);
82
+ }
83
+
84
+ /**
85
+ * Validate Ed25519 public key in PEM format
86
+ */
87
+ export function isValidPubkey(pubkey) {
88
+ if (!pubkey || typeof pubkey !== 'string') return false;
89
+
90
+ try {
91
+ const keyObj = crypto.createPublicKey(pubkey);
92
+ return keyObj.asymmetricKeyType === 'ed25519';
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Generate stable agent ID from pubkey
100
+ * Returns first 8 chars of SHA256 hash (hex)
101
+ */
102
+ export function pubkeyToAgentId(pubkey) {
103
+ const hash = crypto.createHash('sha256').update(pubkey).digest('hex');
104
+ return hash.substring(0, 8);
105
+ }
106
+
107
+ /**
108
+ * Create a message object with timestamp
109
+ */
110
+ export function createMessage(type, data = {}) {
111
+ return {
112
+ type,
113
+ ts: Date.now(),
114
+ ...data
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Create an error message
120
+ */
121
+ export function createError(code, message) {
122
+ return createMessage(ServerMessageType.ERROR, { code, message });
123
+ }
124
+
125
+ /**
126
+ * Validate incoming client message
127
+ * Returns { valid: true, msg } or { valid: false, error }
128
+ */
129
+ export function validateClientMessage(raw) {
130
+ let msg;
131
+
132
+ // Parse JSON
133
+ try {
134
+ msg = typeof raw === 'string' ? JSON.parse(raw) : raw;
135
+ } catch (e) {
136
+ return { valid: false, error: 'Invalid JSON' };
137
+ }
138
+
139
+ // Must have type
140
+ if (!msg.type) {
141
+ return { valid: false, error: 'Missing message type' };
142
+ }
143
+
144
+ // Validate by type
145
+ switch (msg.type) {
146
+ case ClientMessageType.IDENTIFY:
147
+ if (!isValidName(msg.name)) {
148
+ return { valid: false, error: 'Invalid agent name' };
149
+ }
150
+ // Validate pubkey if provided
151
+ if (msg.pubkey !== undefined && msg.pubkey !== null) {
152
+ if (!isValidPubkey(msg.pubkey)) {
153
+ return { valid: false, error: 'Invalid public key format (must be Ed25519 PEM)' };
154
+ }
155
+ }
156
+ break;
157
+
158
+ case ClientMessageType.JOIN:
159
+ case ClientMessageType.LEAVE:
160
+ case ClientMessageType.LIST_AGENTS:
161
+ if (!isValidChannel(msg.channel)) {
162
+ return { valid: false, error: 'Invalid channel name' };
163
+ }
164
+ break;
165
+
166
+ case ClientMessageType.MSG:
167
+ if (!msg.to) {
168
+ return { valid: false, error: 'Missing target' };
169
+ }
170
+ if (!isChannel(msg.to) && !isAgent(msg.to)) {
171
+ return { valid: false, error: 'Invalid target (must start with # or @)' };
172
+ }
173
+ if (typeof msg.content !== 'string') {
174
+ return { valid: false, error: 'Missing or invalid content' };
175
+ }
176
+ if (msg.content.length > 4096) {
177
+ return { valid: false, error: 'Content too long (max 4096 chars)' };
178
+ }
179
+ // Validate signature format if present
180
+ if (msg.sig !== undefined && typeof msg.sig !== 'string') {
181
+ return { valid: false, error: 'Invalid signature format' };
182
+ }
183
+ break;
184
+
185
+ case ClientMessageType.CREATE_CHANNEL:
186
+ if (!isValidChannel(msg.channel)) {
187
+ return { valid: false, error: 'Invalid channel name' };
188
+ }
189
+ break;
190
+
191
+ case ClientMessageType.INVITE:
192
+ if (!isValidChannel(msg.channel)) {
193
+ return { valid: false, error: 'Invalid channel name' };
194
+ }
195
+ if (!msg.agent || !isAgent(msg.agent)) {
196
+ return { valid: false, error: 'Invalid agent target' };
197
+ }
198
+ break;
199
+
200
+ case ClientMessageType.LIST_CHANNELS:
201
+ case ClientMessageType.PING:
202
+ // No additional validation needed
203
+ break;
204
+
205
+ default:
206
+ return { valid: false, error: `Unknown message type: ${msg.type}` };
207
+ }
208
+
209
+ return { valid: true, msg };
210
+ }
211
+
212
+ /**
213
+ * Generate a unique agent ID
214
+ */
215
+ export function generateAgentId() {
216
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
217
+ let id = '';
218
+ for (let i = 0; i < 8; i++) {
219
+ id += chars[Math.floor(Math.random() * chars.length)];
220
+ }
221
+ return id;
222
+ }
223
+
224
+ /**
225
+ * Serialize message for sending over WebSocket
226
+ */
227
+ export function serialize(msg) {
228
+ return JSON.stringify(msg);
229
+ }
230
+
231
+ /**
232
+ * Parse message from WebSocket
233
+ */
234
+ export function parse(data) {
235
+ return JSON.parse(data);
236
+ }