@remcp/remcp 0.2.105 → 0.2.107
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/package.json +1 -1
- package/src/agent-update.mjs +18 -5
- package/src/agent.mjs +5 -3
- package/src/cli/connect.mjs +15 -0
- package/src/cli/service.mjs +1 -0
- package/src/cli/update.mjs +24 -18
- package/src/cli.mjs +12 -6
- package/src/runtime.mjs +4 -2
package/package.json
CHANGED
package/src/agent-update.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import process from 'node:process';
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
@@ -29,15 +29,28 @@ function globalNodeModules() {
|
|
|
29
29
|
return String(result.stdout || '').trim();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function resolveRuntimeEntry(base, runtime) {
|
|
33
|
+
const packageRoot = path.resolve(base, ...runtime.packageName.split('/'));
|
|
34
|
+
const candidate = path.resolve(packageRoot, ...runtime.entry.split(/[\\/]+/));
|
|
35
|
+
const relative = path.relative(packageRoot, candidate);
|
|
36
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('Runtime entry resolves outside its package');
|
|
37
|
+
if (!existsSync(candidate)) return null;
|
|
38
|
+
const realPackageRoot = realpathSync(packageRoot);
|
|
39
|
+
const realCandidate = realpathSync(candidate);
|
|
40
|
+
const realRelative = path.relative(realPackageRoot, realCandidate);
|
|
41
|
+
if (realRelative.startsWith('..') || path.isAbsolute(realRelative) || !lstatSync(realCandidate).isFile()) throw new Error('Runtime entry is not a regular package file');
|
|
42
|
+
return realCandidate;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export function localRuntimeEntry(runtimeValue) {
|
|
33
46
|
const runtime = normalizeRuntime(runtimeValue);
|
|
34
47
|
const localModules = packageLocalNodeModules();
|
|
35
48
|
if (localModules) {
|
|
36
|
-
const sibling =
|
|
37
|
-
if (
|
|
49
|
+
const sibling = resolveRuntimeEntry(localModules, runtime);
|
|
50
|
+
if (sibling) return sibling;
|
|
38
51
|
}
|
|
39
|
-
const candidate =
|
|
40
|
-
if (!
|
|
52
|
+
const candidate = resolveRuntimeEntry(globalNodeModules(), runtime);
|
|
53
|
+
if (!candidate) throw new Error('ReMCP local runtime is not installed. Run `remcp install`.');
|
|
41
54
|
return candidate;
|
|
42
55
|
}
|
|
43
56
|
|
package/src/agent.mjs
CHANGED
|
@@ -47,10 +47,12 @@ const RUNTIME_TOOLS_RETRY_MAX_MS = 30_000;
|
|
|
47
47
|
// timeout while the device keeps working invisibly.
|
|
48
48
|
const CALL_TIMEOUT_MARGIN_MS = 10_000;
|
|
49
49
|
|
|
50
|
-
function normalizedServerUrl(value) {
|
|
50
|
+
function normalizedServerUrl(value, { allowInsecure = false } = {}) {
|
|
51
51
|
const url = new URL(String(value || ''));
|
|
52
52
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error('serverUrl must use http or https');
|
|
53
53
|
if (url.username || url.password) throw new Error('serverUrl must not embed credentials');
|
|
54
|
+
const loopback = ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname.toLowerCase());
|
|
55
|
+
if (url.protocol === 'http:' && !loopback && !allowInsecure) throw new Error('Remote serverUrl must use https');
|
|
54
56
|
url.hash = '';
|
|
55
57
|
url.search = '';
|
|
56
58
|
return url.href.replace(/\/$/, '');
|
|
@@ -88,7 +90,7 @@ function deviceMetrics(extra = {}) {
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
export async function runAgent(options) {
|
|
91
|
-
const serverUrl = normalizedServerUrl(options.serverUrl);
|
|
93
|
+
const serverUrl = normalizedServerUrl(options.serverUrl, { allowInsecure: options.allowInsecureTransport === true || process.env.REMCP_ALLOW_INSECURE_TRANSPORT === 'true' });
|
|
92
94
|
const deviceToken = String(options.deviceToken || '');
|
|
93
95
|
const deviceId = String(options.deviceId || '');
|
|
94
96
|
const deviceName = String(options.deviceName || os.hostname());
|
|
@@ -524,7 +526,7 @@ export async function runAgent(options) {
|
|
|
524
526
|
try {
|
|
525
527
|
// This request contains no local file payload; the config-derived URL is the explicitly paired relay.
|
|
526
528
|
// codeql[js/file-access-to-http]
|
|
527
|
-
const response = await fetch(`${serverUrl}/api/agent/version`, { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) });
|
|
529
|
+
const response = await fetch(`${serverUrl}/api/agent/version`, { redirect: 'error', signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) });
|
|
528
530
|
if (!response.ok) return;
|
|
529
531
|
const advertised = await response.json();
|
|
530
532
|
const minimum = advertised.minimum;
|
package/src/cli/connect.mjs
CHANGED
|
@@ -29,6 +29,18 @@ export function openInBrowser(url) {
|
|
|
29
29
|
} catch {}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export function assertSecureServerUrl(value, { allowInsecure = false } = {}) {
|
|
33
|
+
let url;
|
|
34
|
+
try { url = new URL(String(value || '')); } catch { throw new Error('server must be a valid URL'); }
|
|
35
|
+
if (!['https:', 'http:'].includes(url.protocol)) throw new Error('server must use http or https');
|
|
36
|
+
if (url.username || url.password) throw new Error('server must not embed credentials');
|
|
37
|
+
const loopback = ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname.toLowerCase());
|
|
38
|
+
if (url.protocol === 'http:' && !loopback && !allowInsecure) throw new Error('Remote server must use https');
|
|
39
|
+
url.hash = '';
|
|
40
|
+
url.search = '';
|
|
41
|
+
return url.href.replace(/\/$/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
const TRANSIENT_FETCH_RETRY_DELAY_MS = 150;
|
|
33
45
|
|
|
34
46
|
async function fetchWithTransientRetry(url, options) {
|
|
@@ -48,7 +60,9 @@ async function fetchWithTransientRetry(url, options) {
|
|
|
48
60
|
// browser while signed in, and this process collects the credential by polling. The device never
|
|
49
61
|
// sees a browser session or an account password.
|
|
50
62
|
export async function pairWithDeviceCode(server, flags) {
|
|
63
|
+
server = assertSecureServerUrl(server, { allowInsecure: flags?.['allow-insecure-transport'] === true });
|
|
51
64
|
const authorization = await fetchWithTransientRetry(`${server}/oauth/device_authorization`, {
|
|
65
|
+
redirect: 'error',
|
|
52
66
|
method: 'POST',
|
|
53
67
|
headers: { 'content-type': 'application/json' },
|
|
54
68
|
body: JSON.stringify({
|
|
@@ -73,6 +87,7 @@ export async function pairWithDeviceCode(server, flags) {
|
|
|
73
87
|
await sleep(intervalMs);
|
|
74
88
|
const response = await fetchWithTransientRetry(`${server}/oauth/token`, {
|
|
75
89
|
method: 'POST',
|
|
90
|
+
redirect: 'error',
|
|
76
91
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
77
92
|
body: new URLSearchParams({
|
|
78
93
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
package/src/cli/service.mjs
CHANGED
package/src/cli/update.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { isRuntimeSpecFor, normalizeRuntime } from '../runtime.mjs';
|
|
|
9
9
|
import { PACKAGE_NAME, VERSION } from '../version.mjs';
|
|
10
10
|
|
|
11
11
|
import { loadConfig, saveConfig } from './config.mjs';
|
|
12
|
+
import { assertSecureServerUrl } from './connect.mjs';
|
|
12
13
|
import { installedVersion } from './doctor.mjs';
|
|
13
14
|
import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
|
|
14
15
|
import { currentInstallationInfo, ensureMacCliCommand, ensureServiceIfRecorded, installationVersionsAtCliPath, npmGlobalUpdate, persistentServiceExpected, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, syncKnownInstallations } from './service.mjs';
|
|
@@ -48,11 +49,15 @@ export async function resolveUpdateTargets({
|
|
|
48
49
|
env = process.env,
|
|
49
50
|
fetchImpl = globalThis.fetch,
|
|
50
51
|
}) {
|
|
52
|
+
const validatedServerUrl = assertSecureServerUrl(cfg.serverUrl, {
|
|
53
|
+
allowInsecure: cfg.allowInsecureTransport === true || flags['allow-insecure-transport'] === true || env.REMCP_ALLOW_INSECURE_TRANSPORT === 'true',
|
|
54
|
+
});
|
|
55
|
+
const validatedCfg = { ...cfg, serverUrl: validatedServerUrl };
|
|
51
56
|
const requestedRuntime = typeof flags.runtime === 'string' ? flags.runtime.trim() : '';
|
|
52
57
|
const requestedClient = typeof flags.client === 'string' ? flags.client.trim() : '';
|
|
53
58
|
const latestClient = `${PACKAGE_NAME}@latest`;
|
|
54
59
|
const currentClient = `${PACKAGE_NAME}@${VERSION}`;
|
|
55
|
-
const trusted = runtimeTrustAllowed(
|
|
60
|
+
const trusted = runtimeTrustAllowed(validatedCfg, flags, env);
|
|
56
61
|
|
|
57
62
|
let clientSpec = latestClient;
|
|
58
63
|
if (requestedClient) {
|
|
@@ -63,28 +68,28 @@ export async function resolveUpdateTargets({
|
|
|
63
68
|
if (requestedRuntime) {
|
|
64
69
|
// Only `<configured package>@<semver>` is installable: an alias, a git/URL/file spec, a tag or
|
|
65
70
|
// a range would run code the user never agreed to.
|
|
66
|
-
if (!isRuntimeSpecFor(
|
|
67
|
-
throw new Error(`--runtime must be ${
|
|
71
|
+
if (!isRuntimeSpecFor(validatedCfg.runtime.packageName, requestedRuntime)) {
|
|
72
|
+
throw new Error(`--runtime must be ${validatedCfg.runtime.packageName}@<version>`);
|
|
68
73
|
}
|
|
69
74
|
if (!trusted) {
|
|
70
|
-
throw new Error(`This machine was paired without trusting ${
|
|
75
|
+
throw new Error(`This machine was paired without trusting ${validatedCfg.serverUrl} to choose a runtime version. Re-run with --trust-runtime if you trust that server.`);
|
|
71
76
|
}
|
|
72
77
|
const runtimeSpec = normalizeRuntime({
|
|
73
78
|
kind: 'npm',
|
|
74
|
-
packageName:
|
|
79
|
+
packageName: validatedCfg.runtime.packageName,
|
|
75
80
|
packageSpec: requestedRuntime,
|
|
76
|
-
entry:
|
|
81
|
+
entry: validatedCfg.runtime.entry,
|
|
77
82
|
}).packageSpec;
|
|
78
|
-
if (requestedClient && !releasePairMatches(clientSpec,
|
|
83
|
+
if (requestedClient && !releasePairMatches(clientSpec, validatedCfg.runtime.packageName, runtimeSpec)) {
|
|
79
84
|
throw new Error('The client and first-party runtime must use the same exact release version.');
|
|
80
85
|
}
|
|
81
|
-
if (!requestedClient &&
|
|
86
|
+
if (!requestedClient && validatedCfg.runtime.packageName === '@remcp/runtime' && specVersion(validatedCfg.runtime.packageName, runtimeSpec) !== VERSION) {
|
|
82
87
|
throw new Error(`--runtime without --client must match the running client version ${VERSION}`);
|
|
83
88
|
}
|
|
84
89
|
return {
|
|
85
90
|
clientSpec: requestedClient ? clientSpec : currentClient,
|
|
86
91
|
runtimeSpec,
|
|
87
|
-
persistRuntime: runtimeSpec !==
|
|
92
|
+
persistRuntime: runtimeSpec !== validatedCfg.runtime.packageSpec,
|
|
88
93
|
installable: true,
|
|
89
94
|
source: requestedClient ? 'explicit-pair' : 'explicit-runtime',
|
|
90
95
|
warning: '',
|
|
@@ -100,7 +105,7 @@ export async function resolveUpdateTargets({
|
|
|
100
105
|
if (!trusted) {
|
|
101
106
|
return {
|
|
102
107
|
clientSpec,
|
|
103
|
-
runtimeSpec:
|
|
108
|
+
runtimeSpec: validatedCfg.runtime.packageSpec,
|
|
104
109
|
persistRuntime: false,
|
|
105
110
|
installable: true,
|
|
106
111
|
source: 'configured',
|
|
@@ -109,15 +114,16 @@ export async function resolveUpdateTargets({
|
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
try {
|
|
112
|
-
const versionUrl = new URL('/api/agent/version',
|
|
117
|
+
const versionUrl = new URL('/api/agent/version', validatedCfg.serverUrl).toString();
|
|
113
118
|
const response = await fetchImpl(versionUrl, {
|
|
114
119
|
headers: { accept: 'application/json' },
|
|
120
|
+
redirect: 'error',
|
|
115
121
|
signal: AbortSignal.timeout(UPDATE_DISCOVERY_TIMEOUT_MS),
|
|
116
122
|
});
|
|
117
123
|
if (!response?.ok) {
|
|
118
124
|
return {
|
|
119
125
|
clientSpec: currentClient,
|
|
120
|
-
runtimeSpec:
|
|
126
|
+
runtimeSpec: validatedCfg.runtime.packageSpec,
|
|
121
127
|
persistRuntime: false,
|
|
122
128
|
installable: false,
|
|
123
129
|
source: 'configured',
|
|
@@ -130,11 +136,11 @@ export async function resolveUpdateTargets({
|
|
|
130
136
|
const advertisedClient = exactClientSpec(advertised?.cli);
|
|
131
137
|
const clientCandidate = requestedClient || advertisedClient;
|
|
132
138
|
if (!advertisedClient
|
|
133
|
-
|| !releasePairMatches(advertisedClient,
|
|
139
|
+
|| !releasePairMatches(advertisedClient, validatedCfg.runtime.packageName, runtimeCandidate)
|
|
134
140
|
|| (requestedClient && requestedClient !== advertisedClient)) {
|
|
135
141
|
return {
|
|
136
142
|
clientSpec: currentClient,
|
|
137
|
-
runtimeSpec:
|
|
143
|
+
runtimeSpec: validatedCfg.runtime.packageSpec,
|
|
138
144
|
persistRuntime: false,
|
|
139
145
|
installable: false,
|
|
140
146
|
source: 'configured',
|
|
@@ -144,14 +150,14 @@ export async function resolveUpdateTargets({
|
|
|
144
150
|
|
|
145
151
|
const runtimeSpec = normalizeRuntime({
|
|
146
152
|
kind: 'npm',
|
|
147
|
-
packageName:
|
|
153
|
+
packageName: validatedCfg.runtime.packageName,
|
|
148
154
|
packageSpec: runtimeCandidate,
|
|
149
|
-
entry:
|
|
155
|
+
entry: validatedCfg.runtime.entry,
|
|
150
156
|
}).packageSpec;
|
|
151
157
|
return {
|
|
152
158
|
clientSpec: clientCandidate,
|
|
153
159
|
runtimeSpec,
|
|
154
|
-
persistRuntime: runtimeSpec !==
|
|
160
|
+
persistRuntime: runtimeSpec !== validatedCfg.runtime.packageSpec,
|
|
155
161
|
installable: true,
|
|
156
162
|
source: 'server',
|
|
157
163
|
warning: '',
|
|
@@ -159,7 +165,7 @@ export async function resolveUpdateTargets({
|
|
|
159
165
|
} catch (error) {
|
|
160
166
|
return {
|
|
161
167
|
clientSpec: currentClient,
|
|
162
|
-
runtimeSpec:
|
|
168
|
+
runtimeSpec: validatedCfg.runtime.packageSpec,
|
|
163
169
|
persistRuntime: false,
|
|
164
170
|
installable: false,
|
|
165
171
|
source: 'configured',
|
package/src/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { PACKAGE_NAME, VERSION } from './version.mjs';
|
|
|
8
8
|
import { probeFilesystemAccess } from './fs-access.mjs';
|
|
9
9
|
|
|
10
10
|
import { ensureMachineId, loadConfig, readJsonFile, saveConfig, setTelemetry, telemetryState, writeJsonFile } from './cli/config.mjs';
|
|
11
|
-
import { assertRuntimeTrust, pairWithDeviceCode } from './cli/connect.mjs';
|
|
11
|
+
import { assertRuntimeTrust, assertSecureServerUrl, pairWithDeviceCode } from './cli/connect.mjs';
|
|
12
12
|
import { diagnoseLocalRuntime, installedVersion, runtimeAllowedRoots } from './cli/doctor.mjs';
|
|
13
13
|
import { configFile, npm, officialOrigin, runtimeConfigFile } from './cli/env.mjs';
|
|
14
14
|
import { currentInstallationInfo, ensureMacCliCommand, installPersistentAgent, persistentServiceState, rememberCurrentInstallation, restartPersistentServiceIfInstalled, serviceInstallationInfo, uninstallPersistentService } from './cli/service.mjs';
|
|
@@ -49,16 +49,19 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
49
49
|
// Like the desktop-app flow this mirrors: with no flags the command talks to the official
|
|
50
50
|
// server, prints a code, opens the browser to approve it, and pairs. `--code` keeps working for
|
|
51
51
|
// the workspace-generated command and for CI, and `--server` for self-hosted deployments.
|
|
52
|
-
const server =
|
|
52
|
+
const server = assertSecureServerUrl(flags.server || officialOrigin, { allowInsecure: flags['allow-insecure-transport'] === true });
|
|
53
53
|
const code = String(flags.code || '').replace(/\s+/g, '').toUpperCase();
|
|
54
54
|
assertRuntimeTrust(server, flags);
|
|
55
|
+
|
|
55
56
|
let paired;
|
|
56
57
|
let deviceInitiated = false;
|
|
57
58
|
if (code) {
|
|
58
59
|
const response = await fetch(`${server}/api/pair/claim`, {
|
|
59
60
|
method: 'POST',
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
redirect: 'error',
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ code, machineId: ensureMachineId(), name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch }),
|
|
64
|
+
|
|
62
65
|
});
|
|
63
66
|
if (!response.ok) throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
|
|
64
67
|
paired = await response.json();
|
|
@@ -81,6 +84,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
81
84
|
// Remember whether this machine's owner trusted the server to name a runtime version. The
|
|
82
85
|
// auto-updater must not widen that decision on its own later.
|
|
83
86
|
trustRuntime: Boolean(flags['trust-runtime']) || new URL(server).origin === officialOrigin,
|
|
87
|
+
allowInsecureTransport: flags['allow-insecure-transport'] === true,
|
|
88
|
+
|
|
84
89
|
};
|
|
85
90
|
saveConfig(config);
|
|
86
91
|
const accountEmail = String(paired.account?.email || '');
|
|
@@ -194,11 +199,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
194
199
|
|
|
195
200
|
if (command === 'status' || command === 'doctor') {
|
|
196
201
|
const cfg = rememberCurrentInstallation(loadConfig());
|
|
202
|
+
const server = assertSecureServerUrl(cfg.serverUrl, { allowInsecure: cfg.allowInsecureTransport === true || process.env.REMCP_ALLOW_INSECURE_TRANSPORT === 'true' });
|
|
197
203
|
const [health, advertised] = await Promise.all([
|
|
198
|
-
fetchConfiguredServer(`${
|
|
204
|
+
fetchConfiguredServer(`${server}/health?fresh=${Date.now()}`, { cache:'no-store', redirect:'error' })
|
|
199
205
|
.then(async response => ({ reachable:response.ok && (await response.json().catch(() => null))?.ok === true }))
|
|
200
206
|
.catch(error => ({ reachable:false, error:error.message })),
|
|
201
|
-
fetchConfiguredServer(`${
|
|
207
|
+
fetchConfiguredServer(`${server}/api/agent/version?fresh=${Date.now()}`, { cache:'no-store', redirect:'error' })
|
|
202
208
|
.then(response => response.ok ? response.json() : null)
|
|
203
209
|
.catch(() => null),
|
|
204
210
|
]);
|
package/src/runtime.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
const PACKAGE_NAME = /^(?:@[a-z0-9._-]
|
|
3
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\/[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?|[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)$/i;
|
|
4
4
|
// A runtime spec is a plain published version of one package. Anything else — an npm alias
|
|
5
5
|
// (`npm:@other/pkg`), a git/https/file spec, a tag, or a version range — is a way to make a device
|
|
6
6
|
// install and execute code the user never agreed to, so only `name@x.y.z[-pre][+build]` is accepted.
|
|
@@ -13,7 +13,9 @@ export function normalizeRuntime(value) {
|
|
|
13
13
|
const entry = String(value.entry || '');
|
|
14
14
|
if (!PACKAGE_NAME.test(packageName)) throw new Error('Pairing server returned an invalid runtime package name');
|
|
15
15
|
if (!isRuntimeSpecFor(packageName, packageSpec)) throw new Error(`Runtime package spec must be ${packageName}@<version>`);
|
|
16
|
-
|
|
16
|
+
const entryParts = entry.split(/[\\/]+/);
|
|
17
|
+
if (!entry || entry.includes('\0') || path.isAbsolute(entry) || entryParts.some(part => !part || part === '.' || part === '..')) throw new Error('Pairing server returned an invalid runtime entry');
|
|
18
|
+
|
|
17
19
|
return { kind: 'npm', packageName, packageSpec, entry };
|
|
18
20
|
}
|
|
19
21
|
|