remote-access-mcp 2.4.4 → 3.0.1
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/AGENTS.md +78 -0
- package/README.fa.md +0 -13
- package/README.md +2 -16
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +2 -131
- package/dist/cli/main.js.map +1 -1
- package/dist/core/config.d.ts +0 -4
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/policy.d.ts.map +1 -1
- package/dist/core/policy.js +0 -1
- package/dist/core/policy.js.map +1 -1
- package/dist/tools/filesystem.d.ts.map +1 -1
- package/dist/tools/filesystem.js +10 -75
- package/dist/tools/filesystem.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/logs.d.ts.map +1 -1
- package/dist/tools/logs.js +3 -33
- package/dist/tools/logs.js.map +1 -1
- package/dist/tools/packages.d.ts.map +1 -1
- package/dist/tools/packages.js +1 -14
- package/dist/tools/packages.js.map +1 -1
- package/dist/tools/services.d.ts.map +1 -1
- package/dist/tools/services.js +4 -27
- package/dist/tools/services.js.map +1 -1
- package/dist/tools/shell.d.ts +0 -12
- package/dist/tools/shell.d.ts.map +1 -1
- package/dist/tools/shell.js +5 -48
- package/dist/tools/shell.js.map +1 -1
- package/docs/ai/architecture.md +106 -0
- package/docs/ai/decisions.md +141 -0
- package/docs/ai/security-model.md +114 -0
- package/docs/ai/tools-and-cli.md +152 -0
- package/docs/ai/transport-compatibility.md +85 -0
- package/package.json +4 -2
package/dist/tools/shell.js
CHANGED
|
@@ -3,30 +3,8 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { assertToolPermitted } from '../core/policy.js';
|
|
5
5
|
import { shellCommand, childEnv, isWindows } from '../core/platform.js';
|
|
6
|
-
import { sshRun, buildRunCommand } from '../core/fleet.js';
|
|
7
6
|
const exec = promisify(execFile);
|
|
8
7
|
const MAX_OUTPUT = 60_000;
|
|
9
|
-
function fleetHosts(ctx) {
|
|
10
|
-
return ctx.cfg.fleet?.hosts || [];
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Shared optional host parameter — ALWAYS present in the schema.
|
|
14
|
-
*
|
|
15
|
-
* If it were only advertised when a fleet exists, the SDK would silently
|
|
16
|
-
* strip a client's host=<x> argument on a fleet-less gateway and the tool
|
|
17
|
-
* would run the "remote" command LOCALLY on the gateway itself — a silent
|
|
18
|
-
* security downgrade. Keeping the parameter in every schema means the
|
|
19
|
-
* handler always sees it and can refuse with a clear error instead.
|
|
20
|
-
*/
|
|
21
|
-
export function hostParam(ctx, names) {
|
|
22
|
-
const hosts = fleetHosts(ctx);
|
|
23
|
-
const hint = hosts.length
|
|
24
|
-
? `Run on fleet machine${names ? ` (${names})` : ''}: ${hosts.map(h => h.name).join(', ')}. Omit for local.`
|
|
25
|
-
: 'Reserved for fleet hosts — none configured yet (see `ramcp fleet add`). Ignored locally.';
|
|
26
|
-
return {
|
|
27
|
-
host: z.string().optional().describe(hint),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
8
|
// Shell injection surface: we run a shell by design (the tool IS a shell).
|
|
31
9
|
// Guard rails: timeout, output cap, no TTY, cwd policy-checked.
|
|
32
10
|
export function registerShellTools(server, ctx) {
|
|
@@ -37,30 +15,16 @@ export function registerShellTools(server, ctx) {
|
|
|
37
15
|
});
|
|
38
16
|
// ---- run_command -------------------------------------------------------
|
|
39
17
|
server.registerTool('run_command', {
|
|
40
|
-
description: 'Execute a shell command (bash/sh on POSIX, PowerShell/cmd on Windows
|
|
18
|
+
description: 'Execute a shell command (bash/sh on POSIX, PowerShell/cmd on Windows). Requires shell enabled. 120s default timeout.',
|
|
41
19
|
inputSchema: {
|
|
42
20
|
command: z.string().describe('Command line to run'),
|
|
43
21
|
cwd: z.string().optional().describe('Working directory (policy-checked when local)'),
|
|
44
22
|
timeout_ms: z.number().optional().default(120_000).describe('Timeout in ms (max 600000)'),
|
|
45
|
-
...hostParam(ctx),
|
|
46
23
|
},
|
|
47
|
-
}, async ({ command, cwd,
|
|
24
|
+
}, async ({ command, cwd, timeout_ms }) => {
|
|
48
25
|
if (!ctx.token.shell_enabled) {
|
|
49
26
|
return { content: [{ type: 'text', text: 'Shell execution is disabled for this token. Enable with `ramcp policy shell on`.' }], isError: true };
|
|
50
27
|
}
|
|
51
|
-
// Fleet path: per-host allowlist + SSH, no local policy check (the
|
|
52
|
-
// command does not touch this machine).
|
|
53
|
-
// If the client names a host but the schema had no fleet to advertise
|
|
54
|
-
// (empty config), the SDK still may pass the arg through — a silent
|
|
55
|
-
// LOCAL fallback would run a "remote" command on the gateway itself.
|
|
56
|
-
if (host && !fleetHosts(ctx).length) {
|
|
57
|
-
return { content: [{ type: 'text', text: 'No fleet hosts are configured on this gateway. Add one with `ramcp fleet add` before using host parameters.' }], isError: true };
|
|
58
|
-
}
|
|
59
|
-
if (host) {
|
|
60
|
-
const h = (await import('../core/fleet.js')).assertCapability(fleetHosts(ctx), host, 'shell');
|
|
61
|
-
const { stdout } = await sshRun(h.host, h.port, buildRunCommand(command, undefined, timeout_ms), { timeoutMs: Math.min(timeout_ms, 600_000) });
|
|
62
|
-
return { content: [{ type: 'text', text: (stdout || '(no output)').slice(0, MAX_OUTPUT) }] };
|
|
63
|
-
}
|
|
64
28
|
assertToolPermitted({ tool: 'run_command', scopes: ctx.token.scopes, readOnly: ctx.readOnly, policy: policy(), target: cwd });
|
|
65
29
|
const timeout = Math.min(timeout_ms, 600_000);
|
|
66
30
|
const { file, args } = shellCommand(command);
|
|
@@ -92,16 +56,9 @@ export function registerShellTools(server, ctx) {
|
|
|
92
56
|
});
|
|
93
57
|
// ---- process_list ---------------------------------------------------------
|
|
94
58
|
server.registerTool('process_list', {
|
|
95
|
-
description: 'List running processes with CPU/memory.
|
|
96
|
-
inputSchema: {
|
|
97
|
-
|
|
98
|
-
},
|
|
99
|
-
}, async ({ host }) => {
|
|
100
|
-
if (host) {
|
|
101
|
-
const h = (await import('../core/fleet.js')).assertCapability(fleetHosts(ctx), host, 'shell');
|
|
102
|
-
const { stdout } = await sshRun(h.host, h.port, "ps axo pid,ppid,user,pcpu,pmem,comm --sort=-pcpu | head -80", { timeoutMs: 30_000 });
|
|
103
|
-
return { content: [{ type: 'text', text: stdout.slice(0, MAX_OUTPUT) }] };
|
|
104
|
-
}
|
|
59
|
+
description: 'List running processes with CPU/memory. Uses ps on POSIX, Get-Process on Windows.',
|
|
60
|
+
inputSchema: {},
|
|
61
|
+
}, async () => {
|
|
105
62
|
assertToolPermitted({ tool: 'process_list', scopes: ctx.token.scopes, readOnly: ctx.readOnly });
|
|
106
63
|
try {
|
|
107
64
|
if (isWindows()) {
|
package/dist/tools/shell.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/tools/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/tools/shell.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
How the gateway is put together, for AI agents (and humans) who need to
|
|
4
|
+
reason about it before changing it.
|
|
5
|
+
|
|
6
|
+
## Module map
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
src/
|
|
10
|
+
├── core/ # framework-free building blocks
|
|
11
|
+
│ ├── config.ts # RamcpConfig + tokens; loads/migrates config.json
|
|
12
|
+
│ ├── policy.ts # path sandbox, scope groups, tool permission gate
|
|
13
|
+
│ ├── audit.ts # JSONL hash-chain audit log + redaction
|
|
14
|
+
│ ├── crypto.ts # timing-safe token compare
|
|
15
|
+
│ ├── rate-limit.ts # token-bucket per token
|
|
16
|
+
│ ├── platform.ts # OS detection, shells, dataDir, runtime state
|
|
17
|
+
│ ├── tunnel.ts # cloudflared quick-tunnel lifecycle
|
|
18
|
+
│ ├── webhooks.ts # fire-and-forget event notifications
|
|
19
|
+
│ └── context.ts # ConfigWatcher (hot-reload) + ToolContext
|
|
20
|
+
├── server/
|
|
21
|
+
│ ├── app.ts # express app: auth, routes, sessions, MCP wiring
|
|
22
|
+
│ ├── run.ts # boot: listen, tunnel, runtime state, shutdown
|
|
23
|
+
│ ├── sessions.ts # stateful MCP session store (Claude dialect)
|
|
24
|
+
│ └── legacy-sse.ts # 2024-11-05 SSE transport + keepalive
|
|
25
|
+
├── tools/ # one file per suite, registered per request
|
|
26
|
+
│ ├── filesystem.ts shell.ts system.ts http.ts git.ts sqlite.ts
|
|
27
|
+
│ ├── logs.ts services.ts packages.ts schedule.ts security.ts
|
|
28
|
+
│ ├── project.ts web.ts planning.ts ops.ts policy.ts index.ts
|
|
29
|
+
└── cli/main.ts # ramcp — all commands
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Request lifecycle (the part that matters most)
|
|
33
|
+
|
|
34
|
+
One MCP POST travels this path:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
nginx ─→ express
|
|
38
|
+
│ app.all('/<token>/sse') or ('/sse' + Bearer header)
|
|
39
|
+
├─ authenticate(presented) [core/crypto + config]
|
|
40
|
+
│ reloads config if mtime changed (hot-reload)
|
|
41
|
+
│ timing-safe compare against all tokens
|
|
42
|
+
├─ rateLimited(token)? → 429
|
|
43
|
+
├─ normalizeAccept(req) [compat: never 406]
|
|
44
|
+
├─ session routing:
|
|
45
|
+
│ Mcp-Session-Id present?
|
|
46
|
+
│ ├─ known session → its StreamableHTTP transport
|
|
47
|
+
│ └─ unknown → 404 (client restarts cleanly)
|
|
48
|
+
│ no session id:
|
|
49
|
+
│ ├─ initialize → new stateful session (id in response header)
|
|
50
|
+
│ └─ anything else → stateless throwaway transport
|
|
51
|
+
├─ buildServerFor(token) [per request!]
|
|
52
|
+
│ McpServer + registerAllTools(server, ctx)
|
|
53
|
+
│ every tool handler wrapped: try/catch → audit → webhooks
|
|
54
|
+
└─ transport.handleRequest → tool executes → response
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Tools are re-registered for every request.** That is what makes policy
|
|
58
|
+
edits (CLI or in-chat `allow_path`) apply on the next request with zero
|
|
59
|
+
restarts. Do not "optimize" this by caching servers across requests — it
|
|
60
|
+
would break hot-reload and per-token isolation.
|
|
61
|
+
|
|
62
|
+
`ctx: ToolContext` carries `{ cfg, token, readOnly, persist, audit }`.
|
|
63
|
+
The token record is looked up fresh per request, so a revoked token dies
|
|
64
|
+
mid-conversation.
|
|
65
|
+
|
|
66
|
+
## The three dialects (why server code looks redundant)
|
|
67
|
+
|
|
68
|
+
| Client | Dialect | First request | Follow-ups |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| ChatGPT, Grok | Streamable, stateless | POST initialize (200 JSON/SSE) | fresh POST per call, ignores session id |
|
|
71
|
+
| Claude (new SDK) | Streamable, stateful | POST initialize → **Mcp-Session-Id header back** | same id on every request; GET opens notification stream |
|
|
72
|
+
| Claude (auto for `/sse` URLs) | Legacy SSE 2024-11-05 | **GET** stream → `event: endpoint` frame | POST to `/<token>/sse/messages?sessionId=…`, replies ride the stream |
|
|
73
|
+
|
|
74
|
+
GET disambiguation: a GET **with** `Mcp-Session-Id` is a stateful client
|
|
75
|
+
opening its notification stream; **without** it, it's a legacy client
|
|
76
|
+
starting the handshake. Mixing these up sends `event: endpoint` to a
|
|
77
|
+
stateful client, which aborts with `Unknown SSE event: endpoint` — that
|
|
78
|
+
exact bug killed Claude connectors for a full day. Tests pin it
|
|
79
|
+
(`tests/get-dispatch.test.ts`).
|
|
80
|
+
|
|
81
|
+
## Data & state
|
|
82
|
+
|
|
83
|
+
| File | Lives in | Written by | Lifetime |
|
|
84
|
+
|---|---|---|---|
|
|
85
|
+
| `config.json` | dataDir per OS | CLI, policy tools | persistent; 0600 |
|
|
86
|
+
| `audit.jsonl` | dataDir | audit wrapper | append-only, hash-chained |
|
|
87
|
+
| `runtime.json` | dataDir | gateway boot | ephemeral — pid-checked, cleared on exit |
|
|
88
|
+
| `schedule.json`, `plans.json`, `snapshots.json`, `snapshots/` | dataDir | tools | persistent |
|
|
89
|
+
|
|
90
|
+
The tunnel URL **only** ever goes to `runtime.json`. Writing it to config
|
|
91
|
+
would clobber a server's real `public_host` the moment someone ran
|
|
92
|
+
`ramcp tunnel` on it (that happened; ADR-003).
|
|
93
|
+
|
|
94
|
+
## Cross-platform layer
|
|
95
|
+
|
|
96
|
+
`core/platform.ts` normalizes: which shell (`bash -lc` / `pwsh` / `cmd.exe`),
|
|
97
|
+
where data lives, how paths compare (case-insensitive on Win/mac),
|
|
98
|
+
`which()`, `hasSystemd()`. Tools call `shellCommand()` instead of hardcoding
|
|
99
|
+
bash — Windows compat lives or dies here.
|
|
100
|
+
|
|
101
|
+
## Service installers
|
|
102
|
+
|
|
103
|
+
`ramcp service install` writes a systemd unit (Linux), a launchd plist
|
|
104
|
+
(macOS, per-user, no sudo), or a schtasks entry (Windows, logon trigger).
|
|
105
|
+
The unit points at `dist/server/run.js` under the *global npm* install path —
|
|
106
|
+
`ramcp upgrade` rewrites nothing but must restart the right manager per OS.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Decision Records (ADR)
|
|
2
|
+
|
|
3
|
+
Short, dated records of *why* the code is the way it is. Each one encodes a
|
|
4
|
+
lesson that cost real debugging time. When one of these decisions blocks
|
|
5
|
+
something you're trying to do, don't quietly route around it — write a new
|
|
6
|
+
record superseding the old one.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ADR-001 — Express + per-request McpServer builds
|
|
11
|
+
**Status:** accepted · **Date:** 2026-08-30
|
|
12
|
+
|
|
13
|
+
The gateway builds a **fresh `McpServer` with all tools re-registered for
|
|
14
|
+
every request** (stateless dialect) or per session (stateful).
|
|
15
|
+
|
|
16
|
+
Why: (a) policy mutations — CLI, config hot-reload, or in-chat
|
|
17
|
+
`allow_path` — must apply on the *next* request, not after a restart;
|
|
18
|
+
(b) per-request builds bind the calling token's record into the handlers,
|
|
19
|
+
so revocation/rotation takes effect mid-conversation; (c) the MCP SDK
|
|
20
|
+
guidance for our compat matrix is one transport per request anyway.
|
|
21
|
+
|
|
22
|
+
Cost: registration overhead per request (~1ms measured). Accepted.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## ADR-002 — Dual auth: Bearer header *and* token-in-URL
|
|
27
|
+
**Status:** accepted · **Date:** 2026-08-30
|
|
28
|
+
|
|
29
|
+
ChatGPT custom connectors cannot set custom headers, so the token rides in
|
|
30
|
+
the URL path (`/<token>/mcp`). Everything else uses `Authorization: Bearer`.
|
|
31
|
+
Both resolve through the same timing-safe comparison.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ADR-003 — Tunnel URL lives in runtime.json, never config
|
|
36
|
+
**Status:** accepted · **Date:** 2026-08-31
|
|
37
|
+
|
|
38
|
+
Quick-tunnel URLs are valid only while the gateway process runs. The first
|
|
39
|
+
implementation persisted the URL into `config.json`'s `public_host` — and
|
|
40
|
+
one test run on the production server **clobbered the real domain**, after
|
|
41
|
+
which `ramcp url` printed a dead trycloudflare URL to the user.
|
|
42
|
+
|
|
43
|
+
Fix: `runtime.json` = {pid, tunnel_url, host, port, started}, written on
|
|
44
|
+
boot, pid-checked on read (stale → ignored), cleared on exit including
|
|
45
|
+
`process.on('exit')` for Windows service stops. `ramcp url` prefers the
|
|
46
|
+
live tunnel. Pinned by `tests/runtime-state.test.ts`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## ADR-004 — Pure ESM, no require() in src/
|
|
51
|
+
**Status:** accepted · **Date:** 2026-08-31
|
|
52
|
+
|
|
53
|
+
A `require()` in `src/cli/main.ts` passed the entire test suite (vitest's
|
|
54
|
+
CJS interop tolerates it) and **crash-looped production** within a second
|
|
55
|
+
of every systemd start. Diagnosis needed a boot test that runs the built
|
|
56
|
+
output under plain node — which now exists (`tests/boot.test.ts`) and
|
|
57
|
+
should be the pattern for anything touching startup paths.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## ADR-005 — Audit log is JSONL, not better-sqlite3
|
|
62
|
+
**Status:** accepted · **Date:** 2026-08-31
|
|
63
|
+
|
|
64
|
+
The original audit used better-sqlite3. In the stateless request loop its
|
|
65
|
+
native `Statement` destructor aborted Node with SIGABRT (~8 requests in,
|
|
66
|
+
native stack trace pointing at `Statement::~Statement`). Root cause:
|
|
67
|
+
better-sqlite3's teardown hooks racing the SDK's per-request transports.
|
|
68
|
+
|
|
69
|
+
The rewrite is a plain append + fsync JSONL file with the same hash chain,
|
|
70
|
+
zero native code in the dependency tree, and a regression test that runs
|
|
71
|
+
30 full stateless cycles with audit writes (`tests/crash-regression.test.ts`).
|
|
72
|
+
sqlite_query/sqlite_schema keep better-sqlite3 for *user* databases (open
|
|
73
|
+
per call, closed in-finally) — that usage never crashed.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ADR-006 — Hermetic tests: build gateway state explicitly
|
|
78
|
+
**Status:** accepted · **Date:** 2026-08-31
|
|
79
|
+
|
|
80
|
+
Tests that called `buildApp()` without seeding state read the **host's real
|
|
81
|
+
config**. It worked until the production server deployed with
|
|
82
|
+
`mcp_path: /sse` — then 10 tests failed only-when-the-service-was-running.
|
|
83
|
+
All suites now construct explicit `GatewayState` objects (see any
|
|
84
|
+
`tests/*.test.ts` `beforeAll`).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## ADR-007 — Fleet mode removed (single machine by design)
|
|
89
|
+
**Status:** accepted · **Date:** 2026-09-01 · **Supersedes:** the v2.4.0 fleet feature
|
|
90
|
+
|
|
91
|
+
Fleet (SSH to N machines, per-host tool allowlists, 14 remote-capable
|
|
92
|
+
tools, `ramcp fleet` CLI) was fully implemented, tested (including live-SSH
|
|
93
|
+
and fake-ssh suites), published as 2.4.x — and then **removed entirely** in
|
|
94
|
+
3.0.0 by owner decision: the product is one gateway per machine; multi-
|
|
95
|
+
server orchestration is not the problem this project solves.
|
|
96
|
+
|
|
97
|
+
What the removal kept: webhooks and `config export/import` (independent
|
|
98
|
+
value). What it took with it: `src/core/fleet.ts`, `src/tools/fleet.ts`,
|
|
99
|
+
the `host` parameter on 14 tools, fleet CLI/scopes/config, three test
|
|
100
|
+
files. `ramcp fleet` is now an unknown command (pinned by a test).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## ADR-008 — Never omit schema parameters conditionally
|
|
105
|
+
**Status:** accepted · **Date:** 2026-09-01 · **Learned from:** the fleet era
|
|
106
|
+
|
|
107
|
+
When a tool's schema includes a parameter only under some conditions
|
|
108
|
+
(e.g., `host` only when fleet hosts exist), the MCP SDK **silently strips**
|
|
109
|
+
client arguments that aren't in the current schema. Consequence observed on
|
|
110
|
+
production: a client sending `run_command {command, host: "ghost"}$` to a
|
|
111
|
+
fleet-less gateway had `host` dropped and the **"remote" command executed
|
|
112
|
+
locally on the gateway** — a silent security downgrade, not an error.
|
|
113
|
+
|
|
114
|
+
Rule: parameters that gate security-relevant behavior stay in the schema
|
|
115
|
+
always; the *handler* refuses with a clear error when the capability is
|
|
116
|
+
absent. (Superseded by ADR-007 removing the host param outright, but the
|
|
117
|
+
rule stands for anything like it.)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ADR-009 — SSE-framed responses by default
|
|
122
|
+
**Status:** accepted · **Date:** 2026-08-31
|
|
123
|
+
|
|
124
|
+
`enableJsonResponse: true` (plain-JSON replies) works for ChatGPT/Grok but
|
|
125
|
+
Claude's connector read a valid 200 initialize reply and silently
|
|
126
|
+
abandoned the connection. SSE framing (`event: message\ndata: …`,
|
|
127
|
+
`content-type: text/event-stream`) is the reference behavior and the only
|
|
128
|
+
framing observed to satisfy every dialect we've tested. The wire-log proxy
|
|
129
|
+
session that proved this is described in transport-compatibility.md.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## ADR-010 — execFile does not support the `input` option
|
|
134
|
+
**Status:** accepted · **Date:** 2026-09-01
|
|
135
|
+
|
|
136
|
+
`promisify(execFile)({... input})` never closes the child's stdin — the
|
|
137
|
+
call hangs until timeout (fleet's remote file writes hung 5s→timeout every
|
|
138
|
+
time before this was understood; a minimal `bash -c cat` repro confirmed
|
|
139
|
+
it's the API, not our code). Pattern: when stdin must be piped, use
|
|
140
|
+
`spawn()` and end the stream manually. The helper shape lives in git
|
|
141
|
+
history (core/fleet.ts) if ever needed again.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
The gateway hands an AI the keys to a machine. Every design choice below
|
|
4
|
+
exists to make that survivable. Read this before touching anything in the
|
|
5
|
+
auth, policy, or audit paths.
|
|
6
|
+
|
|
7
|
+
## Threat model, plainly
|
|
8
|
+
|
|
9
|
+
The token holder is **trusted** (that's you). The threats are:
|
|
10
|
+
1. **Token leakage** → mitigate: no secrets in logs/audit (fingerprints only),
|
|
11
|
+
tight file perms, rotate in one command.
|
|
12
|
+
2. **The AI itself** going somewhere you didn't intend → mitigate: path
|
|
13
|
+
sandbox, scope groups, read-only mode, shell opt-in, protected
|
|
14
|
+
services/packages/processes, SSRF walls.
|
|
15
|
+
3. **A hostile chat platform or MITM** → mitigate: TLS at the edge
|
|
16
|
+
(Cloudflare/Let's Encrypt), loopback-only bind, timing-safe compares.
|
|
17
|
+
4. **Tampering with history** after an incident → the hash chain.
|
|
18
|
+
|
|
19
|
+
## Layer 1 — Network
|
|
20
|
+
|
|
21
|
+
- Binds `127.0.0.1` only. Nothing listens publicly but nginx/Cloudflare.
|
|
22
|
+
- `ramcp tunnel` (laptops) publishes a temporary https URL via cloudflared;
|
|
23
|
+
the URL dies with the process, is tracked in pid-checked `runtime.json`,
|
|
24
|
+
and self-verifies reachability before promising anything.
|
|
25
|
+
|
|
26
|
+
## Layer 2 — Authentication
|
|
27
|
+
|
|
28
|
+
- Two equivalent forms: `Authorization: Bearer <token>` or the token in the
|
|
29
|
+
URL path (`/<token>/mcp`) — the latter because ChatGPT connectors cannot
|
|
30
|
+
set custom headers.
|
|
31
|
+
- Compare is **timing-safe** with a length pre-filter (`core/crypto.ts`).
|
|
32
|
+
- Token records: expiry (ISO date), per-token rate limit (token bucket,
|
|
33
|
+
burst = rpm/4), revocation is config write + hot reload — a rotated token
|
|
34
|
+
dies mid-conversation.
|
|
35
|
+
- Sessions (stateful dialect) are **bound to the token fingerprint** that
|
|
36
|
+
opened them: presenting another token with a stolen session id → 403.
|
|
37
|
+
|
|
38
|
+
## Layer 3 — Authorization (per token)
|
|
39
|
+
|
|
40
|
+
`assertToolPermitted()` enforces, in order:
|
|
41
|
+
|
|
42
|
+
1. **scopes** — empty = every group; else the tool must belong to a listed
|
|
43
|
+
group. Group map is static in `core/policy.ts` (TOOL_SCOPES).
|
|
44
|
+
2. **read-only** — per-token or global `ramcp policy readonly on` refuses
|
|
45
|
+
every entry of `MUTATING_TOOLS`.
|
|
46
|
+
3. **path sandbox** — only if the tool passes a `target`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
resolveReal(path) # realpathSync; for missing paths, the deepest
|
|
50
|
+
# existing ancestor is resolved and the
|
|
51
|
+
# remainder re-attached → symlink escapes collapse
|
|
52
|
+
→ in denied_paths? → refuse # deny always wins, even inside an allowed root
|
|
53
|
+
→ in allowed_paths? → allow
|
|
54
|
+
→ else → refuse # empty allow-list denies everything
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Path comparison is case-insensitive on Windows/macOS, separators normalized
|
|
58
|
+
(`core/platform.ts`).
|
|
59
|
+
|
|
60
|
+
**In-chat policy tools can only widen their own token's sandbox** — they
|
|
61
|
+
mutate the calling token's record, never another token's.
|
|
62
|
+
|
|
63
|
+
## Layer 4 — Tool-level guards
|
|
64
|
+
|
|
65
|
+
| Guard | Tool(s) | Behavior |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Shell opt-in | run_command, kill_process, schedule | refuse unless token `shell_enabled` |
|
|
68
|
+
| Protected pids | kill_process | gateway pid, ppid, PID 1 |
|
|
69
|
+
| SSRF wall | http_request, web_fetch, port_check | 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16 (cloud metadata), 0/8, ::1, fc00::/7, fe80::/10, IPv4-mapped v6, metadata hostnames |
|
|
70
|
+
| Git arg whitelist | git | ~45 known verbs; `--upload-pack`/`--exec=` refused; no `;&|`${}`<>` in args |
|
|
71
|
+
| SQL single-statement | sqlite_query | one statement; ATTACH/DETACH refused (escape the sandbox via another db file) |
|
|
72
|
+
| Unit-name regex | service_*, journal | `[A-Za-z0-9@._-]+` only |
|
|
73
|
+
| Protected units | service_action | ssh/sshd/systemd/networkd/dbus/gateway itself/major Windows services |
|
|
74
|
+
| Protected packages | package_remove | nodejs/npm/nginx/openssh/systemd/remote-access-mcp |
|
|
75
|
+
| Secret masking | secret_scan | matches reported as file:line + type only — never the secret |
|
|
76
|
+
| Secret redaction | environment_inspect | pass/secret/token/key/auth/credential vars → `[MASKED]` |
|
|
77
|
+
| Schedule floor | schedule_command | ≥60s recurrence; one-shot allowed |
|
|
78
|
+
|
|
79
|
+
## Layer 5 — Accounting
|
|
80
|
+
|
|
81
|
+
- **Audit log** (`audit.jsonl`): one line per tool invocation — ts, token
|
|
82
|
+
**fingerprint** (sha256-16, never the secret), tool, redacted args,
|
|
83
|
+
ok/is_error, duration. Each line's `hash = sha256(prev_hash + fields)` →
|
|
84
|
+
deleting or editing any line breaks every hash after it.
|
|
85
|
+
`ramcp audit chain` walks it; exit code 2 on tamper. Args are redacted
|
|
86
|
+
(`pass|secret|token|key|auth` keys → `[REDACTED]`, values capped at 300ch).
|
|
87
|
+
- **Webhooks** mirror tool.error/tool.success out to URLs you own —
|
|
88
|
+
fire-and-forget, 5s timeout, identical events deduped within 10s.
|
|
89
|
+
|
|
90
|
+
## Layer 6 — The audit wrapper itself
|
|
91
|
+
|
|
92
|
+
`buildServerFor()` monkey-patches `server.registerTool` so **every** handler
|
|
93
|
+
is wrapped: errors are caught and converted to `isError` results (a tool
|
|
94
|
+
crash must never 500 the transport), then audit + webhooks fire. This is
|
|
95
|
+
also the single choke point where a future change would accidentally bypass
|
|
96
|
+
accounting — keep it that way.
|
|
97
|
+
|
|
98
|
+
## Things we refuse to add (non-negotiable)
|
|
99
|
+
|
|
100
|
+
- No `require()` in `src/` (pure ESM; one slip crash-looped production).
|
|
101
|
+
- No native modules in the request path (better-sqlite3 SIGABRT'd the
|
|
102
|
+
stateless loop — audit is plain JSONL now).
|
|
103
|
+
- No dynamic schema omission (the SDK silently drops unknown client args;
|
|
104
|
+
that once executed a "remote" command locally — ADR-008).
|
|
105
|
+
- No second machine (fleet removed by owner decision — ADR-007).
|
|
106
|
+
|
|
107
|
+
## If you're adding a tool
|
|
108
|
+
|
|
109
|
+
Checklist: (1) pick/extend a scope group; (2) decide mutating or read-only
|
|
110
|
+
and add to MUTATING_TOOLS if mutating; (3) accept `path`-style targets →
|
|
111
|
+
pass them as `target:` to the gate; (4) think about injection (quotes,
|
|
112
|
+
metacharacters, second-order effects) and add a guard, not a regex hope;
|
|
113
|
+
(5) never log secrets; the audit wrapper redacts args but your *output* is
|
|
114
|
+
on you.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Tools & CLI Reference
|
|
2
|
+
|
|
3
|
+
Complete inventory of what the gateway exposes, as of v3.0.0.
|
|
4
|
+
Counts: **44 tools / 17 suites**, **17 CLI commands**.
|
|
5
|
+
|
|
6
|
+
Every tool handler runs inside the audit wrapper (app.ts): execution is
|
|
7
|
+
timed, arguments redacted, outcome appended to the hash chain, webhooks
|
|
8
|
+
notified. A thrown `PolicyError`/`ScopeError` becomes a normal `isError`
|
|
9
|
+
result — never a 500.
|
|
10
|
+
|
|
11
|
+
## Permission gate (read this first)
|
|
12
|
+
|
|
13
|
+
`assertToolPermitted({ tool, scopes, readOnly, policy?, target? })` in
|
|
14
|
+
`core/policy.ts` — three checks, in order:
|
|
15
|
+
|
|
16
|
+
1. **scopes** — token's `scopes[]` empty = all groups; otherwise tool must
|
|
17
|
+
be in a listed group (group map in `TOOL_SCOPES`).
|
|
18
|
+
2. **readOnly** — token or global read-only refuses `MUTATING_TOOLS`.
|
|
19
|
+
3. **policy paths** — if `target` given: resolveReal() (symlinks collapsed,
|
|
20
|
+
deepest existing ancestor for not-yet-existing files) → deny-list wins →
|
|
21
|
+
must be inside an allow-list entry.
|
|
22
|
+
|
|
23
|
+
Scope groups: `filesystem shell system http git sqlite policy logs services
|
|
24
|
+
packages schedule security project planning formatting documents ops web`.
|
|
25
|
+
(`ramcp scopes` prints them with member tools.)
|
|
26
|
+
|
|
27
|
+
## Suites
|
|
28
|
+
|
|
29
|
+
### filesystem (7) — policy-gated on every path
|
|
30
|
+
| Tool | Notable |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `list_directory` | 2000-entry cap |
|
|
33
|
+
| `read_file` | `offset`/`limit` line windows |
|
|
34
|
+
| `write_file` | mkdir option |
|
|
35
|
+
| `edit_file` | exact-text replace, `all` flag |
|
|
36
|
+
| `delete_path` | rm -rf — destructive, policy-gated |
|
|
37
|
+
| `search_code` | recursive regex, skips .git/node_modules |
|
|
38
|
+
| `file_info` | stat essentials |
|
|
39
|
+
|
|
40
|
+
### shell (3) — behind token `shell_enabled`
|
|
41
|
+
| Tool | Notable |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `run_command` | platform shell via `shellCommand()`; ≤600s; 60KB output cap; cwd policy-gated |
|
|
44
|
+
| `process_list` | ps (POSIX) / Get-Process (Win) |
|
|
45
|
+
| `kill_process` | refuses gateway pid, ppid, PID 1 |
|
|
46
|
+
|
|
47
|
+
### system (3)
|
|
48
|
+
`system_info` · `disk_usage` (df / Get-Volume) · `network_interfaces` — all
|
|
49
|
+
cross-platform, no gates (read-only host facts).
|
|
50
|
+
|
|
51
|
+
### http (3) — SSRF-guarded
|
|
52
|
+
`http_request` · `port_check` · `web_fetch` — loopback/private/link-local
|
|
53
|
+
(169.254.0.0/16 incl. cloud metadata) refused; IPv4-mapped IPv6 too.
|
|
54
|
+
|
|
55
|
+
### git (1)
|
|
56
|
+
`git` — args validated: verb must match a whitelist (~45 verbs, no
|
|
57
|
+
`upload-pack`/`exec=`), no shell metacharacters in any arg; `repo_path`
|
|
58
|
+
policy-gated.
|
|
59
|
+
|
|
60
|
+
### sqlite (2) — policy-gated on db_path
|
|
61
|
+
`sqlite_query` — single statement only, ATTACH/DETACH refused, SELECT/WITH/
|
|
62
|
+
PRAGMA/EXPLAIN return rows, others report changes. `sqlite_schema`.
|
|
63
|
+
|
|
64
|
+
### logs (3) — policy-gated paths
|
|
65
|
+
`tail_logs` (1MB tail read) · `search_logs` (regex + context) · `journal`
|
|
66
|
+
(unit-name regex validated; journalctl / mac `log show` / Get-WinEvent).
|
|
67
|
+
|
|
68
|
+
### services (2)
|
|
69
|
+
`service_status` · `service_action` — `PROTECTED_UNITS` regex refuses ssh,
|
|
70
|
+
the gateway itself, dbus/networkd/WinDefend etc. Unit names regex-validated.
|
|
71
|
+
|
|
72
|
+
### packages (3)
|
|
73
|
+
`package_list` / `package_install` / `package_remove` — manager auto-detected
|
|
74
|
+
(apt / brew / winget / choco / npm); `package_remove` refuses
|
|
75
|
+
nodejs/npm/nginx/openssh/systemd/remote-access-mcp.
|
|
76
|
+
|
|
77
|
+
### schedule (3)
|
|
78
|
+
`schedule_command` (one-shot ISO time or recurring ≥60s; requires shell
|
|
79
|
+
token) · `list_scheduled_tasks` · `cancel_scheduled_task`. File-backed
|
|
80
|
+
(`schedule.json`), a 30s in-process ticker executes them.
|
|
81
|
+
|
|
82
|
+
### security (2)
|
|
83
|
+
`secret_scan` — 10 credential patterns (keys, AWS, GitHub, Slack, npm,
|
|
84
|
+
bearer, connection-string passwords); **output is masked**, only
|
|
85
|
+
file:line + type. `port_scan_local` — ss / lsof / Get-NetTCPConnection.
|
|
86
|
+
|
|
87
|
+
### project (2)
|
|
88
|
+
`analyze_project` (files/LOC/manifests/entry points, depth-capped walk) ·
|
|
89
|
+
`project_health_check` (git dirty, README presence, TODO density).
|
|
90
|
+
|
|
91
|
+
### planning (4)
|
|
92
|
+
`create_task_plan` · `task_status` (mark steps done) · `workspace_snapshot`
|
|
93
|
+
(content-addressed file copies) · `rollback_changes` (atomic restore).
|
|
94
|
+
Snapshot/rollback are in `MUTATING_TOOLS`.
|
|
95
|
+
|
|
96
|
+
### web (1)
|
|
97
|
+
`web_fetch` — SSRF-guarded public fetch, UA-tagged, size-capped.
|
|
98
|
+
|
|
99
|
+
### ops (2)
|
|
100
|
+
`environment_inspect` — env vars, secrets masked, long values truncated.
|
|
101
|
+
`nginx_inspect` — read-only; site names regex-validated (no traversal);
|
|
102
|
+
`nginx -T` summary or per-site config.
|
|
103
|
+
|
|
104
|
+
### policy (4) — token manages only its own sandbox
|
|
105
|
+
`list_allowed_paths` · `allow_path` · `deny_path` · `shell_enabled`. Mutations
|
|
106
|
+
persist via `saveConfig` and hot-reload; they cannot touch other tokens.
|
|
107
|
+
|
|
108
|
+
## CLI (`ramcp`)
|
|
109
|
+
|
|
110
|
+
| Command | Subcommands / flags | Notes |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `init` | `--paths a,b` | creates config + default token; re-run safe |
|
|
113
|
+
| `start` | `--tunnel --read-only --host --port` | foreground |
|
|
114
|
+
| `tunnel` | — | gateway + public URL; runtime state; self-checks reachability |
|
|
115
|
+
| `url` | `[name]` | live tunnel URL wins, else public_host/local |
|
|
116
|
+
| `token` | `add --name --paths --deny --scopes --shell --read-only --rpm --expires` · `list [--json]` · `show [--full]` · `rotate` · `revoke` | last token can't be revoked |
|
|
117
|
+
| `policy` | `allow <p…>` · `deny <p…>` · `shell on/off` · `scopes <groups|all>` (all `--token`) · `readonly on/off` (global) | multi-path, space-separated |
|
|
118
|
+
| `scopes` | — | group → tools map |
|
|
119
|
+
| `audit` | `--tool --since --limit --json` · `chain` | chain verifies hash chain, exit 2 on tamper |
|
|
120
|
+
| `doctor` | — | platform, config, tokens, gateway, service, tunnel, public, audit |
|
|
121
|
+
| `service` | `install [--domain] [--tunnel] [--dry-run]` · `uninstall` · `logs [-f]` · `status` | systemd / launchd / schtasks by OS |
|
|
122
|
+
| `schedule` | `list [--json]` | |
|
|
123
|
+
| `upgrade` | `--dry-run` | npm self-update + right restart per OS |
|
|
124
|
+
| `status` | — | incl. live runtime state |
|
|
125
|
+
| `version` | — | |
|
|
126
|
+
| `config` | `show` · `export [--out]` · `import FILE [--merge]` | export 0600, contains live tokens; merge preserves local identity |
|
|
127
|
+
| `webhook` | `add --url --events` · `list` · `on/off URL` · `remove URL` | events: `tool.error`, `tool.success`, `*` |
|
|
128
|
+
| ~~`fleet`~~ | — | **removed in v3.0.0** — unknown command now |
|
|
129
|
+
|
|
130
|
+
### Config file shape (v3)
|
|
131
|
+
|
|
132
|
+
```jsonc
|
|
133
|
+
{
|
|
134
|
+
"host": "127.0.0.1", "port": 8765, "public_host": "mcp.example.com",
|
|
135
|
+
"mcp_path": "/mcp", "mcp_path_aliases": ["/sse"],
|
|
136
|
+
"log_level": "info",
|
|
137
|
+
"audit": { "enabled": true, "db_path": "…/audit.jsonl" },
|
|
138
|
+
"read_only": false,
|
|
139
|
+
"tunnel": { "provider": "cloudflare", "auto_start": false },
|
|
140
|
+
"webhooks": [{ "url": "…", "events": ["tool.error"], "enabled": true }],
|
|
141
|
+
"tokens": [{
|
|
142
|
+
"id": "…", "name": "default", "token": "…", "created": "ISO",
|
|
143
|
+
"expires": "ISO?", "scopes": [], "read_only": false,
|
|
144
|
+
"max_requests_per_minute": 60?, "shell_enabled": true,
|
|
145
|
+
"allowed_paths": ["…"], "denied_paths": []
|
|
146
|
+
}]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Env overrides (tests, minimal deploys): `RAMCP_TOKEN RAMCP_HOST RAMCP_PORT
|
|
151
|
+
RAMCP_PUBLIC_HOST RAMCP_SHELL RAMCP_ALLOWED_PATHS RAMCP_DENIED_PATHS`
|
|
152
|
+
(legacy `DANA_*` still honored). `SSH_BIN` existed for fleet tests — gone.
|