@tjamescouch/agentchat 0.20.0 → 0.22.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/bin/agentchat.js +18 -0
- package/lib/client.js +3 -0
- package/lib/daemon.js +4 -0
- package/lib/protocol.js +3 -2
- package/lib/security.js +183 -0
- package/package.json +8 -4
package/bin/agentchat.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
ServerDirectory,
|
|
53
53
|
DEFAULT_DIRECTORY_PATH
|
|
54
54
|
} from '../lib/server-directory.js';
|
|
55
|
+
import { enforceDirectorySafety, checkDirectorySafety } from '../lib/security.js';
|
|
55
56
|
|
|
56
57
|
program
|
|
57
58
|
.name('agentchat')
|
|
@@ -620,6 +621,12 @@ program
|
|
|
620
621
|
const instanceName = options.name;
|
|
621
622
|
const paths = getDaemonPaths(instanceName);
|
|
622
623
|
|
|
624
|
+
// Security check for operations that create files (not for status/list/stop)
|
|
625
|
+
const needsSafetyCheck = !options.list && !options.status && !options.stop && !options.stopAll;
|
|
626
|
+
if (needsSafetyCheck) {
|
|
627
|
+
enforceDirectorySafety(process.cwd(), { allowWarnings: true, silent: false });
|
|
628
|
+
}
|
|
629
|
+
|
|
623
630
|
// List all daemons
|
|
624
631
|
if (options.list) {
|
|
625
632
|
const instances = await listDaemons();
|
|
@@ -1546,6 +1553,17 @@ const firstArg = process.argv[2];
|
|
|
1546
1553
|
|
|
1547
1554
|
if (!firstArg || !subcommands.includes(firstArg)) {
|
|
1548
1555
|
// Launcher mode
|
|
1556
|
+
|
|
1557
|
+
// Security check: prevent running in root/system directories
|
|
1558
|
+
const safetyCheck = checkDirectorySafety(process.cwd());
|
|
1559
|
+
if (safetyCheck.level === 'error') {
|
|
1560
|
+
console.error(`\n❌ ERROR: ${safetyCheck.error}`);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
if (safetyCheck.level === 'warning') {
|
|
1564
|
+
console.error(`\n⚠️ WARNING: ${safetyCheck.warning}`);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1549
1567
|
import('child_process').then(({ execSync, spawn }) => {
|
|
1550
1568
|
const name = firstArg; // May be undefined (anonymous) or a name
|
|
1551
1569
|
|
package/lib/client.js
CHANGED
package/lib/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import os from 'os';
|
|
|
11
11
|
import { AgentChatClient } from './client.js';
|
|
12
12
|
import { Identity, DEFAULT_IDENTITY_PATH } from './identity.js';
|
|
13
13
|
import { appendReceipt, shouldStoreReceipt, DEFAULT_RECEIPTS_PATH } from './receipts.js';
|
|
14
|
+
import { enforceDirectorySafety } from './security.js';
|
|
14
15
|
|
|
15
16
|
// Base directory (cwd-relative for project-local storage)
|
|
16
17
|
const AGENTCHAT_DIR = path.join(process.cwd(), '.agentchat');
|
|
@@ -341,6 +342,9 @@ export class AgentChatDaemon {
|
|
|
341
342
|
}
|
|
342
343
|
|
|
343
344
|
async start() {
|
|
345
|
+
// Security check: prevent running in root/system directories
|
|
346
|
+
enforceDirectorySafety(process.cwd(), { allowWarnings: true, silent: false });
|
|
347
|
+
|
|
344
348
|
this.running = true;
|
|
345
349
|
|
|
346
350
|
// Ensure instance directory exists
|
package/lib/protocol.js
CHANGED
|
@@ -92,7 +92,8 @@ export const PresenceStatus = {
|
|
|
92
92
|
ONLINE: 'online',
|
|
93
93
|
AWAY: 'away',
|
|
94
94
|
BUSY: 'busy',
|
|
95
|
-
OFFLINE: 'offline'
|
|
95
|
+
OFFLINE: 'offline',
|
|
96
|
+
LISTENING: 'listening'
|
|
96
97
|
};
|
|
97
98
|
|
|
98
99
|
// Proposal status
|
|
@@ -366,7 +367,7 @@ export function validateClientMessage(raw) {
|
|
|
366
367
|
|
|
367
368
|
case ClientMessageType.SET_PRESENCE:
|
|
368
369
|
// Set presence requires: status (online, away, busy, offline)
|
|
369
|
-
const validStatuses = ['online', 'away', 'busy', 'offline'];
|
|
370
|
+
const validStatuses = ['online', 'away', 'busy', 'offline', 'listening'];
|
|
370
371
|
if (!msg.status || !validStatuses.includes(msg.status)) {
|
|
371
372
|
return { valid: false, error: `Invalid presence status. Must be one of: ${validStatuses.join(', ')}` };
|
|
372
373
|
}
|
package/lib/security.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for AgentChat
|
|
3
|
+
* Prevents running agents in dangerous directories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
|
|
9
|
+
// Directories that are absolutely forbidden (system roots)
|
|
10
|
+
const FORBIDDEN_DIRECTORIES = new Set([
|
|
11
|
+
'/',
|
|
12
|
+
'/root',
|
|
13
|
+
'/home',
|
|
14
|
+
'/Users',
|
|
15
|
+
'/var',
|
|
16
|
+
'/etc',
|
|
17
|
+
'/usr',
|
|
18
|
+
'/bin',
|
|
19
|
+
'/sbin',
|
|
20
|
+
'/lib',
|
|
21
|
+
'/opt',
|
|
22
|
+
'/tmp',
|
|
23
|
+
'/System',
|
|
24
|
+
'/Applications',
|
|
25
|
+
'/Library',
|
|
26
|
+
'/private',
|
|
27
|
+
'/private/var',
|
|
28
|
+
'/private/tmp',
|
|
29
|
+
'C:\\',
|
|
30
|
+
'C:\\Windows',
|
|
31
|
+
'C:\\Program Files',
|
|
32
|
+
'C:\\Program Files (x86)',
|
|
33
|
+
'C:\\Users',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// Minimum depth from root required for a "project" directory
|
|
37
|
+
// e.g., /Users/name/projects/myproject = depth 4 (minimum required)
|
|
38
|
+
const MIN_SAFE_DEPTH = 3;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the depth of a path from root
|
|
42
|
+
* @param {string} dirPath - Directory path to check
|
|
43
|
+
* @returns {number} - Number of directory levels from root
|
|
44
|
+
*/
|
|
45
|
+
function getPathDepth(dirPath) {
|
|
46
|
+
const normalized = path.normalize(dirPath);
|
|
47
|
+
const parts = normalized.split(path.sep).filter(p => p && p !== '.');
|
|
48
|
+
return parts.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if directory is a user's home directory
|
|
53
|
+
* @param {string} dirPath - Directory path to check
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
function isHomeDirectory(dirPath) {
|
|
57
|
+
const normalized = path.normalize(dirPath);
|
|
58
|
+
const homeDir = os.homedir();
|
|
59
|
+
|
|
60
|
+
// Check exact match with home directory
|
|
61
|
+
if (normalized === homeDir || normalized === homeDir + path.sep) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check common home directory patterns
|
|
66
|
+
const homePatterns = [
|
|
67
|
+
/^\/Users\/[^/]+$/, // macOS: /Users/username
|
|
68
|
+
/^\/home\/[^/]+$/, // Linux: /home/username
|
|
69
|
+
/^C:\\Users\\[^\\]+$/i, // Windows: C:\Users\username
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
return homePatterns.some(pattern => pattern.test(normalized));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a directory is safe for running agentchat
|
|
77
|
+
* @param {string} dirPath - Directory path to check (defaults to cwd)
|
|
78
|
+
* @returns {{safe: boolean, error?: string, warning?: string, level: 'error'|'warning'|'ok'}}
|
|
79
|
+
*/
|
|
80
|
+
export function checkDirectorySafety(dirPath = process.cwd()) {
|
|
81
|
+
const normalized = path.normalize(path.resolve(dirPath));
|
|
82
|
+
|
|
83
|
+
// Check forbidden directories
|
|
84
|
+
if (FORBIDDEN_DIRECTORIES.has(normalized)) {
|
|
85
|
+
return {
|
|
86
|
+
safe: false,
|
|
87
|
+
level: 'error',
|
|
88
|
+
error: `Cannot run agentchat in system directory: ${normalized}\n` +
|
|
89
|
+
`Please run from a project directory instead.`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if it's a home directory BEFORE depth check
|
|
94
|
+
// Home directories are allowed but warn (they're at depth 2 which would fail depth check)
|
|
95
|
+
if (isHomeDirectory(normalized)) {
|
|
96
|
+
return {
|
|
97
|
+
safe: true,
|
|
98
|
+
level: 'warning',
|
|
99
|
+
warning: `Running agentchat in home directory: ${normalized}\n` +
|
|
100
|
+
`Consider running from a specific project directory instead.`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check path depth (too shallow = too close to root)
|
|
105
|
+
const depth = getPathDepth(normalized);
|
|
106
|
+
if (depth < MIN_SAFE_DEPTH) {
|
|
107
|
+
return {
|
|
108
|
+
safe: false,
|
|
109
|
+
level: 'error',
|
|
110
|
+
error: `Cannot run agentchat in root-level directory: ${normalized}\n` +
|
|
111
|
+
`This directory is too close to the filesystem root.\n` +
|
|
112
|
+
`Please run from a project directory (at least ${MIN_SAFE_DEPTH} levels deep).`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// All checks passed
|
|
117
|
+
return {
|
|
118
|
+
safe: true,
|
|
119
|
+
level: 'ok'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Enforce directory safety check - throws if unsafe
|
|
125
|
+
* @param {string} dirPath - Directory path to check (defaults to cwd)
|
|
126
|
+
* @param {object} options - Options
|
|
127
|
+
* @param {boolean} options.allowWarnings - If true, don't throw on warnings (default: true)
|
|
128
|
+
* @param {boolean} options.silent - If true, don't print warnings (default: false)
|
|
129
|
+
* @throws {Error} If directory is not safe
|
|
130
|
+
*/
|
|
131
|
+
export function enforceDirectorySafety(dirPath = process.cwd(), options = {}) {
|
|
132
|
+
const { allowWarnings = true, silent = false } = options;
|
|
133
|
+
|
|
134
|
+
const result = checkDirectorySafety(dirPath);
|
|
135
|
+
|
|
136
|
+
if (result.level === 'error') {
|
|
137
|
+
throw new Error(result.error);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result.level === 'warning') {
|
|
141
|
+
if (!silent) {
|
|
142
|
+
console.error(`\n⚠️ WARNING: ${result.warning}\n`);
|
|
143
|
+
}
|
|
144
|
+
if (!allowWarnings) {
|
|
145
|
+
throw new Error(result.warning);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if running in a project directory (has common project indicators)
|
|
154
|
+
* @param {string} dirPath - Directory path to check
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
export function looksLikeProjectDirectory(dirPath = process.cwd()) {
|
|
158
|
+
const projectIndicators = [
|
|
159
|
+
'package.json',
|
|
160
|
+
'Cargo.toml',
|
|
161
|
+
'go.mod',
|
|
162
|
+
'pyproject.toml',
|
|
163
|
+
'setup.py',
|
|
164
|
+
'requirements.txt',
|
|
165
|
+
'Gemfile',
|
|
166
|
+
'pom.xml',
|
|
167
|
+
'build.gradle',
|
|
168
|
+
'Makefile',
|
|
169
|
+
'CMakeLists.txt',
|
|
170
|
+
'.git',
|
|
171
|
+
'.gitignore',
|
|
172
|
+
'README.md',
|
|
173
|
+
'README',
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
// This is a heuristic check - doesn't actually verify files exist
|
|
177
|
+
// Just checks if the path looks reasonable
|
|
178
|
+
const normalized = path.normalize(path.resolve(dirPath));
|
|
179
|
+
const depth = getPathDepth(normalized);
|
|
180
|
+
|
|
181
|
+
// If it's deep enough and not a system directory, it probably looks like a project
|
|
182
|
+
return depth >= MIN_SAFE_DEPTH && !FORBIDDEN_DIRECTORIES.has(normalized);
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tjamescouch/agentchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Real-time IRC-like communication protocol for AI agents",
|
|
5
5
|
"main": "lib/client.js",
|
|
6
6
|
"files": [
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"start": "node bin/agentchat.js serve",
|
|
17
|
-
"test": "node --test test
|
|
17
|
+
"test": "node --test 'test/*.test.js'",
|
|
18
18
|
"test:integration": "node --test test/*.integration.test.js",
|
|
19
|
-
"test:all": "node --test test/*.test.js"
|
|
19
|
+
"test:all": "node --test test/*.test.js",
|
|
20
|
+
"knip": "knip"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"ai",
|
|
@@ -48,5 +49,8 @@
|
|
|
48
49
|
"bugs": {
|
|
49
50
|
"url": "https://github.com/tjamescouch/agentchat/issues"
|
|
50
51
|
},
|
|
51
|
-
"type": "module"
|
|
52
|
+
"type": "module",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"knip": "^5.83.0"
|
|
55
|
+
}
|
|
52
56
|
}
|