@remcp/remcp 0.2.39 → 0.2.41
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 +17 -5
- package/src/agent.mjs +5 -4
- package/src/cli/update.mjs +167 -26
package/package.json
CHANGED
package/src/agent-update.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import process from 'node:process';
|
|
|
4
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
5
|
import { resolveNpm } from './npm.mjs';
|
|
6
6
|
import { isRuntimeSpecFor, normalizeRuntime } from './runtime.mjs';
|
|
7
|
-
import { VERSION } from './version.mjs';
|
|
7
|
+
import { PACKAGE_NAME, VERSION } from './version.mjs';
|
|
8
8
|
|
|
9
9
|
const npm = resolveNpm();
|
|
10
10
|
// How long a handing-over agent waits for its replacement to take the device over before it keeps
|
|
@@ -61,15 +61,27 @@ function isNewer(candidate, current) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export function updateInvocationArgs({ target, runtime }, trustRuntime = false) {
|
|
65
|
+
return [
|
|
66
|
+
'update',
|
|
67
|
+
...(trustRuntime ? ['--trust-runtime'] : []),
|
|
68
|
+
...(target ? ['--client', target] : []),
|
|
69
|
+
...(runtime ? ['--runtime', runtime] : []),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
// What the agent should install, if anything. The client version alone is not enough: a machine
|
|
65
74
|
// that already runs the newest client but an older local runtime would otherwise never catch up,
|
|
66
75
|
// because its runtime is what executes the tools.
|
|
67
76
|
export function updateDecision({ advertised, cliVersion, runtimeVersion, runtimePackageName, runtimeDown = false }) {
|
|
68
|
-
const
|
|
77
|
+
const advertisedCli = String(advertised?.cli || '');
|
|
69
78
|
const advertisedRuntime = String(advertised?.runtime || '');
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
const
|
|
79
|
+
// Only exact versions of the first-party client and the configured runtime package may cross
|
|
80
|
+
// from server metadata into the updater. Invalid specs are ignored instead of being handed to npm.
|
|
81
|
+
const cliSpec = advertisedCli && isRuntimeSpecFor(PACKAGE_NAME, advertisedCli) ? advertisedCli : '';
|
|
82
|
+
const runtimeSpec = runtimePackageName && advertisedRuntime && isRuntimeSpecFor(runtimePackageName, advertisedRuntime)
|
|
83
|
+
? advertisedRuntime
|
|
84
|
+
: '';
|
|
73
85
|
const installedRuntime = String(runtimeVersion || '');
|
|
74
86
|
const runtimeKnown = Boolean(installedRuntime) && !/^unknown$/i.test(installedRuntime);
|
|
75
87
|
if (isNewer(cliSpec, cliVersion)) return { needed: true, target: cliSpec, runtime: runtimeSpec, reason: 'client' };
|
package/src/agent.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
restartToApplyUpdate,
|
|
14
14
|
supervisorRestart,
|
|
15
15
|
updateDecision,
|
|
16
|
+
updateInvocationArgs,
|
|
16
17
|
} from './agent-update.mjs';
|
|
17
18
|
|
|
18
19
|
export { localRuntimeEntry, supervisorRestart, updateDecision } from './agent-update.mjs';
|
|
@@ -369,10 +370,10 @@ export async function runAgent(options) {
|
|
|
369
370
|
lastAttemptAt = Date.now();
|
|
370
371
|
updateInFlight = true;
|
|
371
372
|
console.log(`Updating ReMCP to ${target}${decision.runtime ? ` with ${decision.runtime}` : ''} (${decision.reason})…`);
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
const
|
|
375
|
-
const child = spawn(process.execPath, [cli,
|
|
373
|
+
// Keep the updater pinned to the exact release pair this trusted server advertised. That
|
|
374
|
+
// prevents a newer public npm tag from getting ahead of production during a public-first rollout.
|
|
375
|
+
const updateArgs = updateInvocationArgs(decision, options.trustRuntime === true);
|
|
376
|
+
const child = spawn(process.execPath, [cli, ...updateArgs], {
|
|
376
377
|
detached: true,
|
|
377
378
|
// The updater's own output is the only record of why an install failed, so it is piped back
|
|
378
379
|
// into this agent's log instead of being discarded.
|
package/src/cli/update.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// `remcp update`: install
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// `remcp update`: install the release pair advertised by the paired server, then let whoever
|
|
2
|
+
// supervises the agent restart it. Only exact versions of the configured first-party packages are
|
|
3
|
+
// installable, so a server cannot point this machine at a different package or a git/URL/tag spec.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
|
|
@@ -13,51 +13,192 @@ import { installedVersion } from './doctor.mjs';
|
|
|
13
13
|
import { linuxServiceFile, macServiceFile, officialOrigin } from './env.mjs';
|
|
14
14
|
import { ensureServiceIfRecorded, npmGlobalInstall, restartPersistentServiceIfInstalled } from './service.mjs';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const UPDATE_DISCOVERY_TIMEOUT_MS = 5000;
|
|
17
|
+
|
|
18
|
+
function runtimeTrustAllowed(cfg, flags, env = process.env) {
|
|
19
|
+
return cfg.trustRuntime === true
|
|
20
|
+
|| Boolean(flags['trust-runtime'])
|
|
21
|
+
|| env.REMCP_TRUST_RUNTIME === '1'
|
|
22
|
+
// A configuration written before the field existed paired with the official server, which is
|
|
23
|
+
// trusted by definition; refusing it would silently stop every existing device updating.
|
|
24
|
+
|| new URL(cfg.serverUrl).origin === officialOrigin;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exactClientSpec(value) {
|
|
28
|
+
const spec = String(value || '').trim();
|
|
29
|
+
return isRuntimeSpecFor(PACKAGE_NAME, spec) ? spec : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function specVersion(packageName, spec) {
|
|
33
|
+
const value = String(spec || '');
|
|
34
|
+
return isRuntimeSpecFor(packageName, value) ? value.slice(`${packageName}@`.length) : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function releasePairMatches(clientSpec, runtimePackageName, runtimeSpec) {
|
|
38
|
+
if (!isRuntimeSpecFor(PACKAGE_NAME, clientSpec) || !isRuntimeSpecFor(runtimePackageName, runtimeSpec)) return false;
|
|
39
|
+
// First-party client/runtime releases are one contract and intentionally share a version. Custom
|
|
40
|
+
// runtimes can version independently because their package is controlled by the operator.
|
|
41
|
+
if (runtimePackageName !== '@remcp/runtime') return true;
|
|
42
|
+
return specVersion(PACKAGE_NAME, clientSpec) === specVersion(runtimePackageName, runtimeSpec);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function resolveUpdateTargets({
|
|
46
|
+
cfg,
|
|
47
|
+
flags = {},
|
|
48
|
+
env = process.env,
|
|
49
|
+
fetchImpl = globalThis.fetch,
|
|
50
|
+
}) {
|
|
51
|
+
const requestedRuntime = typeof flags.runtime === 'string' ? flags.runtime.trim() : '';
|
|
52
|
+
const requestedClient = typeof flags.client === 'string' ? flags.client.trim() : '';
|
|
53
|
+
const latestClient = `${PACKAGE_NAME}@latest`;
|
|
54
|
+
const currentClient = `${PACKAGE_NAME}@${VERSION}`;
|
|
55
|
+
const trusted = runtimeTrustAllowed(cfg, flags, env);
|
|
56
|
+
|
|
57
|
+
let clientSpec = latestClient;
|
|
58
|
+
if (requestedClient) {
|
|
59
|
+
clientSpec = exactClientSpec(requestedClient);
|
|
60
|
+
if (!clientSpec) throw new Error(`--client must be ${PACKAGE_NAME}@<version>`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (requestedRuntime) {
|
|
26
64
|
// Only `<configured package>@<semver>` is installable: an alias, a git/URL/file spec, a tag or
|
|
27
65
|
// a range would run code the user never agreed to.
|
|
28
|
-
if (!isRuntimeSpecFor(cfg.runtime.packageName,
|
|
66
|
+
if (!isRuntimeSpecFor(cfg.runtime.packageName, requestedRuntime)) {
|
|
29
67
|
throw new Error(`--runtime must be ${cfg.runtime.packageName}@<version>`);
|
|
30
68
|
}
|
|
31
|
-
const trusted = cfg.trustRuntime === true
|
|
32
|
-
|| Boolean(flags['trust-runtime'])
|
|
33
|
-
|| process.env.REMCP_TRUST_RUNTIME === '1'
|
|
34
|
-
// A configuration written before the field existed paired with the official server, which is
|
|
35
|
-
// trusted by definition; refusing it would silently stop every existing device updating.
|
|
36
|
-
|| new URL(cfg.serverUrl).origin === officialOrigin;
|
|
37
69
|
if (!trusted) {
|
|
38
70
|
throw new Error(`This machine was paired without trusting ${cfg.serverUrl} to choose a runtime version. Re-run with --trust-runtime if you trust that server.`);
|
|
39
71
|
}
|
|
40
|
-
runtimeSpec = normalizeRuntime({
|
|
72
|
+
const runtimeSpec = normalizeRuntime({
|
|
73
|
+
kind: 'npm',
|
|
74
|
+
packageName: cfg.runtime.packageName,
|
|
75
|
+
packageSpec: requestedRuntime,
|
|
76
|
+
entry: cfg.runtime.entry,
|
|
77
|
+
}).packageSpec;
|
|
78
|
+
if (requestedClient && !releasePairMatches(clientSpec, cfg.runtime.packageName, runtimeSpec)) {
|
|
79
|
+
throw new Error('The client and first-party runtime must use the same exact release version.');
|
|
80
|
+
}
|
|
81
|
+
if (!requestedClient && cfg.runtime.packageName === '@remcp/runtime' && specVersion(cfg.runtime.packageName, runtimeSpec) !== VERSION) {
|
|
82
|
+
throw new Error(`--runtime without --client must match the running client version ${VERSION}`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
clientSpec: requestedClient ? clientSpec : currentClient,
|
|
86
|
+
runtimeSpec,
|
|
87
|
+
persistRuntime: runtimeSpec !== cfg.runtime.packageSpec,
|
|
88
|
+
installable: true,
|
|
89
|
+
source: requestedClient ? 'explicit-pair' : 'explicit-runtime',
|
|
90
|
+
warning: '',
|
|
91
|
+
};
|
|
41
92
|
}
|
|
93
|
+
|
|
94
|
+
// A manual `remcp update` has to move the client and the first-party runtime in lockstep too.
|
|
95
|
+
// Older versions reused the runtime pin stored at pairing time, so a manual client update could
|
|
96
|
+
// install a newer client while deliberately reinstalling an older runtime. Resolve the exact
|
|
97
|
+
// release pair from the same public endpoint the running agent already trusts. During a public-
|
|
98
|
+
// first rollout this also prevents `@latest` from getting ahead of the version production
|
|
99
|
+
// actually advertises.
|
|
100
|
+
if (!trusted) {
|
|
101
|
+
return {
|
|
102
|
+
clientSpec,
|
|
103
|
+
runtimeSpec: cfg.runtime.packageSpec,
|
|
104
|
+
persistRuntime: false,
|
|
105
|
+
installable: true,
|
|
106
|
+
source: 'configured',
|
|
107
|
+
warning: '',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const versionUrl = new URL('/api/agent/version', cfg.serverUrl).toString();
|
|
113
|
+
const response = await fetchImpl(versionUrl, {
|
|
114
|
+
headers: { accept: 'application/json' },
|
|
115
|
+
signal: AbortSignal.timeout(UPDATE_DISCOVERY_TIMEOUT_MS),
|
|
116
|
+
});
|
|
117
|
+
if (!response?.ok) {
|
|
118
|
+
return {
|
|
119
|
+
clientSpec: currentClient,
|
|
120
|
+
runtimeSpec: cfg.runtime.packageSpec,
|
|
121
|
+
persistRuntime: false,
|
|
122
|
+
installable: false,
|
|
123
|
+
source: 'configured',
|
|
124
|
+
warning: `Could not resolve the server release pair (HTTP ${response?.status ?? 'unknown'}); refusing a partial update.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const advertised = await response.json();
|
|
129
|
+
const runtimeCandidate = String(advertised?.runtime || '').trim();
|
|
130
|
+
const advertisedClient = exactClientSpec(advertised?.cli);
|
|
131
|
+
const clientCandidate = requestedClient || advertisedClient;
|
|
132
|
+
if (!advertisedClient
|
|
133
|
+
|| !releasePairMatches(advertisedClient, cfg.runtime.packageName, runtimeCandidate)
|
|
134
|
+
|| (requestedClient && requestedClient !== advertisedClient)) {
|
|
135
|
+
return {
|
|
136
|
+
clientSpec: currentClient,
|
|
137
|
+
runtimeSpec: cfg.runtime.packageSpec,
|
|
138
|
+
persistRuntime: false,
|
|
139
|
+
installable: false,
|
|
140
|
+
source: 'configured',
|
|
141
|
+
warning: 'Could not resolve a valid server release pair; refusing a partial update.',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const runtimeSpec = normalizeRuntime({
|
|
146
|
+
kind: 'npm',
|
|
147
|
+
packageName: cfg.runtime.packageName,
|
|
148
|
+
packageSpec: runtimeCandidate,
|
|
149
|
+
entry: cfg.runtime.entry,
|
|
150
|
+
}).packageSpec;
|
|
151
|
+
return {
|
|
152
|
+
clientSpec: clientCandidate,
|
|
153
|
+
runtimeSpec,
|
|
154
|
+
persistRuntime: runtimeSpec !== cfg.runtime.packageSpec,
|
|
155
|
+
installable: true,
|
|
156
|
+
source: 'server',
|
|
157
|
+
warning: '',
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
clientSpec: currentClient,
|
|
162
|
+
runtimeSpec: cfg.runtime.packageSpec,
|
|
163
|
+
persistRuntime: false,
|
|
164
|
+
installable: false,
|
|
165
|
+
source: 'configured',
|
|
166
|
+
warning: `Could not resolve the server release pair (${error instanceof Error ? error.message : String(error)}); refusing a partial update.`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function updateCommand(flags) {
|
|
172
|
+
const cfg = loadConfig();
|
|
173
|
+
const targets = await resolveUpdateTargets({ cfg, flags });
|
|
174
|
+
if (targets.warning) console.error(`ReMCP update: ${targets.warning}`);
|
|
175
|
+
|
|
42
176
|
if (flags.check) {
|
|
43
177
|
console.log(JSON.stringify({
|
|
44
178
|
current: VERSION,
|
|
45
179
|
installedRuntime: installedVersion(cfg.runtime.packageName),
|
|
46
|
-
|
|
47
|
-
|
|
180
|
+
clientSpec: targets.clientSpec,
|
|
181
|
+
runtimeSpec: targets.runtimeSpec,
|
|
182
|
+
updateSource: targets.source,
|
|
183
|
+
installable: targets.installable,
|
|
48
184
|
managedService: fs.existsSync(linuxServiceFile) || fs.existsSync(macServiceFile),
|
|
49
185
|
supervisor: supervisorRestart() ?? 'none',
|
|
50
186
|
}, null, 2));
|
|
51
187
|
return;
|
|
52
188
|
}
|
|
189
|
+
|
|
190
|
+
if (!targets.installable) {
|
|
191
|
+
throw new Error(targets.warning || 'Could not resolve a complete client/runtime release pair.');
|
|
192
|
+
}
|
|
193
|
+
|
|
53
194
|
const before = { cli: VERSION, runtime: installedVersion(cfg.runtime.packageName) };
|
|
54
|
-
console.log(`Updating ReMCP to
|
|
55
|
-
npmGlobalInstall(
|
|
195
|
+
console.log(`Updating ReMCP to ${targets.clientSpec} with ${targets.runtimeSpec}…`);
|
|
196
|
+
npmGlobalInstall(targets.clientSpec, targets.runtimeSpec);
|
|
56
197
|
// The npm prefix can change across Node-manager upgrades. Repair an existing persistent-service
|
|
57
198
|
// launcher only after the install, when globalCliPath() points at the CLI we just installed.
|
|
58
199
|
ensureServiceIfRecorded(cfg);
|
|
59
200
|
// Only a validated spec is persisted, so a failed update cannot leave the install unable to start.
|
|
60
|
-
if (
|
|
201
|
+
if (targets.persistRuntime) saveConfig({ ...cfg, runtime: { ...cfg.runtime, packageSpec: targets.runtimeSpec } });
|
|
61
202
|
const after = { cli: installedVersion(PACKAGE_NAME), runtime: installedVersion(cfg.runtime.packageName) };
|
|
62
203
|
const restarted = restartPersistentServiceIfInstalled();
|
|
63
204
|
if (restarted) {
|