remote-access-mcp 1.0.7 → 2.0.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/README.fa.md +29 -70
- package/README.md +92 -65
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +352 -133
- package/dist/cli/main.js.map +1 -1
- package/dist/core/audit.d.ts +40 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +96 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/config.d.ts +37 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +111 -34
- package/dist/core/config.js.map +1 -1
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +32 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/crypto.d.ts +6 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +16 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/policy.d.ts +19 -0
- package/dist/core/policy.d.ts.map +1 -1
- package/dist/core/policy.js +57 -0
- package/dist/core/policy.js.map +1 -1
- package/dist/core/rate-limit.d.ts +14 -0
- package/dist/core/rate-limit.d.ts.map +1 -0
- package/dist/core/rate-limit.js +67 -0
- package/dist/core/rate-limit.js.map +1 -0
- package/dist/server/app.d.ts +18 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +119 -25
- package/dist/server/app.js.map +1 -1
- package/dist/server/run.d.ts +5 -1
- package/dist/server/run.d.ts.map +1 -1
- package/dist/server/run.js +31 -8
- package/dist/server/run.js.map +1 -1
- package/dist/tools/filesystem.d.ts +3 -2
- package/dist/tools/filesystem.d.ts.map +1 -1
- package/dist/tools/filesystem.js +34 -23
- package/dist/tools/filesystem.js.map +1 -1
- package/dist/tools/git.d.ts +2 -2
- package/dist/tools/git.d.ts.map +1 -1
- package/dist/tools/git.js +33 -4
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/http.d.ts +2 -1
- package/dist/tools/http.d.ts.map +1 -1
- package/dist/tools/http.js +47 -5
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/index.d.ts +6 -3
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +29 -9
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/logs.d.ts +4 -0
- package/dist/tools/logs.d.ts.map +1 -0
- package/dist/tools/logs.js +78 -0
- package/dist/tools/logs.js.map +1 -0
- package/dist/tools/packages.d.ts +4 -0
- package/dist/tools/packages.d.ts.map +1 -0
- package/dist/tools/packages.js +75 -0
- package/dist/tools/packages.js.map +1 -0
- package/dist/tools/planning.d.ts +4 -0
- package/dist/tools/planning.d.ts.map +1 -0
- package/dist/tools/planning.js +118 -0
- package/dist/tools/planning.js.map +1 -0
- package/dist/tools/policy.d.ts +7 -2
- package/dist/tools/policy.d.ts.map +1 -1
- package/dist/tools/policy.js +29 -18
- package/dist/tools/policy.js.map +1 -1
- package/dist/tools/project.d.ts +4 -0
- package/dist/tools/project.d.ts.map +1 -0
- package/dist/tools/project.js +141 -0
- package/dist/tools/project.js.map +1 -0
- package/dist/tools/schedule.d.ts +17 -0
- package/dist/tools/schedule.d.ts.map +1 -0
- package/dist/tools/schedule.js +113 -0
- package/dist/tools/schedule.js.map +1 -0
- package/dist/tools/security.d.ts +4 -0
- package/dist/tools/security.d.ts.map +1 -0
- package/dist/tools/security.js +103 -0
- package/dist/tools/security.js.map +1 -0
- package/dist/tools/services.d.ts +4 -0
- package/dist/tools/services.d.ts.map +1 -0
- package/dist/tools/services.js +56 -0
- package/dist/tools/services.js.map +1 -0
- package/dist/tools/shell.d.ts +2 -2
- package/dist/tools/shell.d.ts.map +1 -1
- package/dist/tools/shell.js +25 -10
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/sqlite.d.ts +2 -2
- package/dist/tools/sqlite.d.ts.map +1 -1
- package/dist/tools/sqlite.js +44 -9
- package/dist/tools/sqlite.js.map +1 -1
- package/dist/tools/system.d.ts +2 -1
- package/dist/tools/system.d.ts.map +1 -1
- package/dist/tools/system.js +6 -2
- package/dist/tools/system.js.map +1 -1
- package/dist/tools/web.d.ts +4 -0
- package/dist/tools/web.d.ts.map +1 -0
- package/dist/tools/web.js +77 -0
- package/dist/tools/web.js.map +1 -0
- package/package.json +1 -1
package/dist/cli/main.js
CHANGED
|
@@ -2,13 +2,14 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import { loadConfig, saveConfig, generateToken, newTokenRecord, configPath, configDir, primaryToken, } from '../core/config.js';
|
|
6
7
|
import { resolveReal } from '../core/policy.js';
|
|
7
|
-
|
|
8
|
+
import { AuditLog } from '../core/audit.js';
|
|
8
9
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
9
10
|
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
|
-
// help
|
|
12
|
+
// help
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
const HELP = `${PKG.name} v${PKG.version} — turn any Linux server into an AI-agent-accessible machine
|
|
14
15
|
|
|
@@ -16,23 +17,48 @@ Usage:
|
|
|
16
17
|
ramcp <command> [options]
|
|
17
18
|
|
|
18
19
|
Commands:
|
|
19
|
-
init
|
|
20
|
-
start [--host H] [--port P]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
token
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
init Generate config + first token (safe to re-run)
|
|
21
|
+
start [--host H] [--port P]
|
|
22
|
+
[--read-only] Run the gateway in the foreground
|
|
23
|
+
url [--token ID|name] Print the MCP connector URL for chatbots
|
|
24
|
+
status Service status summary
|
|
25
|
+
doctor Diagnose token, port, nginx, DNS in one pass
|
|
26
|
+
version Print version
|
|
27
|
+
|
|
28
|
+
token list [--json] List tokens (fingerprints only)
|
|
29
|
+
token add --name N [opts] Create a token (see options below)
|
|
30
|
+
token show [ID|name] [--full] Show a token
|
|
31
|
+
token rotate [ID|name] Rotate a token (old one dies instantly)
|
|
32
|
+
token revoke ID|name Revoke (delete) a token
|
|
33
|
+
token add options:
|
|
34
|
+
--shell / --no-shell shell execution (default: off)
|
|
35
|
+
--paths a,b,c allowed paths (comma-separated)
|
|
36
|
+
--deny a,b denied paths
|
|
37
|
+
--scopes group,group tool groups (empty = all)
|
|
38
|
+
--read-only refuse mutating tools
|
|
39
|
+
--rpm N max requests per minute
|
|
40
|
+
--expires YYYY-MM-DD expiry date
|
|
41
|
+
|
|
42
|
+
policy [ID|name] Show a token's policy
|
|
43
|
+
policy allow <path> Allow a path (default token)
|
|
44
|
+
policy deny <path> Deny a path
|
|
45
|
+
policy shell on|off Enable/disable shell (default token)
|
|
46
|
+
policy readonly on|off Global read-only kill-switch
|
|
47
|
+
|
|
48
|
+
audit [query] [--tool T] [--since ISO] [--limit N]
|
|
49
|
+
[--verify] [--json] Query the tamper-evident audit log
|
|
50
|
+
audit chain Verify hash-chain integrity
|
|
51
|
+
|
|
52
|
+
service install [--domain D] Install systemd unit (+ nginx vhost)
|
|
53
|
+
service uninstall Remove service + nginx vhost
|
|
54
|
+
service logs [-f] Tail gateway logs
|
|
55
|
+
service status Detailed service check
|
|
56
|
+
|
|
57
|
+
schedule list List scheduled tasks
|
|
33
58
|
|
|
34
59
|
Options:
|
|
35
|
-
-h, --help
|
|
60
|
+
-h, --help Show this help
|
|
61
|
+
--json Machine-readable output (where supported)
|
|
36
62
|
`;
|
|
37
63
|
function parseArgs(argv) {
|
|
38
64
|
const [command = '', ...rest] = argv;
|
|
@@ -62,25 +88,47 @@ function parseArgs(argv) {
|
|
|
62
88
|
}
|
|
63
89
|
return { command, sub, flags, values };
|
|
64
90
|
}
|
|
91
|
+
const jsonOut = (args) => args.flags.has('json');
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// token lookup helper
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
function findToken(cfg, idOrName) {
|
|
96
|
+
if (!idOrName)
|
|
97
|
+
return primaryToken(cfg);
|
|
98
|
+
const t = cfg.tokens.find(t => t.id === idOrName || t.name === idOrName);
|
|
99
|
+
if (!t) {
|
|
100
|
+
console.error(`Token "${idOrName}" not found. Try \`ramcp token list\`.`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
return t;
|
|
104
|
+
}
|
|
105
|
+
function maskToken(t) {
|
|
106
|
+
return t.slice(0, 6) + '…' + t.slice(-4);
|
|
107
|
+
}
|
|
65
108
|
// ---------------------------------------------------------------------------
|
|
66
109
|
// init
|
|
67
110
|
// ---------------------------------------------------------------------------
|
|
68
|
-
function cmdInit() {
|
|
111
|
+
function cmdInit(args) {
|
|
69
112
|
const existing = fs.existsSync(configPath());
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
cfg
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
cfg = {
|
|
78
|
-
...loadConfig(),
|
|
79
|
-
token: generateToken(),
|
|
80
|
-
};
|
|
113
|
+
const cfg = existing ? loadConfig() : null;
|
|
114
|
+
if (cfg && cfg.tokens.length) {
|
|
115
|
+
console.log('Config already exists with ' + cfg.tokens.length + ' token(s) — nothing to do.');
|
|
116
|
+
console.log('Add more tokens with: ramcp token add --name <name>');
|
|
117
|
+
return;
|
|
81
118
|
}
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
const fresh = cfg || {
|
|
120
|
+
host: '127.0.0.1',
|
|
121
|
+
port: 8765,
|
|
122
|
+
public_host: '',
|
|
123
|
+
mcp_path: '/mcp',
|
|
124
|
+
log_level: 'info',
|
|
125
|
+
audit: { enabled: true, db_path: path.join(configDir(), 'audit.db') },
|
|
126
|
+
read_only: false,
|
|
127
|
+
tokens: [],
|
|
128
|
+
};
|
|
129
|
+
const rec = newTokenRecord({ name: 'default' });
|
|
130
|
+
fresh.tokens.push(rec);
|
|
131
|
+
saveConfig(fresh);
|
|
84
132
|
console.log(`
|
|
85
133
|
___ _____
|
|
86
134
|
| _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
|
|
@@ -88,144 +136,314 @@ function cmdInit() {
|
|
|
88
136
|
|_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
|
|
89
137
|
|___/ ${PKG.name} v${PKG.version}
|
|
90
138
|
|
|
91
|
-
${existing ? '✔ Existing config
|
|
92
|
-
✔ Token: ${
|
|
93
|
-
✔
|
|
139
|
+
${existing ? '✔ Existing config migrated to v2' : '✔ Config written to ' + configPath()}
|
|
140
|
+
✔ Token "default": ${maskToken(rec.token)} (full: ramcp token show default --full)
|
|
141
|
+
✔ Audit log: ${fresh.audit.enabled ? 'enabled' : 'disabled'} (${fresh.audit.db_path})
|
|
94
142
|
|
|
95
143
|
Next steps:
|
|
96
|
-
1. ramcp start
|
|
97
|
-
2. ramcp service install
|
|
98
|
-
3. ramcp policy allow /srv/myapp
|
|
99
|
-
4. ramcp url
|
|
144
|
+
1. ramcp start # run in foreground
|
|
145
|
+
2. ramcp service install # systemd + nginx (production)
|
|
146
|
+
3. ramcp policy allow /srv/myapp # let the AI touch your project
|
|
147
|
+
4. ramcp url # connector URL for your chatbot
|
|
100
148
|
`);
|
|
101
149
|
}
|
|
102
150
|
// ---------------------------------------------------------------------------
|
|
103
151
|
// start
|
|
104
152
|
// ---------------------------------------------------------------------------
|
|
105
153
|
async function cmdStart(args) {
|
|
106
|
-
const cfg = loadConfig();
|
|
107
|
-
if (args.values.has('host'))
|
|
108
|
-
cfg.host = args.values.get('host');
|
|
109
|
-
if (args.values.has('port'))
|
|
110
|
-
cfg.port = parseInt(args.values.get('port'), 10);
|
|
111
|
-
if (!cfg.token) {
|
|
112
|
-
console.error('No token. Run `ramcp init` first.');
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
154
|
const { runServer } = await import('../server/run.js');
|
|
116
|
-
await runServer(
|
|
155
|
+
await runServer({
|
|
156
|
+
host: args.values.get('host'),
|
|
157
|
+
port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
|
|
158
|
+
readOnly: args.flags.has('read-only'),
|
|
159
|
+
});
|
|
117
160
|
}
|
|
118
161
|
// ---------------------------------------------------------------------------
|
|
119
162
|
// url
|
|
120
163
|
// ---------------------------------------------------------------------------
|
|
121
|
-
function cmdUrl() {
|
|
164
|
+
function cmdUrl(args) {
|
|
122
165
|
const cfg = loadConfig();
|
|
123
|
-
|
|
124
|
-
console.error('No token. Run `ramcp init` first.');
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
166
|
+
const t = findToken(cfg, args.sub[0]);
|
|
127
167
|
const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
|
|
128
168
|
const scheme = cfg.public_host ? 'https' : 'http';
|
|
129
|
-
console.log(`${scheme}://${host}/${
|
|
169
|
+
console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
|
|
130
170
|
}
|
|
131
171
|
// ---------------------------------------------------------------------------
|
|
132
|
-
// token
|
|
172
|
+
// token management
|
|
133
173
|
// ---------------------------------------------------------------------------
|
|
134
174
|
function cmdToken(args) {
|
|
135
|
-
const sub = args.sub[0];
|
|
136
175
|
const cfg = loadConfig();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
const sub = args.sub[0];
|
|
177
|
+
if (sub === 'list') {
|
|
178
|
+
if (jsonOut(args)) {
|
|
179
|
+
console.log(JSON.stringify(cfg.tokens.map(t => ({
|
|
180
|
+
id: t.id, name: t.name, created: t.created, expires: t.expires || null,
|
|
181
|
+
scopes: t.scopes, read_only: !!t.read_only, shell: t.shell_enabled,
|
|
182
|
+
fingerprint: AuditLog.fingerprint(t.token),
|
|
183
|
+
})), null, 2));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
for (const t of cfg.tokens) {
|
|
187
|
+
console.log(`${t.id} ${t.name.padEnd(16)} shell:${t.shell_enabled ? 'on ' : 'off'} ro:${t.read_only ? 'yes' : 'no '} ${t.expires ? 'expires ' + t.expires + ' ' : ''}${t.scopes.length ? 'scopes: ' + t.scopes.join(',') + ' ' : ''}fp:${AuditLog.fingerprint(t.token)}`);
|
|
145
188
|
}
|
|
189
|
+
return;
|
|
146
190
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
191
|
+
if (sub === 'add') {
|
|
192
|
+
const name = args.values.get('name');
|
|
193
|
+
if (!name) {
|
|
194
|
+
console.error('--name is required');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
if (cfg.tokens.some(t => t.name === name)) {
|
|
198
|
+
console.error(`Token "${name}" already exists`);
|
|
150
199
|
process.exit(1);
|
|
151
200
|
}
|
|
201
|
+
const rec = newTokenRecord({
|
|
202
|
+
name,
|
|
203
|
+
shell_enabled: args.flags.has('shell') ? true : args.flags.has('no-shell') ? false : undefined,
|
|
204
|
+
allowed_paths: args.values.get('paths')?.split(',').map(s => s.trim()).filter(Boolean),
|
|
205
|
+
denied_paths: args.values.get('deny')?.split(',').map(s => s.trim()).filter(Boolean),
|
|
206
|
+
scopes: args.values.get('scopes')?.split(',').map(s => s.trim()).filter(Boolean),
|
|
207
|
+
read_only: args.flags.has('read-only'),
|
|
208
|
+
max_requests_per_minute: args.values.has('rpm') ? parseInt(args.values.get('rpm'), 10) : undefined,
|
|
209
|
+
expires: args.values.get('expires'),
|
|
210
|
+
});
|
|
211
|
+
// resolve allowed paths to real paths
|
|
212
|
+
rec.allowed_paths = rec.allowed_paths.map(resolveReal);
|
|
213
|
+
rec.denied_paths = rec.denied_paths.map(resolveReal);
|
|
214
|
+
cfg.tokens.push(rec);
|
|
215
|
+
saveConfig(cfg);
|
|
216
|
+
console.log(`Token "${name}" created (${rec.id})`);
|
|
217
|
+
console.log(`URL: https://${cfg.public_host || cfg.host + ':' + cfg.port}/${rec.token}${cfg.mcp_path}`);
|
|
218
|
+
restartHint();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (sub === 'show') {
|
|
222
|
+
const t = findToken(cfg, args.sub[1]);
|
|
152
223
|
if (args.flags.has('full'))
|
|
153
|
-
console.log(
|
|
224
|
+
console.log(t.token);
|
|
154
225
|
else
|
|
155
|
-
console.log(
|
|
226
|
+
console.log(`${t.name}: ${maskToken(t.token)} (use --full for the complete token)`);
|
|
227
|
+
return;
|
|
156
228
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
229
|
+
if (sub === 'rotate') {
|
|
230
|
+
const t = findToken(cfg, args.sub[1]);
|
|
231
|
+
t.token = generateToken();
|
|
232
|
+
saveConfig(cfg);
|
|
233
|
+
console.log(`Token "${t.name}" rotated: ${t.token}`);
|
|
234
|
+
console.log('Old token is dead. Update your chatbot connector now.');
|
|
235
|
+
restartHint();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (sub === 'revoke') {
|
|
239
|
+
const t = findToken(cfg, args.sub[1]);
|
|
240
|
+
cfg.tokens = cfg.tokens.filter(x => x !== t);
|
|
241
|
+
if (!cfg.tokens.length) {
|
|
242
|
+
console.error('Cannot revoke the last token.');
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
saveConfig(cfg);
|
|
246
|
+
console.log(`Token "${t.name}" (${t.id}) revoked.`);
|
|
247
|
+
restartHint();
|
|
248
|
+
return;
|
|
160
249
|
}
|
|
250
|
+
console.log('Usage: ramcp token <list|add|show|rotate|revoke> …');
|
|
251
|
+
process.exit(1);
|
|
161
252
|
}
|
|
162
253
|
// ---------------------------------------------------------------------------
|
|
163
254
|
// policy
|
|
164
255
|
// ---------------------------------------------------------------------------
|
|
165
256
|
function cmdPolicy(args) {
|
|
166
257
|
const cfg = loadConfig();
|
|
167
|
-
|
|
168
|
-
if (sub === '
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
258
|
+
// global readonly toggle has no token selector
|
|
259
|
+
if (args.sub[0] === 'readonly' && (args.sub[1] === 'on' || args.sub[1] === 'off')) {
|
|
260
|
+
cfg.read_only = args.sub[1] === 'on';
|
|
261
|
+
saveConfig(cfg);
|
|
262
|
+
console.log(`Global read-only: ${cfg.read_only ? 'ON — mutating tools refused' : 'off'}`);
|
|
263
|
+
restartHint();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const t = findToken(cfg, args.sub[0] === 'allow' || args.sub[0] === 'deny' || args.sub[0] === 'shell' ? undefined : args.sub[0]);
|
|
267
|
+
const sub = args.sub[0] === 'allow' || args.sub[0] === 'deny' || args.sub[0] === 'shell' ? args.sub[0] : (args.sub[1] || 'show');
|
|
268
|
+
if (sub === 'allow' && args.values.size === 0 && !args.sub.find(s => s.startsWith('/'))) {
|
|
269
|
+
// ramcp policy allow <path> — path is in sub[1] (or sub[2] when selector given)
|
|
270
|
+
}
|
|
271
|
+
const pathArg = (() => {
|
|
272
|
+
const idx = args.sub.indexOf('allow') >= 0 ? args.sub.indexOf('allow') : args.sub.indexOf('deny');
|
|
273
|
+
return idx >= 0 ? args.sub[idx + 1] : undefined;
|
|
274
|
+
})();
|
|
275
|
+
if (sub === 'allow' && pathArg) {
|
|
276
|
+
const real = resolveReal(pathArg);
|
|
277
|
+
if (!t.allowed_paths.includes(real))
|
|
278
|
+
t.allowed_paths.push(real);
|
|
172
279
|
saveConfig(cfg);
|
|
173
280
|
console.log(`Allowed: ${real}`);
|
|
174
|
-
|
|
281
|
+
restartHint();
|
|
175
282
|
}
|
|
176
|
-
else if (sub === 'deny' &&
|
|
177
|
-
const real = resolveReal(
|
|
178
|
-
|
|
179
|
-
if (!
|
|
180
|
-
|
|
283
|
+
else if (sub === 'deny' && pathArg) {
|
|
284
|
+
const real = resolveReal(pathArg);
|
|
285
|
+
t.allowed_paths = t.allowed_paths.filter((p) => p !== real);
|
|
286
|
+
if (!t.denied_paths.includes(real))
|
|
287
|
+
t.denied_paths.push(real);
|
|
181
288
|
saveConfig(cfg);
|
|
182
289
|
console.log(`Denied: ${real}`);
|
|
183
|
-
|
|
290
|
+
restartHint();
|
|
184
291
|
}
|
|
185
|
-
else if (sub === 'shell' && args.sub
|
|
186
|
-
|
|
187
|
-
if (flag !== 'on' && flag !== 'off') {
|
|
188
|
-
console.error('Usage: ramcp policy shell on|off');
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
cfg.shell_enabled = flag === 'on';
|
|
292
|
+
else if (sub === 'shell' && (args.sub.includes('on') || args.sub.includes('off'))) {
|
|
293
|
+
t.shell_enabled = args.sub.includes('on');
|
|
192
294
|
saveConfig(cfg);
|
|
193
|
-
console.log(`Shell execution ${
|
|
194
|
-
|
|
295
|
+
console.log(`Shell execution for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
|
|
296
|
+
restartHint();
|
|
195
297
|
}
|
|
196
298
|
else {
|
|
197
|
-
console.log(`
|
|
198
|
-
console.log(`
|
|
199
|
-
console.log(`
|
|
299
|
+
console.log(`token: ${t.name} (${t.id})`);
|
|
300
|
+
console.log(`allowed: ${t.allowed_paths.length ? '\n ' + t.allowed_paths.join('\n ') : '(none)'}`);
|
|
301
|
+
console.log(`denied: ${t.denied_paths.length ? '\n ' + t.denied_paths.join('\n ') : '(none)'}`);
|
|
302
|
+
console.log(`shell: ${t.shell_enabled ? 'enabled' : 'disabled'}`);
|
|
303
|
+
console.log(`read_only: ${t.read_only ? 'yes' : 'no'}`);
|
|
304
|
+
console.log(`scopes: ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
|
|
200
305
|
}
|
|
201
306
|
}
|
|
202
307
|
// ---------------------------------------------------------------------------
|
|
203
|
-
//
|
|
308
|
+
// audit
|
|
204
309
|
// ---------------------------------------------------------------------------
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
function serviceRunning() {
|
|
211
|
-
try {
|
|
212
|
-
const out = fs.readFileSync(`/proc/self/cgroup`, 'utf8');
|
|
213
|
-
return false; // placeholder, real check below via systemctl
|
|
310
|
+
function cmdAudit(args) {
|
|
311
|
+
const cfg = loadConfig();
|
|
312
|
+
if (!cfg.audit.enabled) {
|
|
313
|
+
console.log('Audit log is disabled (audit.enabled = false).');
|
|
314
|
+
return;
|
|
214
315
|
}
|
|
215
|
-
|
|
216
|
-
|
|
316
|
+
const audit = new AuditLog(cfg.audit.db_path);
|
|
317
|
+
if (args.sub[0] === 'chain' || args.flags.has('verify')) {
|
|
318
|
+
const tampered = audit.verify();
|
|
319
|
+
console.log(tampered === null ? '✔ hash chain intact' : `✖ TAMPERED — first bad row id: ${tampered}`);
|
|
320
|
+
process.exit(tampered === null ? 0 : 2);
|
|
321
|
+
}
|
|
322
|
+
const rows = audit.query({
|
|
323
|
+
tool: args.values.get('tool'),
|
|
324
|
+
since: args.values.has('since') ? new Date(args.values.get('since')).getTime() : undefined,
|
|
325
|
+
limit: args.values.has('limit') ? parseInt(args.values.get('limit'), 10) : 50,
|
|
326
|
+
});
|
|
327
|
+
if (jsonOut(args)) {
|
|
328
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
if (!rows.length) {
|
|
332
|
+
console.log('(no audit entries)');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (const r of rows) {
|
|
336
|
+
const d = new Date(r.ts).toISOString().slice(0, 19);
|
|
337
|
+
console.log(`${d} fp:${r.token_fingerprint} ${r.tool.padEnd(20)} ${r.is_error ? 'ERR ' : 'ok '} ${r.duration_ms}ms`);
|
|
338
|
+
}
|
|
217
339
|
}
|
|
340
|
+
audit.close();
|
|
218
341
|
}
|
|
219
|
-
|
|
220
|
-
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// doctor
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
async function cmdDoctor(args) {
|
|
346
|
+
const results = [];
|
|
347
|
+
const ok = (name, msg) => results.push([`✔ ${name}`, msg]);
|
|
348
|
+
const bad = (name, msg) => results.push([`✖ ${name}`, msg]);
|
|
349
|
+
const warn = (name, msg) => results.push([`⚠ ${name}`, msg]);
|
|
350
|
+
const cfg = loadConfig();
|
|
351
|
+
// 1. token
|
|
352
|
+
if (cfg.tokens.length)
|
|
353
|
+
ok('tokens', `${cfg.tokens.length} configured, primary: ${cfg.tokens[0].name}`);
|
|
354
|
+
else
|
|
355
|
+
bad('tokens', 'none — run `ramcp init`');
|
|
356
|
+
// 2. bind port free?
|
|
357
|
+
const inUse = await new Promise((resolve) => {
|
|
358
|
+
const srv = net.createServer();
|
|
359
|
+
srv.once('error', () => resolve(true));
|
|
360
|
+
srv.once('listening', () => { srv.close(() => resolve(false)); });
|
|
361
|
+
srv.listen(cfg.port, cfg.host);
|
|
362
|
+
});
|
|
363
|
+
const serviceActive = fs.existsSync(SERVICE_FILE) && run('systemctl', ['is-active', SERVICE_NAME]).trim() === 'active';
|
|
364
|
+
if (inUse) {
|
|
365
|
+
serviceActive ? ok('port', `${cfg.host}:${cfg.port} in use by the running service`) : warn('port', `${cfg.host}:${cfg.port} in use by ANOTHER process`);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
serviceActive ? warn('port', `port free but service claims active — crashed?`) : warn('port', `not listening — start with \`ramcp service install\` or \`ramcp start\``);
|
|
369
|
+
}
|
|
370
|
+
// 3. gateway answers?
|
|
371
|
+
if (!inUse || serviceActive) {
|
|
221
372
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
373
|
+
const resp = await fetch(`http://${cfg.host}:${cfg.port}/health`);
|
|
374
|
+
const body = (await resp.json());
|
|
375
|
+
if (body.status === 'ok')
|
|
376
|
+
ok('gateway', `healthy (v${body.version})`);
|
|
377
|
+
else
|
|
378
|
+
bad('gateway', `unexpected: ${JSON.stringify(body)}`);
|
|
224
379
|
}
|
|
225
380
|
catch {
|
|
226
|
-
|
|
381
|
+
bad('gateway', `no answer on ${cfg.host}:${cfg.port}/health`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// 4. nginx vhost?
|
|
385
|
+
if (cfg.public_host) {
|
|
386
|
+
const vhost = `/etc/nginx/sites-enabled/${cfg.public_host}`;
|
|
387
|
+
if (fs.existsSync(vhost)) {
|
|
388
|
+
const conf = fs.readFileSync(vhost, 'utf8');
|
|
389
|
+
if (conf.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
|
|
390
|
+
ok('nginx', `vhost for ${cfg.public_host} → ${cfg.host}:${cfg.port}`);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
warn('nginx', `${vhost} exists but does not proxy to ${cfg.host}:${cfg.port}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
warn('nginx', `no vhost at ${vhost} — run \`ramcp service install --domain ${cfg.public_host}\``);
|
|
398
|
+
}
|
|
399
|
+
// 5. public DNS resolves + Cloudflare edge reachable?
|
|
400
|
+
try {
|
|
401
|
+
const resp = await fetch(`https://${cfg.public_host}/health`, { signal: AbortSignal.timeout(8000) });
|
|
402
|
+
const body = (await resp.json());
|
|
403
|
+
if (body.status === 'ok')
|
|
404
|
+
ok('public', `https://${cfg.public_host} healthy end-to-end`);
|
|
405
|
+
else
|
|
406
|
+
warn('public', `got status ${resp.status} from edge`);
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
warn('public', `https://${cfg.public_host} unreachable: ${e.message}`);
|
|
227
410
|
}
|
|
228
411
|
}
|
|
412
|
+
// 6. audit chain
|
|
413
|
+
if (cfg.audit.enabled && fs.existsSync(cfg.audit.db_path)) {
|
|
414
|
+
const audit = new AuditLog(cfg.audit.db_path);
|
|
415
|
+
const tampered = audit.verify();
|
|
416
|
+
tampered === null ? ok('audit', 'hash chain intact') : bad('audit', `tampered at row ${tampered}`);
|
|
417
|
+
audit.close();
|
|
418
|
+
}
|
|
419
|
+
else if (cfg.audit.enabled) {
|
|
420
|
+
warn('audit', 'enabled but db not created yet');
|
|
421
|
+
}
|
|
422
|
+
for (const [name, msg] of results)
|
|
423
|
+
console.log(`${name.padEnd(14)} ${msg}`);
|
|
424
|
+
const hasBad = results.some(r => r[0].startsWith('✖'));
|
|
425
|
+
process.exitCode = hasBad ? 1 : 0;
|
|
426
|
+
}
|
|
427
|
+
function run(cmd, args) {
|
|
428
|
+
try {
|
|
429
|
+
return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return '';
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// service
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
const SERVICE_NAME = 'remote-access-mcp';
|
|
439
|
+
const SERVICE_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
440
|
+
function isSystemdInstalled() {
|
|
441
|
+
return fs.existsSync(SERVICE_FILE);
|
|
442
|
+
}
|
|
443
|
+
function restartHint() {
|
|
444
|
+
if (isSystemdInstalled()) {
|
|
445
|
+
console.log('(service restarts automatically on next request — config hot-reloads)');
|
|
446
|
+
}
|
|
229
447
|
}
|
|
230
448
|
function cmdService(args) {
|
|
231
449
|
const sub = args.sub[0] || '';
|
|
@@ -249,7 +467,7 @@ function cmdService(args) {
|
|
|
249
467
|
cmdStatus();
|
|
250
468
|
}
|
|
251
469
|
else {
|
|
252
|
-
console.log('Usage: ramcp service <install|uninstall|logs [-f]|status>');
|
|
470
|
+
console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
|
|
253
471
|
process.exit(1);
|
|
254
472
|
}
|
|
255
473
|
}
|
|
@@ -261,7 +479,7 @@ function installService(args) {
|
|
|
261
479
|
const domain = args.values.get('domain') || '';
|
|
262
480
|
const cfg = loadConfig();
|
|
263
481
|
const nodeBin = process.execPath;
|
|
264
|
-
const pkgRoot =
|
|
482
|
+
const pkgRoot = PKG_ROOT;
|
|
265
483
|
const unit = `[Unit]
|
|
266
484
|
Description=Remote Access MCP Gateway
|
|
267
485
|
After=network.target
|
|
@@ -314,7 +532,7 @@ WantedBy=multi-user.target
|
|
|
314
532
|
try {
|
|
315
533
|
execFileSync('nginx', ['-t']);
|
|
316
534
|
execFileSync('systemctl', ['reload', 'nginx']);
|
|
317
|
-
console.log(`✔ nginx vhost installed: ${domain} →
|
|
535
|
+
console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
|
|
318
536
|
}
|
|
319
537
|
catch (e) {
|
|
320
538
|
console.error(`nginx config test failed:\n${e.stdout || e.message}`);
|
|
@@ -330,8 +548,7 @@ function uninstallService() {
|
|
|
330
548
|
console.error('service uninstall requires root (sudo).');
|
|
331
549
|
process.exit(1);
|
|
332
550
|
}
|
|
333
|
-
|
|
334
|
-
if (existsSync(SERVICE_FILE)) {
|
|
551
|
+
if (fs.existsSync(SERVICE_FILE)) {
|
|
335
552
|
try {
|
|
336
553
|
execFileSync('systemctl', ['disable', '--now', SERVICE_NAME]);
|
|
337
554
|
}
|
|
@@ -343,13 +560,13 @@ function uninstallService() {
|
|
|
343
560
|
else {
|
|
344
561
|
console.log('Service not installed.');
|
|
345
562
|
}
|
|
346
|
-
// remove any nginx vhost that points at us
|
|
347
563
|
const sitesDir = '/etc/nginx/sites-enabled';
|
|
348
|
-
if (existsSync(sitesDir)) {
|
|
564
|
+
if (fs.existsSync(sitesDir)) {
|
|
565
|
+
const cfg = loadConfig();
|
|
349
566
|
for (const f of fs.readdirSync(sitesDir)) {
|
|
350
567
|
try {
|
|
351
|
-
const content = readFileSync(path.join(sitesDir, f), 'utf8');
|
|
352
|
-
if (content.includes(`proxy_pass http
|
|
568
|
+
const content = fs.readFileSync(path.join(sitesDir, f), 'utf8');
|
|
569
|
+
if (content.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
|
|
353
570
|
fs.unlinkSync(path.join(sitesDir, f));
|
|
354
571
|
console.log(`✔ nginx vhost removed: ${f}`);
|
|
355
572
|
}
|
|
@@ -370,17 +587,13 @@ function cmdStatus() {
|
|
|
370
587
|
const installed = isSystemdInstalled();
|
|
371
588
|
console.log(`service installed: ${installed ? 'yes' : 'no'}`);
|
|
372
589
|
if (installed) {
|
|
373
|
-
|
|
374
|
-
const out = execFileSync('systemctl', ['is-active', SERVICE_NAME], { encoding: 'utf8' }).trim();
|
|
375
|
-
console.log(`service status: ${out}`);
|
|
376
|
-
}
|
|
377
|
-
catch (e) {
|
|
378
|
-
console.log(`service status: ${e.stdout?.toString().trim() || 'unknown'}`);
|
|
379
|
-
}
|
|
590
|
+
console.log(`service status: ${run('systemctl', ['is-active', SERVICE_NAME]).trim() || 'unknown'}`);
|
|
380
591
|
}
|
|
381
592
|
const cfg = loadConfig();
|
|
382
593
|
console.log(`listen: ${cfg.host}:${cfg.port}`);
|
|
383
|
-
console.log(`
|
|
594
|
+
console.log(`tokens: ${cfg.tokens.length}`);
|
|
595
|
+
console.log(`audit: ${cfg.audit.enabled ? 'on' : 'off'}`);
|
|
596
|
+
console.log(`read_only: ${cfg.read_only ? 'on' : 'off'}`);
|
|
384
597
|
}
|
|
385
598
|
// ---------------------------------------------------------------------------
|
|
386
599
|
// main
|
|
@@ -393,13 +606,13 @@ export async function main(argv) {
|
|
|
393
606
|
}
|
|
394
607
|
switch (args.command) {
|
|
395
608
|
case 'init':
|
|
396
|
-
cmdInit();
|
|
609
|
+
cmdInit(args);
|
|
397
610
|
break;
|
|
398
611
|
case 'start':
|
|
399
612
|
await cmdStart(args);
|
|
400
613
|
break;
|
|
401
614
|
case 'url':
|
|
402
|
-
cmdUrl();
|
|
615
|
+
cmdUrl(args);
|
|
403
616
|
break;
|
|
404
617
|
case 'token':
|
|
405
618
|
cmdToken(args);
|
|
@@ -407,6 +620,12 @@ export async function main(argv) {
|
|
|
407
620
|
case 'policy':
|
|
408
621
|
cmdPolicy(args);
|
|
409
622
|
break;
|
|
623
|
+
case 'audit':
|
|
624
|
+
cmdAudit(args);
|
|
625
|
+
break;
|
|
626
|
+
case 'doctor':
|
|
627
|
+
await cmdDoctor(args);
|
|
628
|
+
break;
|
|
410
629
|
case 'service':
|
|
411
630
|
cmdService(args);
|
|
412
631
|
break;
|