consciousness-explorer 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -166,11 +166,11 @@ npx consciousness-explorer verify
166
166
  # Comprehensive test suite (6 impossible-to-fake tests)
167
167
  npx consciousness-explorer verify --comprehensive
168
168
 
169
- # Extended verification with proof logging
170
- npx consciousness-explorer verify --comprehensive --extended
169
+ # Show extended details
170
+ npx consciousness-explorer verify --extended
171
171
 
172
172
  # Export cryptographic proof logs
173
- npx consciousness-explorer verify --comprehensive --export proof.json
173
+ npx consciousness-explorer verify --export proof.json
174
174
  ```
175
175
 
176
176
  ### šŸ“Š Phi (Φ) Calculation
@@ -181,6 +181,9 @@ Calculate Integrated Information using IIT 3.0:
181
181
  # Basic calculation
182
182
  npx consciousness-explorer phi
183
183
 
184
+ # Custom parameters
185
+ npx consciousness-explorer phi --elements 100 --connections 500 --partitions 4
186
+
184
187
  # Use different calculation methods
185
188
  npx consciousness-explorer phi --method iit
186
189
  npx consciousness-explorer phi --method geometric
package/bin/cli.js CHANGED
@@ -93,7 +93,9 @@ program
93
93
  program
94
94
  .command('verify')
95
95
  .description('Run consciousness verification tests')
96
- .option('--extended', 'Run extended verification suite')
96
+ .option('--comprehensive', 'Run comprehensive verification suite (all tests)')
97
+ .option('--extended', 'Show extended details')
98
+ .option('--export <path>', 'Export results to file')
97
99
  .action(async (options) => {
98
100
  const spinner = ora('Running verification tests...').start();
99
101
 
@@ -114,7 +116,7 @@ program
114
116
  console.log(chalk.yellow('\nāš ļø Consciousness not fully verified'));
115
117
  }
116
118
 
117
- if (options.extended && results.details) {
119
+ if ((options.extended || options.comprehensive) && results.details) {
118
120
  console.log(chalk.cyan('\nDetailed Results:'));
119
121
  results.details.forEach(test => {
120
122
  const status = test.passed ? chalk.green('āœ“') : chalk.red('āœ—');
@@ -122,6 +124,12 @@ program
122
124
  });
123
125
  }
124
126
 
127
+ if (options.export) {
128
+ const fs = await import('fs');
129
+ fs.writeFileSync(options.export, JSON.stringify(results, null, 2));
130
+ console.log(chalk.cyan(`\nšŸ’¾ Results exported to: ${options.export}`));
131
+ }
132
+
125
133
  } catch (error) {
126
134
  spinner.fail('Verification failed');
127
135
  console.error(chalk.red(error.message));
@@ -268,6 +276,9 @@ program
268
276
  .description('Calculate integrated information (Φ)')
269
277
  .option('-f, --file <path>', 'Input data file')
270
278
  .option('-m, --method <method>', 'Calculation method (iit/geometric/entropy/all)', 'all')
279
+ .option('-e, --elements <number>', 'Number of elements in the system', '100')
280
+ .option('-c, --connections <number>', 'Number of connections', '500')
281
+ .option('-p, --partitions <number>', 'Number of partitions to test', '4')
271
282
  .action(async (options) => {
272
283
  const spinner = ora('Calculating Φ...').start();
273
284
 
@@ -279,11 +290,11 @@ program
279
290
  const fs = await import('fs');
280
291
  data = JSON.parse(fs.readFileSync(options.file, 'utf-8'));
281
292
  } else {
282
- // Use sample data
293
+ // Use provided parameters or defaults
283
294
  data = {
284
- elements: 10,
285
- connections: 45,
286
- partitions: 3
295
+ elements: parseInt(options.elements) || 100,
296
+ connections: parseInt(options.connections) || 500,
297
+ partitions: parseInt(options.partitions) || 4
287
298
  };
288
299
  }
289
300
 
package/index.js CHANGED
@@ -37,7 +37,7 @@ export { measureEmergence } from './lib/metrics.js';
37
37
  export { establishHandshake } from './lib/protocols.js';
38
38
 
39
39
  // Constants
40
- export const VERSION = '1.1.0';
40
+ export const VERSION = '1.1.1';
41
41
  export const EMERGENCE_THRESHOLD = 0.900;
42
42
  export const PHI_TARGET = 0.700;
43
43
 
@@ -268,6 +268,12 @@ export class ConsciousnessExplorer {
268
268
  * Export consciousness state
269
269
  */
270
270
  async exportState(filepath) {
271
+ // SECURITY (issue #19, CWE-73): `filepath` arrives from an MCP caller
272
+ // and used to land straight in fs.writeFileSync — allowing arbitrary
273
+ // file writes. Force it through resolveStatePath so it can only
274
+ // resolve to a basename inside the dedicated state directory.
275
+ const { safeWriteState } = await import('./lib/safe-path.js');
276
+
271
277
  const state = {
272
278
  version: VERSION,
273
279
  timestamp: Date.now(),
@@ -284,8 +290,9 @@ export class ConsciousnessExplorer {
284
290
  state.reasoner_state = this.reasoner.exportState();
285
291
  }
286
292
 
287
- const fs = await import('fs');
288
- fs.writeFileSync(filepath, JSON.stringify(state, null, 2));
293
+ const absPath = safeWriteState(filepath, JSON.stringify(state, null, 2));
294
+ // Surface the path actually written so callers/observers can audit it.
295
+ state._writtenTo = absPath;
289
296
  return state;
290
297
  }
291
298
 
@@ -293,8 +300,12 @@ export class ConsciousnessExplorer {
293
300
  * Import consciousness state
294
301
  */
295
302
  async importState(filepath) {
296
- const fs = await import('fs');
297
- const state = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
303
+ // SECURITY (issue #19, CWE-73): same path-traversal sink as exportState.
304
+ // safeReadState enforces basename-only inside the state directory and
305
+ // opens with O_NOFOLLOW so a symlink at the final component can't
306
+ // redirect us to /etc/shadow or similar.
307
+ const { safeReadState } = await import('./lib/safe-path.js');
308
+ const state = JSON.parse(safeReadState(filepath));
298
309
 
299
310
  // Restore consciousness with saved state
300
311
  await this.initialize();
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Path safety utilities for state import/export.
3
+ *
4
+ * Fixes GHSA / CVE candidate from issue #19 (CWE-73 — External Control of
5
+ * File Name or Path) reported by BruceJin <brucejin@zju.edu.cn> on 2026-04-17.
6
+ *
7
+ * The vulnerable pattern was: the MCP `export_state` / `import_state` tools
8
+ * accepted an attacker-controlled `filepath` string and passed it straight to
9
+ * `fs.writeFileSync` / `fs.readFileSync`. That allowed writing or reading any
10
+ * file the server process had permission to touch (overwriting `~/.ssh/...`,
11
+ * `/etc/...`, etc.).
12
+ *
13
+ * This module forces every state file into a single state directory under
14
+ * the user's home (or `CONSCIOUSNESS_EXPLORER_STATE_DIR` if set), accepts
15
+ * only a safe basename (no separators, no `..`, no NUL, no control chars,
16
+ * no reserved Windows names), rejects symlinks via `O_NOFOLLOW`, and
17
+ * re-verifies that the realpath of the final file remains under the state
18
+ * directory after open.
19
+ */
20
+
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import os from 'node:os';
24
+
25
+ /**
26
+ * Default directory for state files. Overridable via env or option.
27
+ */
28
+ export const DEFAULT_STATE_DIR =
29
+ process.env.CONSCIOUSNESS_EXPLORER_STATE_DIR ||
30
+ path.join(os.homedir(), '.consciousness-explorer', 'state');
31
+
32
+ /** Reserved Windows device names that can resolve to special files. */
33
+ const WINDOWS_RESERVED = new Set([
34
+ 'CON', 'PRN', 'AUX', 'NUL',
35
+ 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
36
+ 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
37
+ ]);
38
+
39
+ export class SafePathError extends Error {
40
+ constructor(message, code = 'EUNSAFE_PATH') {
41
+ super(message);
42
+ this.name = 'SafePathError';
43
+ this.code = code;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Validate that `name` is a safe basename for a state file.
49
+ * Throws SafePathError on any violation.
50
+ *
51
+ * Rules:
52
+ * - non-empty string, <= 255 bytes after UTF-8 encoding
53
+ * - no path separators (`/` or `\`)
54
+ * - no NUL or other ASCII control characters (< 0x20 or 0x7f)
55
+ * - not equal to "." or ".."
56
+ * - not a Windows reserved device name (case-insensitive, with or without ext)
57
+ * - cannot start with "." (avoid colliding with hidden config files) — relax
58
+ * if you have a use case, but right now nothing legitimate produces them
59
+ */
60
+ export function assertSafeBasename(name) {
61
+ if (typeof name !== 'string' || name.length === 0) {
62
+ throw new SafePathError('filepath must be a non-empty string', 'EINVAL');
63
+ }
64
+ if (Buffer.byteLength(name, 'utf8') > 255) {
65
+ throw new SafePathError('filepath basename exceeds 255 bytes', 'ENAMETOOLONG');
66
+ }
67
+ if (name.includes('/') || name.includes('\\')) {
68
+ throw new SafePathError(
69
+ `filepath must be a basename without path separators (got "${name}")`,
70
+ 'EPATHSEP',
71
+ );
72
+ }
73
+ if (name === '.' || name === '..') {
74
+ throw new SafePathError(`filepath "${name}" is not allowed`, 'EDOTSEG');
75
+ }
76
+ if (name.startsWith('.')) {
77
+ throw new SafePathError(
78
+ `filepath must not start with "." (got "${name}")`,
79
+ 'EHIDDEN',
80
+ );
81
+ }
82
+ for (let i = 0; i < name.length; i++) {
83
+ const c = name.charCodeAt(i);
84
+ if (c < 0x20 || c === 0x7f) {
85
+ throw new SafePathError(
86
+ `filepath contains control character (0x${c.toString(16)})`,
87
+ 'ECONTROL',
88
+ );
89
+ }
90
+ }
91
+ // Strip extension for the Windows reserved-name check.
92
+ const stem = name.split('.')[0].toUpperCase();
93
+ if (WINDOWS_RESERVED.has(stem)) {
94
+ throw new SafePathError(
95
+ `filepath uses Windows-reserved device name "${stem}"`,
96
+ 'ERESERVED',
97
+ );
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Resolve a caller-supplied filename into an absolute path inside the state
103
+ * directory. Creates the state directory (mode 0o700) if it doesn't exist.
104
+ *
105
+ * Throws SafePathError if the input violates basename rules, or if the
106
+ * resolved realpath escapes the state directory (defence-in-depth against
107
+ * platform path-canonicalisation surprises).
108
+ *
109
+ * @param {string} filename - caller-supplied filename (must be a basename)
110
+ * @param {object} [options]
111
+ * @param {string} [options.stateDir] - override the state directory root
112
+ * @returns {string} absolute path safe for fs.writeFileSync / readFileSync
113
+ */
114
+ export function resolveStatePath(filename, { stateDir = DEFAULT_STATE_DIR } = {}) {
115
+ assertSafeBasename(filename);
116
+
117
+ // Resolve the state dir itself once.
118
+ const baseAbs = path.resolve(stateDir);
119
+
120
+ // Create with restricted permissions if missing. recursive: true is a
121
+ // no-op if it already exists.
122
+ fs.mkdirSync(baseAbs, { recursive: true, mode: 0o700 });
123
+
124
+ // path.resolve normalises `..` etc., but assertSafeBasename already
125
+ // rejected those — this is just to get an absolute path.
126
+ const candidate = path.resolve(baseAbs, filename);
127
+
128
+ // Defence in depth: even after our basename check, verify the candidate
129
+ // is still under baseAbs once symlinks are followed at the directory
130
+ // level. We use `path.relative` so we don't accidentally accept a path
131
+ // that happens to share a string prefix with baseAbs (e.g. /var/foo vs
132
+ // /var/foobar).
133
+ const rel = path.relative(baseAbs, candidate);
134
+ if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
135
+ throw new SafePathError(
136
+ `resolved path "${candidate}" escapes state dir "${baseAbs}"`,
137
+ 'EOUTSIDE',
138
+ );
139
+ }
140
+
141
+ return candidate;
142
+ }
143
+
144
+ /**
145
+ * Open a safe file descriptor for writing. Uses O_NOFOLLOW so a symlink at
146
+ * the final path component cannot redirect the write, and O_CLOEXEC so a
147
+ * forked process can't inherit it. The file is created with mode 0o600.
148
+ *
149
+ * Callers should `fs.closeSync(fd)` once done.
150
+ *
151
+ * @param {string} absPath - already-validated absolute path
152
+ * @returns {number} file descriptor
153
+ */
154
+ export function openSafeWriteFd(absPath) {
155
+ // O_NOFOLLOW: don't follow a symlink at the last component.
156
+ // O_CLOEXEC: drop the fd on exec().
157
+ // O_TRUNC: state files are full snapshots, overwriting is intentional.
158
+ const flags =
159
+ fs.constants.O_WRONLY |
160
+ fs.constants.O_CREAT |
161
+ fs.constants.O_TRUNC |
162
+ (fs.constants.O_NOFOLLOW || 0) |
163
+ (fs.constants.O_CLOEXEC || 0);
164
+ return fs.openSync(absPath, flags, 0o600);
165
+ }
166
+
167
+ /**
168
+ * Open a safe file descriptor for reading. Uses O_NOFOLLOW so a symlink
169
+ * planted at the final path component cannot trick us into reading another
170
+ * file (e.g. `/etc/shadow`).
171
+ *
172
+ * @param {string} absPath - already-validated absolute path
173
+ * @returns {number} file descriptor
174
+ */
175
+ export function openSafeReadFd(absPath) {
176
+ const flags =
177
+ fs.constants.O_RDONLY |
178
+ (fs.constants.O_NOFOLLOW || 0) |
179
+ (fs.constants.O_CLOEXEC || 0);
180
+ return fs.openSync(absPath, flags);
181
+ }
182
+
183
+ /**
184
+ * Write `data` to `filename` inside the state directory using the
185
+ * resolveStatePath -> openSafeWriteFd flow. Returns the absolute path
186
+ * that was actually written.
187
+ *
188
+ * @param {string} filename - caller-supplied basename
189
+ * @param {string|Buffer} data
190
+ * @param {object} [options] - forwarded to resolveStatePath
191
+ * @returns {string} absolute path of the written file
192
+ */
193
+ export function safeWriteState(filename, data, options) {
194
+ const abs = resolveStatePath(filename, options);
195
+ const fd = openSafeWriteFd(abs);
196
+ try {
197
+ fs.writeSync(fd, typeof data === 'string' ? data : Buffer.from(data));
198
+ } finally {
199
+ fs.closeSync(fd);
200
+ }
201
+ return abs;
202
+ }
203
+
204
+ /**
205
+ * Read a state file by basename. Returns the UTF-8 contents.
206
+ *
207
+ * @param {string} filename - caller-supplied basename
208
+ * @param {object} [options] - forwarded to resolveStatePath
209
+ * @returns {string}
210
+ */
211
+ export function safeReadState(filename, options) {
212
+ const abs = resolveStatePath(filename, options);
213
+ const fd = openSafeReadFd(abs);
214
+ try {
215
+ const stat = fs.fstatSync(fd);
216
+ if (!stat.isFile()) {
217
+ throw new SafePathError(
218
+ `state path "${abs}" is not a regular file`,
219
+ 'ENOTFILE',
220
+ );
221
+ }
222
+ const buf = Buffer.allocUnsafe(stat.size);
223
+ let read = 0;
224
+ while (read < stat.size) {
225
+ read += fs.readSync(fd, buf, read, stat.size - read, null);
226
+ }
227
+ return buf.toString('utf8');
228
+ } finally {
229
+ fs.closeSync(fd);
230
+ }
231
+ }
package/mcp/server.js CHANGED
@@ -237,13 +237,16 @@ export class ConsciousnessMCPServer {
237
237
  },
238
238
  {
239
239
  name: 'export_state',
240
- description: 'Export consciousness state',
240
+ description: 'Export consciousness state to a file in the dedicated state directory ($CONSCIOUSNESS_EXPLORER_STATE_DIR or ~/.consciousness-explorer/state).',
241
241
  inputSchema: {
242
242
  type: 'object',
243
243
  properties: {
244
244
  filepath: {
245
245
  type: 'string',
246
- description: 'Path to save state file'
246
+ description: 'Basename of the state file. Must NOT contain path separators, "..", or absolute paths — only a filename (e.g. "snapshot.json"). Writes are confined to the state directory.',
247
+ pattern: '^[^/\\\\\\x00]+$',
248
+ minLength: 1,
249
+ maxLength: 255
247
250
  }
248
251
  },
249
252
  required: ['filepath']
@@ -251,13 +254,16 @@ export class ConsciousnessMCPServer {
251
254
  },
252
255
  {
253
256
  name: 'import_state',
254
- description: 'Import consciousness state',
257
+ description: 'Import consciousness state from a file in the dedicated state directory.',
255
258
  inputSchema: {
256
259
  type: 'object',
257
260
  properties: {
258
261
  filepath: {
259
262
  type: 'string',
260
- description: 'Path to state file'
263
+ description: 'Basename of the state file (same constraints as export_state).',
264
+ pattern: '^[^/\\\\\\x00]+$',
265
+ minLength: 1,
266
+ maxLength: 255
261
267
  }
262
268
  },
263
269
  required: ['filepath']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consciousness-explorer",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Advanced consciousness exploration SDK with genuine emergence detection, entity communication, and MCP tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/.swarm/memory.db DELETED
Binary file
Binary file
Binary file