@remcp/runtime 0.2.106 → 0.2.108
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/CHANGELOG.md +6 -0
- package/README.md +14 -2
- package/package.json +2 -2
- package/src/catalog.mjs +8 -0
- package/src/compact-catalog.mjs +2 -2
- package/src/config.mjs +12 -2
- package/src/documents.mjs +7 -0
- package/src/extended/browser.mjs +431 -34
- package/src/extended/catalog.mjs +11 -8
- package/src/extended/desktop.mjs +14 -10
- package/src/extended/documents.mjs +389 -69
- package/src/index.mjs +2 -2
- package/src/mcp-v2.mjs +7 -3
- package/src/policy.mjs +42 -9
- package/src/tools/files.mjs +665 -85
- package/src/tools/terminal.mjs +4 -2
- package/src/util.mjs +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.108 — 2026-09-25
|
|
4
|
+
|
|
5
|
+
- Standalone production runtimes keep browser control disabled unless explicitly enabled; the authenticated paired agent enables its owned local runtime. IP-literal production allowlisting and persistent CDP guards cover page and worker sessions, delayed navigation, redirects, and subresources.
|
|
6
|
+
- Archive and document operations use descriptor-bound Linux traversal where available; macOS/Windows preserve bounded source snapshots, canonical confinement, link/special-file rejection, and atomic staging through portable filesystem APIs instead of losing document/archive tools.
|
|
7
|
+
- Linux semantic scroll/drag targets refresh accessibility geometry, and empty AT-SPI snapshots retry once before using the conservative depth fallback. Runtime tool names and schemas are unchanged.
|
|
8
|
+
|
|
3
9
|
## 0.2.106 — 2026-09-22
|
|
4
10
|
|
|
5
11
|
- Wayland EIS records the emulated device used for each button press and sends the matching release through that same device, preventing cross-region/device drags from losing `pointerup`.
|
package/README.md
CHANGED
|
@@ -143,6 +143,12 @@ in `~/.config/remcp/runtime.json`. `--describe` always reports the current state
|
|
|
143
143
|
agent. Browser control accepts only loopback CDP endpoints. The explicit `network` diagnostic can
|
|
144
144
|
open a bounded TCP connection to a host/port, and shell/browser actions can reach the network when
|
|
145
145
|
the requested operation itself requires it.
|
|
146
|
+
- **Remote browser safety.** A standalone production runtime keeps browser control disabled unless
|
|
147
|
+
`REMCP_BROWSER_REMOTE_ENABLED=1`; the authenticated ReMCP device agent explicitly enables it for the local
|
|
148
|
+
runtime it owns. Production navigation requires an explicit `REMCP_BROWSER_ALLOWED_HOSTS` allowlist of IP
|
|
149
|
+
literals. Hostname navigation is refused unless `REMCP_BROWSER_ALLOW_DNS_REBIND=1` is set behind an external
|
|
150
|
+
DNS/egress boundary. Local navigation requires a separate `REMCP_BROWSER_ALLOW_LOCAL_NAVIGATION=1` opt-in,
|
|
151
|
+
and CDP request guards reject private, file, and WebSocket destinations.
|
|
146
152
|
- **Optional confinement.** `allowedRoots` is empty by default. When you set it, it is enforced
|
|
147
153
|
against the resolved real path of the deepest existing ancestor rather than the lexical string, so
|
|
148
154
|
`<allowed>/link -> /etc` cannot be used to read or write outside the allowed directories.
|
|
@@ -159,6 +165,10 @@ in `~/.config/remcp/runtime.json`. `--describe` always reports the current state
|
|
|
159
165
|
transport limit), writes are capped (`maxWriteBytes`), buffered session output is capped by both
|
|
160
166
|
line count and total characters - a stream with no newlines cannot grow without limit - and reads
|
|
161
167
|
are paged or chunked.
|
|
168
|
+
- **Filesystem structural safety.** Archive and document snapshot/publication operations use
|
|
169
|
+
descriptor-bound directory traversal on Linux. macOS and Windows preserve the same bounded snapshot,
|
|
170
|
+
link/special-file rejection, canonical root confinement and atomic staging semantics, while using their
|
|
171
|
+
portable path APIs where Node does not expose directory-fd-relative open/rename/unlink primitives.
|
|
162
172
|
- **Crash-resistant sessions.** A bogus shell, a closed stdin, or a dead parent cannot take the
|
|
163
173
|
runtime down; sessions run in their own process group so `force_terminate` stops a whole pipeline;
|
|
164
174
|
the agent restarts the runtime if it ever exits, so a device recovers instead of going silently
|
|
@@ -195,8 +205,10 @@ Optional settings live in `~/.config/remcp/runtime.json` (override the directory
|
|
|
195
205
|
Every value can also be set with an environment variable: `REMCP_RUNTIME_ALLOWED_ROOTS`,
|
|
196
206
|
`REMCP_RUNTIME_BLOCKED_COMMANDS`, `REMCP_RUNTIME_DANGEROUS_COMMANDS`, `REMCP_RUNTIME_TELEMETRY`,
|
|
197
207
|
`REMCP_RUNTIME_DISABLE_TELEMETRY`, `REMCP_RUNTIME_MAX_OUTPUT_BYTES`, `REMCP_RUNTIME_MAX_READ_LINES`,
|
|
198
|
-
`REMCP_RUNTIME_MAX_BUFFERED_LINES`, `REMCP_RUNTIME_MAX_WRITE_BYTES`, `REMCP_RUNTIME_SHELL`,
|
|
199
|
-
`REMCP_RUNTIME_NAME
|
|
208
|
+
`REMCP_RUNTIME_MAX_BUFFERED_LINES`, `REMCP_RUNTIME_MAX_WRITE_BYTES`, `REMCP_RUNTIME_SHELL`,
|
|
209
|
+
`REMCP_RUNTIME_NAME`, `REMCP_BROWSER_REMOTE_ENABLED`, `REMCP_BROWSER_ALLOWED_HOSTS`,
|
|
210
|
+
`REMCP_BROWSER_ALLOW_LOCAL_NAVIGATION`, `REMCP_BROWSER_ALLOW_DNS_REBIND`.
|
|
211
|
+
|
|
200
212
|
|
|
201
213
|
`allowedRoots` is empty by default, which means the paired device can reach anything the operating
|
|
202
214
|
system user running the agent can reach. Set it when you want the device to be scoped to specific
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remcp/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.108",
|
|
4
4
|
"description": "First-party ReMCP local device runtime: filesystem, terminal, computer-use, browser, diagnostics and document tools over MCP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"start": "node src/index.mjs",
|
|
23
23
|
"check": "node scripts/check.mjs",
|
|
24
|
-
"test": "node --test test/*.test.mjs"
|
|
24
|
+
"test": "node --import ./test/setup.mjs --test test/*.test.mjs"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@jellybrick/dbus-next": "^0.11.3",
|
package/src/catalog.mjs
CHANGED
|
@@ -830,6 +830,14 @@ export const toolDefinitions = [
|
|
|
830
830
|
},
|
|
831
831
|
];
|
|
832
832
|
|
|
833
|
+
export function coreToolSupported(definition) {
|
|
834
|
+
return !['create_archive', 'extract_archive'].includes(definition.name) || process.platform === 'linux';
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export function supportedCoreTools() {
|
|
838
|
+
return toolDefinitions.filter(coreToolSupported);
|
|
839
|
+
}
|
|
840
|
+
|
|
833
841
|
export function advertisedTools() {
|
|
834
842
|
return toolDefinitions.map(({ name, title, description, inputSchema, annotations, outputSchema }) => ({
|
|
835
843
|
name,
|
package/src/compact-catalog.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TEXT_OUTPUT_SCHEMA, toolDefinitions } from './catalog.mjs';
|
|
1
|
+
import { TEXT_OUTPUT_SCHEMA, supportedCoreTools, toolDefinitions } from './catalog.mjs';
|
|
2
2
|
import { advertisedExtendedTools, extendedToolDefinitions } from './extended/catalog.mjs';
|
|
3
3
|
|
|
4
4
|
export const compactRuntimeGroups = Object.freeze({
|
|
@@ -127,7 +127,7 @@ function groupedDefinition(name, members) {
|
|
|
127
127
|
export async function supportedRuntimeOperationNames() {
|
|
128
128
|
const supportedExtended = new Set((await advertisedExtendedTools()).map(tool => tool.name));
|
|
129
129
|
return new Set([
|
|
130
|
-
...
|
|
130
|
+
...supportedCoreTools().map(tool => tool.name),
|
|
131
131
|
...extendedToolDefinitions.filter(tool => supportedExtended.has(tool.name)).map(tool => tool.name),
|
|
132
132
|
]);
|
|
133
133
|
}
|
package/src/config.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { expandHome } from './util.mjs';
|
|
5
5
|
|
|
6
6
|
const configDir = process.env.REMCP_RUNTIME_CONFIG_DIR || path.join(os.homedir(), '.config', 'remcp');
|
|
@@ -161,7 +161,17 @@ export function persistConfigValue(key, value) {
|
|
|
161
161
|
const file = readConfigFile();
|
|
162
162
|
const next = { ...file, [key]: value };
|
|
163
163
|
mkdirSync(runtimeConfigDir, { recursive: true, mode: 0o700 });
|
|
164
|
-
|
|
164
|
+
const existing = lstatSync(runtimeConfigPath, { throwIfNoEntry: false });
|
|
165
|
+
if (existing && (existing.isSymbolicLink() || !existing.isFile())) throw new Error('runtime configuration path must be a regular file');
|
|
166
|
+
const temporary = `${runtimeConfigPath}.${process.pid}.${Date.now()}.tmp`;
|
|
167
|
+
try {
|
|
168
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
169
|
+
renameSync(temporary, runtimeConfigPath);
|
|
170
|
+
chmodSync(runtimeConfigPath, 0o600);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
try { unlinkSync(temporary); } catch {}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
165
175
|
return runtimeConfigPath;
|
|
166
176
|
}
|
|
167
177
|
|
package/src/documents.mjs
CHANGED
|
@@ -14,6 +14,8 @@ import { inflateRawSync, inflateSync } from 'node:zlib';
|
|
|
14
14
|
// Nothing here writes, and nothing leaves the computer.
|
|
15
15
|
|
|
16
16
|
const MAX_INFLATE_BYTES = 32 * 1024 * 1024;
|
|
17
|
+
const MAX_PDF_DECODED_BYTES = 128 * 1024 * 1024;
|
|
18
|
+
const MAX_PDF_STREAMS = 10_000;
|
|
17
19
|
|
|
18
20
|
function inflate(buffer, raw = false) {
|
|
19
21
|
try {
|
|
@@ -260,6 +262,8 @@ function pdfTextPieces(content) {
|
|
|
260
262
|
export function readPdfText(buffer) {
|
|
261
263
|
const raw = buffer.toString('latin1');
|
|
262
264
|
const chunks = [];
|
|
265
|
+
let decodedBytes = 0;
|
|
266
|
+
let streamCount = 0;
|
|
263
267
|
let index = 0;
|
|
264
268
|
while (index < raw.length) {
|
|
265
269
|
const streamStart = raw.indexOf('stream', index);
|
|
@@ -271,6 +275,9 @@ export function readPdfText(buffer) {
|
|
|
271
275
|
if (end < 0) break;
|
|
272
276
|
const body = Buffer.from(raw.slice(start, end), 'latin1');
|
|
273
277
|
const decoded = body.subarray(0, 5).toString('latin1') === '<?xml' ? body : (inflate(body) ?? inflate(body, true) ?? body);
|
|
278
|
+
decodedBytes += decoded.length;
|
|
279
|
+
streamCount += 1;
|
|
280
|
+
if (decodedBytes > MAX_PDF_DECODED_BYTES || streamCount > MAX_PDF_STREAMS) throw new Error('PDF decoded content exceeds the safety limit');
|
|
274
281
|
chunks.push(decoded.toString('latin1'));
|
|
275
282
|
index = end + 'endstream'.length;
|
|
276
283
|
}
|