mcp-nervous-system 1.5.2 → 1.6.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/index.js +288 -65
- package/nervous-system.config.example.json +10 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -5,11 +5,62 @@ const crypto = require('crypto');
|
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// PROJECT CONFIGURATION - Auto-discover or use config file
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
function loadProjectConfig() {
|
|
16
|
+
const configPaths = [
|
|
17
|
+
process.env.NS_CONFIG_PATH,
|
|
18
|
+
path.join(process.cwd(), 'nervous-system.config.json'),
|
|
19
|
+
path.join(os.homedir(), '.nervous-system', 'config.json'),
|
|
20
|
+
path.join(__dirname, 'nervous-system.config.json'),
|
|
21
|
+
].filter(Boolean);
|
|
22
|
+
|
|
23
|
+
for (const cp of configPaths) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(cp, 'utf8');
|
|
26
|
+
const cfg = JSON.parse(raw);
|
|
27
|
+
cfg._source = cp;
|
|
28
|
+
return cfg;
|
|
29
|
+
} catch (e) { continue; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return defaults that work for any project
|
|
33
|
+
return {
|
|
34
|
+
_source: 'defaults',
|
|
35
|
+
project_root: process.cwd(),
|
|
36
|
+
data_dir: null,
|
|
37
|
+
logs_dir: null,
|
|
38
|
+
html_dir: null,
|
|
39
|
+
protected_files_list: null,
|
|
40
|
+
config_file: null,
|
|
41
|
+
roles_file: null,
|
|
42
|
+
docs_to_audit: [],
|
|
43
|
+
pm2_managed: false,
|
|
44
|
+
html_pages: [],
|
|
45
|
+
package_json: null,
|
|
46
|
+
github_repo: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PROJECT = loadProjectConfig();
|
|
51
|
+
|
|
52
|
+
function projectPath(key) {
|
|
53
|
+
const val = PROJECT[key];
|
|
54
|
+
if (!val) return null;
|
|
55
|
+
if (path.isAbsolute(val)) return val;
|
|
56
|
+
return path.join(PROJECT.project_root || process.cwd(), val);
|
|
57
|
+
}
|
|
58
|
+
|
|
8
59
|
const PORT = 3475;
|
|
9
60
|
|
|
10
61
|
const KILL_SECRET = process.env.KILL_SECRET || 'ns-kill-2026';
|
|
11
|
-
const AUDIT_CHAIN_FILE = '
|
|
12
|
-
const VIOLATIONS_LOG = '
|
|
62
|
+
const AUDIT_CHAIN_FILE = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'audit-chain.json') : path.join(os.homedir(), '.nervous-system', 'audit-chain.json');
|
|
63
|
+
const VIOLATIONS_LOG = projectPath('logs_dir') ? path.join(projectPath('logs_dir'), 'guardrail-violations.log') : path.join(os.homedir(), '.nervous-system', 'guardrail-violations.log');
|
|
13
64
|
const GENESIS_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
14
65
|
const activeDispatches = [];
|
|
15
66
|
const MAX_CONCURRENT_DISPATCHES = 2;
|
|
@@ -108,7 +159,7 @@ function dispatchToLLM(task, maxTurns) {
|
|
|
108
159
|
const freeMB = getFreeMB();
|
|
109
160
|
if (freeMB < 500) return { dispatched: false, error: `Insufficient RAM: ${freeMB}MB free (need 500MB+)` };
|
|
110
161
|
const ts = Date.now();
|
|
111
|
-
const logFile =
|
|
162
|
+
const logFile = projectPath('logs_dir') ? `${projectPath('logs_dir')}/dispatch-${ts}.log` : path.join(os.homedir(), '.nervous-system', `dispatch-${ts}.log`);
|
|
112
163
|
const turns = maxTurns || 15;
|
|
113
164
|
try {
|
|
114
165
|
const escaped = task.replace(/"/g, '\\"');
|
|
@@ -129,7 +180,7 @@ const MCP_VERSION = '2024-11-05';
|
|
|
129
180
|
// Server info
|
|
130
181
|
const SERVER_INFO = {
|
|
131
182
|
name: 'nervous-system',
|
|
132
|
-
version: '1.
|
|
183
|
+
version: '1.6.0'
|
|
133
184
|
};
|
|
134
185
|
|
|
135
186
|
// ============================================================
|
|
@@ -138,7 +189,7 @@ const SERVER_INFO = {
|
|
|
138
189
|
|
|
139
190
|
const FRAMEWORK = {
|
|
140
191
|
name: 'The Nervous System',
|
|
141
|
-
version: '1.
|
|
192
|
+
version: '1.6.0',
|
|
142
193
|
author: 'Arthur Palyan',
|
|
143
194
|
tagline: 'Anthropic built the brain. Arthur built the nervous system that keeps it from hurting itself.',
|
|
144
195
|
problem: 'LLMs lose context between sessions, loop on problems instead of dispatching, silently fail without progress notes, edit protected files, drift from the real problem, and solve instead of asking.',
|
|
@@ -649,6 +700,21 @@ const TOOLS = [
|
|
|
649
700
|
page: { type: 'string', description: "Specific page to check (e.g. 'gateway.html'), or 'all' for everything" }
|
|
650
701
|
}
|
|
651
702
|
}
|
|
703
|
+
},
|
|
704
|
+
// NEW: Pre-Publish Audit
|
|
705
|
+
{
|
|
706
|
+
name: 'pre_publish_audit',
|
|
707
|
+
annotations: { title: 'Pre-Publish Audit', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
708
|
+
description: 'Scans the Nervous System source code itself before publishing. Catches hardcoded secrets, personal data, non-portable paths, and internal naming that should not ship to clients. RUN THIS BEFORE EVERY npm publish.',
|
|
709
|
+
inputSchema: {
|
|
710
|
+
type: 'object',
|
|
711
|
+
properties: {
|
|
712
|
+
source_file: {
|
|
713
|
+
type: 'string',
|
|
714
|
+
description: 'Path to the NS source file to audit. Defaults to own index.js'
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
652
718
|
}
|
|
653
719
|
];
|
|
654
720
|
|
|
@@ -686,7 +752,10 @@ function safeReadFile(filePath) {
|
|
|
686
752
|
function auditRoles() {
|
|
687
753
|
const drifts = [];
|
|
688
754
|
let cleanChecks = 0;
|
|
689
|
-
const rolesFile = '
|
|
755
|
+
const rolesFile = projectPath('roles_file');
|
|
756
|
+
if (!rolesFile) {
|
|
757
|
+
return { drifts: [], cleanChecks: 0, skipped: 'roles_file not configured' };
|
|
758
|
+
}
|
|
690
759
|
const roles = safeReadJSON(rolesFile);
|
|
691
760
|
if (!roles || !roles.members) {
|
|
692
761
|
drifts.push({ type: 'missing_source', source: rolesFile, target: '', field: '', expected: 'valid JSON with members array', found: 'missing or invalid' });
|
|
@@ -699,7 +768,8 @@ function auditRoles() {
|
|
|
699
768
|
}
|
|
700
769
|
|
|
701
770
|
// Check family-status.json
|
|
702
|
-
const statusFile = '
|
|
771
|
+
const statusFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-status.json') : null;
|
|
772
|
+
if (!statusFile) { return { drifts, cleanChecks }; }
|
|
703
773
|
const status = safeReadJSON(statusFile);
|
|
704
774
|
if (status && status.members) {
|
|
705
775
|
for (const m of status.members) {
|
|
@@ -715,8 +785,8 @@ function auditRoles() {
|
|
|
715
785
|
}
|
|
716
786
|
|
|
717
787
|
// Check system-config.json
|
|
718
|
-
const configFile = '
|
|
719
|
-
const config = safeReadJSON(configFile);
|
|
788
|
+
const configFile = projectPath('config_file');
|
|
789
|
+
const config = configFile ? safeReadJSON(configFile) : null;
|
|
720
790
|
if (config && config.family_members) {
|
|
721
791
|
for (const m of config.family_members) {
|
|
722
792
|
const src = sourceRoles[m.id];
|
|
@@ -731,8 +801,8 @@ function auditRoles() {
|
|
|
731
801
|
}
|
|
732
802
|
|
|
733
803
|
// Check family-guide.json
|
|
734
|
-
const guideFile = '
|
|
735
|
-
const guide = safeReadJSON(guideFile);
|
|
804
|
+
const guideFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-guide.json') : null;
|
|
805
|
+
const guide = guideFile ? safeReadJSON(guideFile) : null;
|
|
736
806
|
if (guide && guide.members) {
|
|
737
807
|
for (const m of guide.members) {
|
|
738
808
|
const src = sourceRoles[m.id];
|
|
@@ -747,11 +817,12 @@ function auditRoles() {
|
|
|
747
817
|
}
|
|
748
818
|
|
|
749
819
|
// Check HTML files for role references
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
{ path: '
|
|
753
|
-
{ path: '
|
|
754
|
-
|
|
820
|
+
const htmlDir = projectPath('html_dir');
|
|
821
|
+
const htmlFiles = htmlDir ? [
|
|
822
|
+
{ path: path.join(htmlDir, 'index.html'), name: 'index.html' },
|
|
823
|
+
{ path: path.join(htmlDir, 'explorer.html'), name: 'explorer.html' },
|
|
824
|
+
{ path: path.join(htmlDir, 'meet.html'), name: 'meet.html' }
|
|
825
|
+
] : [];
|
|
755
826
|
for (const hf of htmlFiles) {
|
|
756
827
|
const content = safeReadFile(hf.path);
|
|
757
828
|
if (!content) continue;
|
|
@@ -761,7 +832,7 @@ function auditRoles() {
|
|
|
761
832
|
}
|
|
762
833
|
|
|
763
834
|
// Check mcp-ops-server.js
|
|
764
|
-
const opsContent = safeReadFile('
|
|
835
|
+
const opsContent = projectPath('project_root') ? safeReadFile(path.join(projectPath('project_root') || process.cwd(), 'mcp-ops-server.js')) : null;
|
|
765
836
|
if (opsContent) {
|
|
766
837
|
for (const [id, src] of Object.entries(sourceRoles)) {
|
|
767
838
|
if (opsContent.includes(`"${src.aka}"`) || opsContent.includes(`'${src.aka}'`)) {
|
|
@@ -776,7 +847,10 @@ function auditRoles() {
|
|
|
776
847
|
function auditVersions() {
|
|
777
848
|
const drifts = [];
|
|
778
849
|
let cleanChecks = 0;
|
|
779
|
-
const pkgFile = '
|
|
850
|
+
const pkgFile = projectPath('package_json');
|
|
851
|
+
if (!pkgFile) {
|
|
852
|
+
return { drifts: [], cleanChecks: 0, skipped: 'package_json not configured' };
|
|
853
|
+
}
|
|
780
854
|
const pkg = safeReadJSON(pkgFile);
|
|
781
855
|
const expectedVersion = pkg ? pkg.version : null;
|
|
782
856
|
if (!expectedVersion) {
|
|
@@ -785,7 +859,8 @@ function auditVersions() {
|
|
|
785
859
|
}
|
|
786
860
|
|
|
787
861
|
// Check SERVER_INFO.version and FRAMEWORK.version in index.js
|
|
788
|
-
const
|
|
862
|
+
const ghRepo = projectPath('github_repo');
|
|
863
|
+
const indexContent = ghRepo ? safeReadFile(path.join(ghRepo, 'index.js')) : null;
|
|
789
864
|
if (indexContent) {
|
|
790
865
|
const siMatch = indexContent.match(/SERVER_INFO\s*=\s*\{[^}]*version:\s*'([^']+)'/);
|
|
791
866
|
if (siMatch) {
|
|
@@ -826,7 +901,7 @@ function auditVersions() {
|
|
|
826
901
|
}
|
|
827
902
|
|
|
828
903
|
// Check BUSINESS_BUILDER.md
|
|
829
|
-
const bbContent = safeReadFile('
|
|
904
|
+
const bbContent = projectPath('data_dir') ? safeReadFile(path.join(projectPath('data_dir'), 'BUSINESS_BUILDER.md')) : null;
|
|
830
905
|
if (bbContent) {
|
|
831
906
|
const bbMatch = bbContent.match(/[Nn]ervous [Ss]ystem.*?v?(\d+\.\d+\.\d+)/);
|
|
832
907
|
if (bbMatch && bbMatch[1] !== expectedVersion) {
|
|
@@ -835,7 +910,7 @@ function auditVersions() {
|
|
|
835
910
|
}
|
|
836
911
|
|
|
837
912
|
// Check gateway.html
|
|
838
|
-
const gwContent = safeReadFile('
|
|
913
|
+
const gwContent = projectPath('html_dir') ? safeReadFile(path.join(projectPath('html_dir'), 'gateway.html')) : null;
|
|
839
914
|
if (gwContent) {
|
|
840
915
|
const gwMatch = gwContent.match(/[Vv]ersion[:\s]*v?(\d+\.\d+\.\d+)/);
|
|
841
916
|
if (gwMatch && gwMatch[1] !== expectedVersion) {
|
|
@@ -844,7 +919,7 @@ function auditVersions() {
|
|
|
844
919
|
}
|
|
845
920
|
|
|
846
921
|
// Check README.md
|
|
847
|
-
const readmeContent = safeReadFile('
|
|
922
|
+
const readmeContent = ghRepo ? safeReadFile(path.join(ghRepo, 'README.md')) : null;
|
|
848
923
|
if (readmeContent) {
|
|
849
924
|
const rmMatch = readmeContent.match(/[Vv]ersion[:\s]*v?(\d+\.\d+\.\d+)/);
|
|
850
925
|
if (rmMatch && rmMatch[1] !== expectedVersion) {
|
|
@@ -853,7 +928,7 @@ function auditVersions() {
|
|
|
853
928
|
}
|
|
854
929
|
|
|
855
930
|
// Check family-roles.json stats
|
|
856
|
-
const roles = safeReadJSON('
|
|
931
|
+
const roles = projectPath('roles_file') ? safeReadJSON(projectPath('roles_file')) : null;
|
|
857
932
|
if (roles && roles.stats) {
|
|
858
933
|
if (roles.stats.ns_version && roles.stats.ns_version !== expectedVersion) {
|
|
859
934
|
drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'family-roles.json', field: 'stats.ns_version', expected: expectedVersion, found: roles.stats.ns_version });
|
|
@@ -874,10 +949,13 @@ function auditFiles() {
|
|
|
874
949
|
let cleanChecks = 0;
|
|
875
950
|
|
|
876
951
|
// Check UNTOUCHABLE_FILES.txt - verify each file exists
|
|
877
|
-
const
|
|
952
|
+
const untouchableFile = projectPath('protected_files_list');
|
|
953
|
+
const untouchableContent = untouchableFile ? safeReadFile(untouchableFile) : null;
|
|
878
954
|
if (untouchableContent) {
|
|
879
955
|
const lines = untouchableContent.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
880
|
-
for (const
|
|
956
|
+
for (const rawLine of lines) {
|
|
957
|
+
const filePath = rawLine.split(/\s*[\(\#]/)[0].trim();
|
|
958
|
+
if (!filePath || !filePath.startsWith('/')) continue;
|
|
881
959
|
if (fs.existsSync(filePath)) {
|
|
882
960
|
cleanChecks++;
|
|
883
961
|
} else {
|
|
@@ -887,10 +965,8 @@ function auditFiles() {
|
|
|
887
965
|
}
|
|
888
966
|
|
|
889
967
|
// Check LLM_STARTUP.md and BUSINESS_BUILDER.md for file references
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
{ path: '/root/family-data/BUSINESS_BUILDER.md', name: 'BUSINESS_BUILDER.md' }
|
|
893
|
-
];
|
|
968
|
+
const docsToAudit = PROJECT.docs_to_audit || [];
|
|
969
|
+
const docsToCheck = docsToAudit.map(p => ({ path: p, name: path.basename(p) }));
|
|
894
970
|
|
|
895
971
|
// Get PM2 running scripts
|
|
896
972
|
let pm2Scripts = {};
|
|
@@ -906,7 +982,8 @@ function auditFiles() {
|
|
|
906
982
|
const content = safeReadFile(doc.path);
|
|
907
983
|
if (!content) continue;
|
|
908
984
|
// Look for .js file references
|
|
909
|
-
|
|
985
|
+
// Match .js files but exclude .json, .jsonl, .jsx
|
|
986
|
+
const jsRefs = (content.match(/\/[^\s)]+\.js\b/g) || []).filter(r => !r.match(/\.json[l]?$/));
|
|
910
987
|
for (const ref of jsRefs) {
|
|
911
988
|
if (fs.existsSync(ref)) {
|
|
912
989
|
cleanChecks++;
|
|
@@ -928,7 +1005,8 @@ function auditFiles() {
|
|
|
928
1005
|
}
|
|
929
1006
|
|
|
930
1007
|
// Check system-config.json syntax_check_scripts
|
|
931
|
-
const
|
|
1008
|
+
const sysConfigFile = projectPath('config_file');
|
|
1009
|
+
const config = sysConfigFile ? safeReadJSON(sysConfigFile) : null;
|
|
932
1010
|
if (config && config.syntax_check_scripts) {
|
|
933
1011
|
for (const script of config.syntax_check_scripts) {
|
|
934
1012
|
if (fs.existsSync(script)) {
|
|
@@ -955,10 +1033,10 @@ function auditProcesses() {
|
|
|
955
1033
|
return { drifts, cleanChecks };
|
|
956
1034
|
}
|
|
957
1035
|
|
|
958
|
-
const
|
|
1036
|
+
const procConfigFile = projectPath('config_file');
|
|
1037
|
+
const config = procConfigFile ? safeReadJSON(procConfigFile) : null;
|
|
959
1038
|
if (!config || !config.processes) {
|
|
960
|
-
|
|
961
|
-
const roles = safeReadJSON('/root/family-data/family-roles.json');
|
|
1039
|
+
const roles = projectPath('roles_file') ? safeReadJSON(projectPath('roles_file')) : null;
|
|
962
1040
|
if (roles && roles.members) {
|
|
963
1041
|
const expectedProcs = [];
|
|
964
1042
|
for (const m of roles.members) {
|
|
@@ -1007,18 +1085,19 @@ function auditWebsite() {
|
|
|
1007
1085
|
let cleanChecks = 0;
|
|
1008
1086
|
|
|
1009
1087
|
// Source of truth values
|
|
1010
|
-
const pkgFile = '
|
|
1011
|
-
const pkg = safeReadJSON(pkgFile);
|
|
1012
|
-
const expectedVersion = pkg ? pkg.version :
|
|
1088
|
+
const pkgFile = projectPath('package_json');
|
|
1089
|
+
const pkg = pkgFile ? safeReadJSON(pkgFile) : null;
|
|
1090
|
+
const expectedVersion = pkg ? pkg.version : SERVER_INFO.version;
|
|
1013
1091
|
const actualToolCount = TOOLS.length;
|
|
1014
1092
|
const actualResourceCount = RESOURCES.length;
|
|
1015
1093
|
|
|
1016
|
-
const roles = safeReadJSON('
|
|
1094
|
+
const roles = projectPath('roles_file') ? safeReadJSON(projectPath('roles_file')) : null;
|
|
1017
1095
|
const expectedMemberCount = roles && roles.stats ? roles.stats.member_count : 11;
|
|
1018
1096
|
const expectedProcessCount = roles && roles.stats ? roles.stats.process_count : 28;
|
|
1019
1097
|
|
|
1020
1098
|
// Count protected files (non-comment, non-blank lines starting with /)
|
|
1021
|
-
const
|
|
1099
|
+
const protListFile = projectPath('protected_files_list');
|
|
1100
|
+
const untouchableContent = protListFile ? safeReadFile(protListFile) : null;
|
|
1022
1101
|
let protectedFileCount = 0;
|
|
1023
1102
|
if (untouchableContent) {
|
|
1024
1103
|
protectedFileCount = untouchableContent.split('\n').filter(l => l.trim() && !l.trim().startsWith('#') && l.trim().startsWith('/')).length;
|
|
@@ -1028,7 +1107,10 @@ function auditWebsite() {
|
|
|
1028
1107
|
const roleNames = roles && roles.members ? roles.members.map(m => m.name) : [];
|
|
1029
1108
|
|
|
1030
1109
|
// 1. Check all .html files in /root/family-home/
|
|
1031
|
-
const familyHomeDir = '
|
|
1110
|
+
const familyHomeDir = projectPath('html_dir');
|
|
1111
|
+
if (!familyHomeDir) {
|
|
1112
|
+
return { drifts: [], cleanChecks: 0, skipped: 'html_dir not configured' };
|
|
1113
|
+
}
|
|
1032
1114
|
let htmlFiles = [];
|
|
1033
1115
|
try {
|
|
1034
1116
|
htmlFiles = fs.readdirSync(familyHomeDir).filter(f => f.endsWith('.html')).map(f => familyHomeDir + f);
|
|
@@ -1091,7 +1173,8 @@ function auditWebsite() {
|
|
|
1091
1173
|
}
|
|
1092
1174
|
|
|
1093
1175
|
// 2. Check family-guide.json
|
|
1094
|
-
const
|
|
1176
|
+
const guideFile2 = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-guide.json') : null;
|
|
1177
|
+
const guide = guideFile2 ? safeReadJSON(guideFile2) : null;
|
|
1095
1178
|
if (guide) {
|
|
1096
1179
|
const guideStr = JSON.stringify(guide);
|
|
1097
1180
|
// Check version refs
|
|
@@ -1117,7 +1200,7 @@ function auditWebsite() {
|
|
|
1117
1200
|
} else { cleanChecks++; }
|
|
1118
1201
|
|
|
1119
1202
|
// 3. Check mcp-stripe-checkout.js for version refs
|
|
1120
|
-
const checkoutContent = safeReadFile('
|
|
1203
|
+
const checkoutContent = projectPath('project_root') ? safeReadFile(path.join(projectPath('project_root') || process.cwd(), 'mcp-stripe-checkout.js')) : null;
|
|
1121
1204
|
if (checkoutContent) {
|
|
1122
1205
|
const checkoutVersions = checkoutContent.match(/v(\d+\.\d+\.\d+)/g) || [];
|
|
1123
1206
|
for (const cv of checkoutVersions) {
|
|
@@ -1130,7 +1213,8 @@ function auditWebsite() {
|
|
|
1130
1213
|
}
|
|
1131
1214
|
|
|
1132
1215
|
// 4. Check system-config.json for version/tool counts
|
|
1133
|
-
const
|
|
1216
|
+
const sysConfigFile2 = projectPath('config_file');
|
|
1217
|
+
const sysConfig = sysConfigFile2 ? safeReadJSON(sysConfigFile2) : null;
|
|
1134
1218
|
if (sysConfig) {
|
|
1135
1219
|
const scStr = JSON.stringify(sysConfig);
|
|
1136
1220
|
const scVersions = scStr.match(/v(\d+\.\d+\.\d+)/g) || [];
|
|
@@ -1144,7 +1228,7 @@ function auditWebsite() {
|
|
|
1144
1228
|
}
|
|
1145
1229
|
|
|
1146
1230
|
// 5. Check FREE_TOOLS in mcp-api-middleware.js match actual tool names
|
|
1147
|
-
const middlewareContent = safeReadFile('
|
|
1231
|
+
const middlewareContent = projectPath('project_root') ? safeReadFile(path.join(projectPath('project_root') || process.cwd(), 'mcp-api-middleware.js')) : null;
|
|
1148
1232
|
if (middlewareContent) {
|
|
1149
1233
|
const freeToolsMatch = middlewareContent.match(/'nervous-system':\s*\[([^\]]+)\]/);
|
|
1150
1234
|
if (freeToolsMatch) {
|
|
@@ -1164,7 +1248,7 @@ function auditWebsite() {
|
|
|
1164
1248
|
}
|
|
1165
1249
|
|
|
1166
1250
|
// 6. Check sitemap.xml has all public pages
|
|
1167
|
-
const sitemapContent = safeReadFile('
|
|
1251
|
+
const sitemapContent = familyHomeDir ? safeReadFile(path.join(familyHomeDir, 'sitemap.xml')) : null;
|
|
1168
1252
|
if (sitemapContent && htmlFiles.length > 0) {
|
|
1169
1253
|
const publicPages = htmlFiles.filter(f => {
|
|
1170
1254
|
const name = f.split('/').pop();
|
|
@@ -1226,13 +1310,21 @@ function runSecurityAudit() {
|
|
|
1226
1310
|
let checksPassed = 0;
|
|
1227
1311
|
|
|
1228
1312
|
// 1. Scan HTML files for hardcoded passwords/secrets
|
|
1229
|
-
const htmlDir = '
|
|
1313
|
+
const htmlDir = projectPath('html_dir');
|
|
1314
|
+
if (!htmlDir) {
|
|
1315
|
+
return { status: 'skipped', vulnerability_count: 0, checks_passed: 0, vulnerabilities: [], skipped: 'html_dir not configured' };
|
|
1316
|
+
}
|
|
1230
1317
|
const secretPatterns = [
|
|
1231
|
-
/\d{10}:AA[A-Za-z0-9_-]{30,}/g,
|
|
1232
|
-
/sk-ant-[a-zA-Z0-9_-]+/g,
|
|
1233
|
-
/
|
|
1234
|
-
/
|
|
1235
|
-
/
|
|
1318
|
+
/\d{10}:AA[A-Za-z0-9_-]{30,}/g, // Telegram bot tokens
|
|
1319
|
+
/sk-ant-[a-zA-Z0-9_-]+/g, // Anthropic API keys
|
|
1320
|
+
/sk_live_[a-zA-Z0-9]+/g, // Stripe live keys
|
|
1321
|
+
/sk_test_[a-zA-Z0-9]+/g, // Stripe test keys
|
|
1322
|
+
/npm_[A-Za-z0-9]{20,}/g, // npm tokens
|
|
1323
|
+
/ghp_[A-Za-z0-9]{20,}/g, // GitHub PATs
|
|
1324
|
+
/BOT_TOKEN\s*[:=]\s*['"][^'"]+['"]/gi, // Generic bot tokens
|
|
1325
|
+
/password\s*[:=]\s*['"][^'"]{8,}['"]/gi, // Hardcoded passwords
|
|
1326
|
+
/api[_-]?key\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}['"]/gi, // API keys
|
|
1327
|
+
/secret\s*[:=]\s*['"][A-Za-z0-9_\-]{16,}['"]/gi, // Secrets
|
|
1236
1328
|
];
|
|
1237
1329
|
try {
|
|
1238
1330
|
const htmlFiles = fs.readdirSync(htmlDir).filter(f => f.endsWith('.html'));
|
|
@@ -1240,11 +1332,24 @@ function runSecurityAudit() {
|
|
|
1240
1332
|
const content = safeReadFile(htmlDir + hf);
|
|
1241
1333
|
if (!content) continue;
|
|
1242
1334
|
let fileClean = true;
|
|
1335
|
+
const contentLines = content.split('\n');
|
|
1243
1336
|
for (const pat of secretPatterns) {
|
|
1244
1337
|
pat.lastIndex = 0;
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1338
|
+
let realMatches = 0;
|
|
1339
|
+
for (const line of contentLines) {
|
|
1340
|
+
// Skip lines that are defining detection patterns (not actual secrets)
|
|
1341
|
+
if (line.trim().match(/^\s*\/.*\/[gim]*,?\s*$/) ||
|
|
1342
|
+
line.includes('SENS_PAT') ||
|
|
1343
|
+
line.includes('secretPatterns') ||
|
|
1344
|
+
line.includes('leakPatterns') ||
|
|
1345
|
+
line.includes('dangerPatterns') ||
|
|
1346
|
+
line.includes('redact')) continue;
|
|
1347
|
+
pat.lastIndex = 0;
|
|
1348
|
+
const m = line.match(pat);
|
|
1349
|
+
if (m) realMatches += m.length;
|
|
1350
|
+
}
|
|
1351
|
+
if (realMatches > 0) {
|
|
1352
|
+
vulnerabilities.push({ type: 'hardcoded_secret', file: hf, pattern: pat.source, count: realMatches });
|
|
1248
1353
|
fileClean = false;
|
|
1249
1354
|
}
|
|
1250
1355
|
}
|
|
@@ -1255,7 +1360,7 @@ function runSecurityAudit() {
|
|
|
1255
1360
|
}
|
|
1256
1361
|
|
|
1257
1362
|
// 2. Check auth endpoints use server-side validation
|
|
1258
|
-
const serverContent = safeReadFile('
|
|
1363
|
+
const serverContent = htmlDir ? safeReadFile(path.join(htmlDir, 'server.js')) : null;
|
|
1259
1364
|
if (serverContent) {
|
|
1260
1365
|
if (serverContent.includes('getSessionFromReq') || serverContent.includes('getAccessLevel')) {
|
|
1261
1366
|
checksPassed++;
|
|
@@ -1287,7 +1392,8 @@ function runSecurityAudit() {
|
|
|
1287
1392
|
}
|
|
1288
1393
|
|
|
1289
1394
|
// 5. Check bridge rate limiting
|
|
1290
|
-
|
|
1395
|
+
const projRoot = projectPath('project_root') || process.cwd();
|
|
1396
|
+
if (serverContent && serverContent.includes('rate') || fs.existsSync(path.join(projRoot, 'rate-limit.js')) || fs.existsSync(path.join(projRoot, 'bridge-ratelimit.js'))) {
|
|
1291
1397
|
checksPassed++;
|
|
1292
1398
|
} else {
|
|
1293
1399
|
vulnerabilities.push({ type: 'missing_rate_limit', file: 'bridge', detail: 'No rate limiting found for bridge' });
|
|
@@ -1311,7 +1417,8 @@ function runSecurityAudit() {
|
|
|
1311
1417
|
|
|
1312
1418
|
// 7. Check api-credentials.json permissions
|
|
1313
1419
|
try {
|
|
1314
|
-
const credFile = '
|
|
1420
|
+
const credFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'api-credentials.json') : null;
|
|
1421
|
+
if (!credFile) { checksPassed++; }
|
|
1315
1422
|
if (fs.existsSync(credFile)) {
|
|
1316
1423
|
const stats = fs.statSync(credFile);
|
|
1317
1424
|
const mode = (stats.mode & 0o777).toString(8);
|
|
@@ -1363,10 +1470,14 @@ function runSecurityAudit() {
|
|
|
1363
1470
|
|
|
1364
1471
|
function runAutoPropagators() {
|
|
1365
1472
|
const results = [];
|
|
1473
|
+
const workersDir = projectPath('project_root') ? path.join(projectPath('project_root') || process.cwd(), 'family-workers') : null;
|
|
1474
|
+
if (!workersDir || !fs.existsSync(workersDir)) {
|
|
1475
|
+
return { timestamp: new Date().toISOString(), propagators_run: 0, results: [], skipped: 'family-workers directory not found' };
|
|
1476
|
+
}
|
|
1366
1477
|
const scripts = [
|
|
1367
|
-
{ name: 'role', path: '
|
|
1368
|
-
{ name: 'version', path: '
|
|
1369
|
-
{ name: 'content', path: '
|
|
1478
|
+
{ name: 'role', path: path.join(workersDir, 'role-propagator.js') },
|
|
1479
|
+
{ name: 'version', path: path.join(workersDir, 'version-propagator.js') },
|
|
1480
|
+
{ name: 'content', path: path.join(workersDir, 'content-propagator.js') }
|
|
1370
1481
|
];
|
|
1371
1482
|
for (const script of scripts) {
|
|
1372
1483
|
try {
|
|
@@ -1389,7 +1500,10 @@ function runAutoPropagators() {
|
|
|
1389
1500
|
// ============================================================
|
|
1390
1501
|
|
|
1391
1502
|
function runPageHealth(page) {
|
|
1392
|
-
const FAMILY_HOME = '
|
|
1503
|
+
const FAMILY_HOME = projectPath('html_dir');
|
|
1504
|
+
if (!FAMILY_HOME) {
|
|
1505
|
+
return { status: 'skipped', pages_checked: 0, issue_count: 0, issues: [], skipped: 'html_dir not configured' };
|
|
1506
|
+
}
|
|
1393
1507
|
const issues = [];
|
|
1394
1508
|
|
|
1395
1509
|
let htmlFiles;
|
|
@@ -1543,6 +1657,111 @@ function runPageHealth(page) {
|
|
|
1543
1657
|
};
|
|
1544
1658
|
}
|
|
1545
1659
|
|
|
1660
|
+
|
|
1661
|
+
// ============================================================
|
|
1662
|
+
// PRE-PUBLISH AUDIT ENGINE
|
|
1663
|
+
// ============================================================
|
|
1664
|
+
|
|
1665
|
+
function runPrePublishAudit(sourceFile) {
|
|
1666
|
+
const findings = [];
|
|
1667
|
+
const file = sourceFile || __filename;
|
|
1668
|
+
let content;
|
|
1669
|
+
try {
|
|
1670
|
+
content = fs.readFileSync(file, 'utf8');
|
|
1671
|
+
} catch (e) {
|
|
1672
|
+
return { status: 'error', error: 'Cannot read file: ' + e.message };
|
|
1673
|
+
}
|
|
1674
|
+
const lines = content.split('\n');
|
|
1675
|
+
|
|
1676
|
+
// 1. Check for hardcoded absolute paths (non-portable)
|
|
1677
|
+
lines.forEach((line, idx) => {
|
|
1678
|
+
if (line.trim().startsWith('//')) return;
|
|
1679
|
+
if (line.includes('description:') || line.includes('context:')) return;
|
|
1680
|
+
if (line.includes('description,') || line.includes("description'")) return;
|
|
1681
|
+
|
|
1682
|
+
if (line.match(/['"\`]\/root\//)) {
|
|
1683
|
+
findings.push({
|
|
1684
|
+
type: 'hardcoded_path',
|
|
1685
|
+
line: idx + 1,
|
|
1686
|
+
preview: line.trim().substring(0, 100),
|
|
1687
|
+
fix: 'Use projectPath() or configurable path'
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
if (line.match(/['"\`]\/home\//)) {
|
|
1691
|
+
findings.push({
|
|
1692
|
+
type: 'hardcoded_path',
|
|
1693
|
+
line: idx + 1,
|
|
1694
|
+
preview: line.trim().substring(0, 100),
|
|
1695
|
+
fix: 'Use projectPath() or os.homedir()'
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// 2. Check for personal data that should not ship
|
|
1701
|
+
const personalPatterns = [
|
|
1702
|
+
{ name: 'email_address', pat: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
|
|
1703
|
+
{ name: 'phone_number', pat: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g },
|
|
1704
|
+
{ name: 'ip_address', pat: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g },
|
|
1705
|
+
];
|
|
1706
|
+
lines.forEach((line, idx) => {
|
|
1707
|
+
if (line.trim().startsWith('//') || line.includes('description')) return;
|
|
1708
|
+
if (line.includes('regex') || line.includes('pattern') || line.includes('pat:')) return;
|
|
1709
|
+
for (const pp of personalPatterns) {
|
|
1710
|
+
pp.pat.lastIndex = 0;
|
|
1711
|
+
if (pp.pat.test(line)) {
|
|
1712
|
+
findings.push({
|
|
1713
|
+
type: 'personal_data',
|
|
1714
|
+
subtype: pp.name,
|
|
1715
|
+
line: idx + 1,
|
|
1716
|
+
preview: line.trim().substring(0, 100),
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
// 3. Check for internal naming that should be generic
|
|
1723
|
+
const internalTerms = [
|
|
1724
|
+
'family-data', 'family-home', 'family-logs', 'family-roles',
|
|
1725
|
+
'family-guide', 'family-status', 'family-workers',
|
|
1726
|
+
'PAPA_FULL', 'PAPA_READ', 'ARTHUR_CHAT_ID',
|
|
1727
|
+
];
|
|
1728
|
+
lines.forEach((line, idx) => {
|
|
1729
|
+
if (line.trim().startsWith('//')) return;
|
|
1730
|
+
if (line.includes('description:') || line.includes('context:') || line.includes('tagline:')) return;
|
|
1731
|
+
if (line.includes('origin_story')) return;
|
|
1732
|
+
for (const term of internalTerms) {
|
|
1733
|
+
if (line.toLowerCase().includes(term.toLowerCase()) &&
|
|
1734
|
+
!line.includes('// ')) {
|
|
1735
|
+
findings.push({
|
|
1736
|
+
type: 'internal_naming',
|
|
1737
|
+
term: term,
|
|
1738
|
+
line: idx + 1,
|
|
1739
|
+
preview: line.trim().substring(0, 100),
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
// 4. GATE: Block publish if critical findings
|
|
1746
|
+
const critical = findings.filter(f =>
|
|
1747
|
+
f.type === 'personal_data' ||
|
|
1748
|
+
(f.type === 'hardcoded_path' && !f.preview.includes('description'))
|
|
1749
|
+
);
|
|
1750
|
+
|
|
1751
|
+
return {
|
|
1752
|
+
status: findings.length === 0 ? 'ready_to_publish' :
|
|
1753
|
+
critical.length > 0 ? 'BLOCKED_critical_findings' : 'warnings_only',
|
|
1754
|
+
total_findings: findings.length,
|
|
1755
|
+
critical_count: critical.length,
|
|
1756
|
+
findings: findings,
|
|
1757
|
+
recommendation: critical.length > 0 ?
|
|
1758
|
+
'DO NOT PUBLISH. Fix critical findings first.' :
|
|
1759
|
+
findings.length > 0 ?
|
|
1760
|
+
'Review warnings before publishing. None are blockers.' :
|
|
1761
|
+
'Clean. Safe to publish.'
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1546
1765
|
// ============================================================
|
|
1547
1766
|
// Handle tool calls
|
|
1548
1767
|
// ============================================================
|
|
@@ -1668,6 +1887,10 @@ function handleToolCall(name, args) {
|
|
|
1668
1887
|
return runPageHealth(args.page || 'all');
|
|
1669
1888
|
}
|
|
1670
1889
|
|
|
1890
|
+
case 'pre_publish_audit': {
|
|
1891
|
+
return runPrePublishAudit(args.source_file);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1671
1894
|
default:
|
|
1672
1895
|
return { error: 'Unknown tool' };
|
|
1673
1896
|
}
|
|
@@ -1789,7 +2012,7 @@ const server = http.createServer((req, res) => {
|
|
|
1789
2012
|
// Health check
|
|
1790
2013
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
1791
2014
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1792
|
-
res.end(JSON.stringify({ status: 'ok', service: 'nervous-system-mcp', version: '1.
|
|
2015
|
+
res.end(JSON.stringify({ status: 'ok', service: 'nervous-system-mcp', version: '1.6.0', protocol: MCP_VERSION }));
|
|
1793
2016
|
return;
|
|
1794
2017
|
}
|
|
1795
2018
|
|
|
@@ -1906,7 +2129,7 @@ const server = http.createServer((req, res) => {
|
|
|
1906
2129
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1907
2130
|
res.end(JSON.stringify({
|
|
1908
2131
|
name: 'The Nervous System MCP Server',
|
|
1909
|
-
version: '1.
|
|
2132
|
+
version: '1.6.0',
|
|
1910
2133
|
protocol: MCP_VERSION,
|
|
1911
2134
|
description: 'LLM behavioral enforcement framework. 7 core rules, preflight checks, session handoffs, worklogs, violation logging, kill switch, hash-chained audit, and forced reflection cycles. Built by Arthur Palyan.',
|
|
1912
2135
|
endpoints: {
|
|
@@ -1928,8 +2151,8 @@ const server = http.createServer((req, res) => {
|
|
|
1928
2151
|
migrateExistingViolations();
|
|
1929
2152
|
|
|
1930
2153
|
server.listen(PORT, '127.0.0.1', () => {
|
|
1931
|
-
console.error(`[MCP Server] Nervous System v1.
|
|
2154
|
+
console.error(`[MCP Server] Nervous System v1.6.0 running on port ${PORT}`);
|
|
1932
2155
|
console.error(`[MCP Server] SSE: /sse | HTTP: /mcp | Health: /health | Kill: POST /kill | Audit: GET /audit/verify | Dispatches: GET /dispatches`);
|
|
1933
2156
|
console.error(`[MCP Server] Protocol: ${MCP_VERSION}`);
|
|
1934
|
-
console.error(`[MCP Server] Tools: ${TOOLS.length} (including kill switch, audit chain, dispatch, drift audit, page health)`);
|
|
2157
|
+
console.error(`[MCP Server] Tools: ${TOOLS.length} (including kill switch, audit chain, dispatch, drift audit, page health, pre-publish audit)`);
|
|
1935
2158
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-nervous-system",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules,
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 17 tools including kill switch, audit chain, dispatch, drift audit, security audit, page health, pre-publish audit, and session close. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mcp-nervous-system": "./stdio.js"
|