remote-access-mcp 1.0.6 → 1.1.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 +354 -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 +123 -24
- 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,11 +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';
|
|
9
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
8
11
|
// ---------------------------------------------------------------------------
|
|
9
|
-
// help
|
|
12
|
+
// help
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
const HELP = `${PKG.name} v${PKG.version} — turn any Linux server into an AI-agent-accessible machine
|
|
12
15
|
|
|
@@ -14,23 +17,48 @@ Usage:
|
|
|
14
17
|
ramcp <command> [options]
|
|
15
18
|
|
|
16
19
|
Commands:
|
|
17
|
-
init
|
|
18
|
-
start [--host H] [--port P]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
token
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
58
|
|
|
32
59
|
Options:
|
|
33
|
-
-h, --help
|
|
60
|
+
-h, --help Show this help
|
|
61
|
+
--json Machine-readable output (where supported)
|
|
34
62
|
`;
|
|
35
63
|
function parseArgs(argv) {
|
|
36
64
|
const [command = '', ...rest] = argv;
|
|
@@ -60,25 +88,47 @@ function parseArgs(argv) {
|
|
|
60
88
|
}
|
|
61
89
|
return { command, sub, flags, values };
|
|
62
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
|
+
}
|
|
63
108
|
// ---------------------------------------------------------------------------
|
|
64
109
|
// init
|
|
65
110
|
// ---------------------------------------------------------------------------
|
|
66
|
-
function cmdInit() {
|
|
111
|
+
function cmdInit(args) {
|
|
67
112
|
const existing = fs.existsSync(configPath());
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
cfg
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
cfg = {
|
|
76
|
-
...loadConfig(),
|
|
77
|
-
token: generateToken(),
|
|
78
|
-
};
|
|
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;
|
|
79
118
|
}
|
|
80
|
-
|
|
81
|
-
|
|
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);
|
|
82
132
|
console.log(`
|
|
83
133
|
___ _____
|
|
84
134
|
| _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
|
|
@@ -86,144 +136,314 @@ function cmdInit() {
|
|
|
86
136
|
|_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
|
|
87
137
|
|___/ ${PKG.name} v${PKG.version}
|
|
88
138
|
|
|
89
|
-
${existing ? '✔ Existing config
|
|
90
|
-
✔ Token: ${
|
|
91
|
-
✔
|
|
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})
|
|
92
142
|
|
|
93
143
|
Next steps:
|
|
94
|
-
1. ramcp start
|
|
95
|
-
2. ramcp service install
|
|
96
|
-
3. ramcp policy allow /srv/myapp
|
|
97
|
-
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
|
|
98
148
|
`);
|
|
99
149
|
}
|
|
100
150
|
// ---------------------------------------------------------------------------
|
|
101
151
|
// start
|
|
102
152
|
// ---------------------------------------------------------------------------
|
|
103
153
|
async function cmdStart(args) {
|
|
104
|
-
const cfg = loadConfig();
|
|
105
|
-
if (args.values.has('host'))
|
|
106
|
-
cfg.host = args.values.get('host');
|
|
107
|
-
if (args.values.has('port'))
|
|
108
|
-
cfg.port = parseInt(args.values.get('port'), 10);
|
|
109
|
-
if (!cfg.token) {
|
|
110
|
-
console.error('No token. Run `ramcp init` first.');
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
154
|
const { runServer } = await import('../server/run.js');
|
|
114
|
-
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
|
+
});
|
|
115
160
|
}
|
|
116
161
|
// ---------------------------------------------------------------------------
|
|
117
162
|
// url
|
|
118
163
|
// ---------------------------------------------------------------------------
|
|
119
|
-
function cmdUrl() {
|
|
164
|
+
function cmdUrl(args) {
|
|
120
165
|
const cfg = loadConfig();
|
|
121
|
-
|
|
122
|
-
console.error('No token. Run `ramcp init` first.');
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
166
|
+
const t = findToken(cfg, args.sub[0]);
|
|
125
167
|
const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
|
|
126
168
|
const scheme = cfg.public_host ? 'https' : 'http';
|
|
127
|
-
console.log(`${scheme}://${host}/${
|
|
169
|
+
console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
|
|
128
170
|
}
|
|
129
171
|
// ---------------------------------------------------------------------------
|
|
130
|
-
// token
|
|
172
|
+
// token management
|
|
131
173
|
// ---------------------------------------------------------------------------
|
|
132
174
|
function cmdToken(args) {
|
|
133
|
-
const sub = args.sub[0];
|
|
134
175
|
const cfg = loadConfig();
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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)}`);
|
|
143
188
|
}
|
|
189
|
+
return;
|
|
144
190
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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`);
|
|
148
199
|
process.exit(1);
|
|
149
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]);
|
|
150
223
|
if (args.flags.has('full'))
|
|
151
|
-
console.log(
|
|
224
|
+
console.log(t.token);
|
|
152
225
|
else
|
|
153
|
-
console.log(
|
|
226
|
+
console.log(`${t.name}: ${maskToken(t.token)} (use --full for the complete token)`);
|
|
227
|
+
return;
|
|
154
228
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
158
249
|
}
|
|
250
|
+
console.log('Usage: ramcp token <list|add|show|rotate|revoke> …');
|
|
251
|
+
process.exit(1);
|
|
159
252
|
}
|
|
160
253
|
// ---------------------------------------------------------------------------
|
|
161
254
|
// policy
|
|
162
255
|
// ---------------------------------------------------------------------------
|
|
163
256
|
function cmdPolicy(args) {
|
|
164
257
|
const cfg = loadConfig();
|
|
165
|
-
|
|
166
|
-
if (sub === '
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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);
|
|
170
279
|
saveConfig(cfg);
|
|
171
280
|
console.log(`Allowed: ${real}`);
|
|
172
|
-
|
|
281
|
+
restartHint();
|
|
173
282
|
}
|
|
174
|
-
else if (sub === 'deny' &&
|
|
175
|
-
const real = resolveReal(
|
|
176
|
-
|
|
177
|
-
if (!
|
|
178
|
-
|
|
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);
|
|
179
288
|
saveConfig(cfg);
|
|
180
289
|
console.log(`Denied: ${real}`);
|
|
181
|
-
|
|
290
|
+
restartHint();
|
|
182
291
|
}
|
|
183
|
-
else if (sub === 'shell' && args.sub
|
|
184
|
-
|
|
185
|
-
if (flag !== 'on' && flag !== 'off') {
|
|
186
|
-
console.error('Usage: ramcp policy shell on|off');
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
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');
|
|
190
294
|
saveConfig(cfg);
|
|
191
|
-
console.log(`Shell execution ${
|
|
192
|
-
|
|
295
|
+
console.log(`Shell execution for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
|
|
296
|
+
restartHint();
|
|
193
297
|
}
|
|
194
298
|
else {
|
|
195
|
-
console.log(`
|
|
196
|
-
console.log(`
|
|
197
|
-
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)'}`);
|
|
198
305
|
}
|
|
199
306
|
}
|
|
200
307
|
// ---------------------------------------------------------------------------
|
|
201
|
-
//
|
|
308
|
+
// audit
|
|
202
309
|
// ---------------------------------------------------------------------------
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
function serviceRunning() {
|
|
209
|
-
try {
|
|
210
|
-
const out = fs.readFileSync(`/proc/self/cgroup`, 'utf8');
|
|
211
|
-
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;
|
|
212
315
|
}
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}
|
|
215
339
|
}
|
|
340
|
+
audit.close();
|
|
216
341
|
}
|
|
217
|
-
|
|
218
|
-
|
|
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) {
|
|
219
372
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
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)}`);
|
|
222
379
|
}
|
|
223
380
|
catch {
|
|
224
|
-
|
|
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}`);
|
|
225
410
|
}
|
|
226
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
|
+
}
|
|
227
447
|
}
|
|
228
448
|
function cmdService(args) {
|
|
229
449
|
const sub = args.sub[0] || '';
|
|
@@ -247,7 +467,7 @@ function cmdService(args) {
|
|
|
247
467
|
cmdStatus();
|
|
248
468
|
}
|
|
249
469
|
else {
|
|
250
|
-
console.log('Usage: ramcp service <install|uninstall|logs [-f]|status>');
|
|
470
|
+
console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
|
|
251
471
|
process.exit(1);
|
|
252
472
|
}
|
|
253
473
|
}
|
|
@@ -259,7 +479,7 @@ function installService(args) {
|
|
|
259
479
|
const domain = args.values.get('domain') || '';
|
|
260
480
|
const cfg = loadConfig();
|
|
261
481
|
const nodeBin = process.execPath;
|
|
262
|
-
const pkgRoot =
|
|
482
|
+
const pkgRoot = PKG_ROOT;
|
|
263
483
|
const unit = `[Unit]
|
|
264
484
|
Description=Remote Access MCP Gateway
|
|
265
485
|
After=network.target
|
|
@@ -312,7 +532,7 @@ WantedBy=multi-user.target
|
|
|
312
532
|
try {
|
|
313
533
|
execFileSync('nginx', ['-t']);
|
|
314
534
|
execFileSync('systemctl', ['reload', 'nginx']);
|
|
315
|
-
console.log(`✔ nginx vhost installed: ${domain} →
|
|
535
|
+
console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
|
|
316
536
|
}
|
|
317
537
|
catch (e) {
|
|
318
538
|
console.error(`nginx config test failed:\n${e.stdout || e.message}`);
|
|
@@ -328,8 +548,7 @@ function uninstallService() {
|
|
|
328
548
|
console.error('service uninstall requires root (sudo).');
|
|
329
549
|
process.exit(1);
|
|
330
550
|
}
|
|
331
|
-
|
|
332
|
-
if (existsSync(SERVICE_FILE)) {
|
|
551
|
+
if (fs.existsSync(SERVICE_FILE)) {
|
|
333
552
|
try {
|
|
334
553
|
execFileSync('systemctl', ['disable', '--now', SERVICE_NAME]);
|
|
335
554
|
}
|
|
@@ -341,13 +560,13 @@ function uninstallService() {
|
|
|
341
560
|
else {
|
|
342
561
|
console.log('Service not installed.');
|
|
343
562
|
}
|
|
344
|
-
// remove any nginx vhost that points at us
|
|
345
563
|
const sitesDir = '/etc/nginx/sites-enabled';
|
|
346
|
-
if (existsSync(sitesDir)) {
|
|
564
|
+
if (fs.existsSync(sitesDir)) {
|
|
565
|
+
const cfg = loadConfig();
|
|
347
566
|
for (const f of fs.readdirSync(sitesDir)) {
|
|
348
567
|
try {
|
|
349
|
-
const content = readFileSync(path.join(sitesDir, f), 'utf8');
|
|
350
|
-
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}`)) {
|
|
351
570
|
fs.unlinkSync(path.join(sitesDir, f));
|
|
352
571
|
console.log(`✔ nginx vhost removed: ${f}`);
|
|
353
572
|
}
|
|
@@ -368,17 +587,13 @@ function cmdStatus() {
|
|
|
368
587
|
const installed = isSystemdInstalled();
|
|
369
588
|
console.log(`service installed: ${installed ? 'yes' : 'no'}`);
|
|
370
589
|
if (installed) {
|
|
371
|
-
|
|
372
|
-
const out = execFileSync('systemctl', ['is-active', SERVICE_NAME], { encoding: 'utf8' }).trim();
|
|
373
|
-
console.log(`service status: ${out}`);
|
|
374
|
-
}
|
|
375
|
-
catch (e) {
|
|
376
|
-
console.log(`service status: ${e.stdout?.toString().trim() || 'unknown'}`);
|
|
377
|
-
}
|
|
590
|
+
console.log(`service status: ${run('systemctl', ['is-active', SERVICE_NAME]).trim() || 'unknown'}`);
|
|
378
591
|
}
|
|
379
592
|
const cfg = loadConfig();
|
|
380
593
|
console.log(`listen: ${cfg.host}:${cfg.port}`);
|
|
381
|
-
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'}`);
|
|
382
597
|
}
|
|
383
598
|
// ---------------------------------------------------------------------------
|
|
384
599
|
// main
|
|
@@ -391,13 +606,13 @@ export async function main(argv) {
|
|
|
391
606
|
}
|
|
392
607
|
switch (args.command) {
|
|
393
608
|
case 'init':
|
|
394
|
-
cmdInit();
|
|
609
|
+
cmdInit(args);
|
|
395
610
|
break;
|
|
396
611
|
case 'start':
|
|
397
612
|
await cmdStart(args);
|
|
398
613
|
break;
|
|
399
614
|
case 'url':
|
|
400
|
-
cmdUrl();
|
|
615
|
+
cmdUrl(args);
|
|
401
616
|
break;
|
|
402
617
|
case 'token':
|
|
403
618
|
cmdToken(args);
|
|
@@ -405,6 +620,12 @@ export async function main(argv) {
|
|
|
405
620
|
case 'policy':
|
|
406
621
|
cmdPolicy(args);
|
|
407
622
|
break;
|
|
623
|
+
case 'audit':
|
|
624
|
+
cmdAudit(args);
|
|
625
|
+
break;
|
|
626
|
+
case 'doctor':
|
|
627
|
+
await cmdDoctor(args);
|
|
628
|
+
break;
|
|
408
629
|
case 'service':
|
|
409
630
|
cmdService(args);
|
|
410
631
|
break;
|