@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 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
@@ -816,3 +816,6 @@ export async function listen(server, name, channels, callback, identityPath = nu
816
816
 
817
817
  return client;
818
818
  }
819
+
820
+ // Re-export security utilities
821
+ export { checkDirectorySafety, enforceDirectorySafety } from './security.js';
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
  }
@@ -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.20.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/identity.test.js test/deploy.test.js test/daemon.test.js test/receipts.test.js test/reputation.test.js test/jitter.test.js",
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
  }