@remcp/runtime 0.2.139 → 0.2.140
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 +8 -0
- package/package.json +1 -1
- package/src/catalog.mjs +10 -4
- package/src/tools/files.mjs +66 -23
- package/src/util.mjs +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.140 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
- Windows `create_archive` / `extract_archive` are enabled when the native inbox `tar.exe` (libarchive/bsdtar) backend is available.
|
|
6
|
+
- Windows ZIP creation/extraction uses the same validated tar backend as tar-family archives, preserving traversal, link/special-file, expanded-size, staging, post-extraction, and atomic-commit safety checks.
|
|
7
|
+
- Archive member names are normalized to portable forward slashes before backend invocation.
|
|
8
|
+
- `read_binary` now sizes each returned base64 chunk against the live output cap so the JSON envelope remains parseable under reduced per-call limits; `nextOffsetBytes` reflects the actual returned prefix.
|
|
9
|
+
- Public Windows release CI exercises tar.gz, ZIP, and option-like filename round trips. Fully capable Windows now advertises all 83 runtime tools.
|
|
10
|
+
|
|
3
11
|
## 0.2.139 — 2026-10-02
|
|
4
12
|
|
|
5
13
|
- `create_archive` and `extract_archive` are now advertised on macOS in addition to Linux.
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fileToolHandlers } from './tools/files.mjs';
|
|
1
|
+
import { archiveBackendAvailable, fileToolHandlers } from './tools/files.mjs';
|
|
2
2
|
import { searchToolHandlers } from './tools/search.mjs';
|
|
3
3
|
import { terminalToolHandlers } from './tools/terminal.mjs';
|
|
4
4
|
import { systemToolHandlers } from './tools/system.mjs';
|
|
@@ -830,12 +830,18 @@ export const toolDefinitions = [
|
|
|
830
830
|
},
|
|
831
831
|
];
|
|
832
832
|
|
|
833
|
-
export function coreToolSupported(definition, {
|
|
834
|
-
|
|
833
|
+
export function coreToolSupported(definition, {
|
|
834
|
+
platform = process.platform,
|
|
835
|
+
archiveAvailable = platform === 'win32' ? archiveBackendAvailable({ platform }) : true,
|
|
836
|
+
} = {}) {
|
|
837
|
+
if (!['create_archive', 'extract_archive'].includes(definition.name)) return true;
|
|
838
|
+
if (platform === 'linux' || platform === 'darwin') return true;
|
|
839
|
+
return platform === 'win32' && archiveAvailable === true;
|
|
835
840
|
}
|
|
836
841
|
|
|
837
842
|
export function supportedCoreTools() {
|
|
838
|
-
|
|
843
|
+
const archiveAvailable = process.platform === 'win32' ? archiveBackendAvailable({ platform:process.platform }) : true;
|
|
844
|
+
return toolDefinitions.filter(definition => coreToolSupported(definition, { platform:process.platform, archiveAvailable }));
|
|
839
845
|
}
|
|
840
846
|
|
|
841
847
|
export function advertisedTools() {
|
package/src/tools/files.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { describeFilesystemFailure } from '../permissions.mjs';
|
|
|
12
12
|
import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
|
|
13
13
|
import { applyHunks, parseUnifiedDiff } from '../patch.mjs';
|
|
14
14
|
import { countEvent, recordEvent } from '../telemetry.mjs';
|
|
15
|
-
import { clampInteger, decodeText, displayPath, fail, globToRegExp, image, isInsideRoot, looksBinary, multi, pageLines, resolveSafePath, splitLines, text, throwIfCancelled } from '../util.mjs';
|
|
15
|
+
import { clampInteger, decodeText, displayPath, fail, fitsOutput, globToRegExp, image, isInsideRoot, looksBinary, multi, pageLines, resolveSafePath, splitLines, text, throwIfCancelled } from '../util.mjs';
|
|
16
16
|
|
|
17
17
|
const MAX_INLINE_FILE_BYTES = 20 * 1024 * 1024;
|
|
18
18
|
// An image travels base64-encoded, which costs a third more bytes. The agent's stdio transport holds
|
|
@@ -785,16 +785,33 @@ export async function readBinaryTool(args) {
|
|
|
785
785
|
try {
|
|
786
786
|
const buffer = Buffer.alloc(end - start);
|
|
787
787
|
if (buffer.length) await handle.read(buffer, 0, buffer.length, start);
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
788
|
+
const renderPayload = lengthBytes => {
|
|
789
|
+
const actualEnd = start + lengthBytes;
|
|
790
|
+
return JSON.stringify({
|
|
791
|
+
path: displayPath(absolute),
|
|
792
|
+
size: info.size,
|
|
793
|
+
offsetBytes: start,
|
|
794
|
+
lengthBytes,
|
|
795
|
+
nextOffsetBytes: actualEnd < info.size ? actualEnd : null,
|
|
796
|
+
complete: actualEnd >= info.size,
|
|
797
|
+
encoding: 'base64',
|
|
798
|
+
data: buffer.subarray(0, lengthBytes).toString('base64'),
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
let returnedLength = buffer.length;
|
|
802
|
+
let payload = renderPayload(returnedLength);
|
|
803
|
+
if (!fitsOutput(payload)) {
|
|
804
|
+
let low = 0;
|
|
805
|
+
let high = returnedLength;
|
|
806
|
+
while (low < high) {
|
|
807
|
+
const candidate = Math.ceil((low + high) / 2);
|
|
808
|
+
if (fitsOutput(renderPayload(candidate))) low = candidate;
|
|
809
|
+
else high = candidate - 1;
|
|
810
|
+
}
|
|
811
|
+
returnedLength = low;
|
|
812
|
+
payload = renderPayload(returnedLength);
|
|
813
|
+
if (!fitsOutput(payload)) fail('Runtime output limit is too small to return read_binary metadata');
|
|
814
|
+
}
|
|
798
815
|
return text(payload);
|
|
799
816
|
} finally {
|
|
800
817
|
await opened.close();
|
|
@@ -1775,14 +1792,31 @@ async function moveArchiveFile(source, destination) {
|
|
|
1775
1792
|
}
|
|
1776
1793
|
}
|
|
1777
1794
|
|
|
1778
|
-
function
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1795
|
+
function archiveCommandProbe(name, versionArgs = ['--version']) {
|
|
1796
|
+
const result = spawnSync(name, versionArgs, { encoding: 'utf8' });
|
|
1797
|
+
return !result.error && result.status === 0 ? name : null;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
function archiveTool({ platform = process.platform, probe = archiveCommandProbe } = {}) {
|
|
1801
|
+
const tarName = platform === 'win32' ? 'tar.exe' : 'tar';
|
|
1802
|
+
// Windows ships libarchive/bsdtar as tar.exe. It can create/read ZIP as well as tar-family
|
|
1803
|
+
// archives, so keeping one backend avoids introducing a second extraction/safety path.
|
|
1804
|
+
if (platform === 'win32') return { tar: probe(tarName), zip:null, unzip:null };
|
|
1783
1805
|
// Info-ZIP unzip (the default on Debian/Ubuntu) treats --version as an invalid combination and
|
|
1784
1806
|
// exits 10 even though the binary is healthy. Its portable version probe is -v.
|
|
1785
|
-
return { tar: probe(
|
|
1807
|
+
return { tar: probe(tarName), zip: probe('zip'), unzip: probe('unzip', ['-v']) };
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
export function archiveBackendAvailable({ platform = process.platform, probe = archiveCommandProbe } = {}) {
|
|
1811
|
+
if (platform === 'win32') return Boolean(archiveTool({ platform, probe }).tar);
|
|
1812
|
+
return platform === 'linux' || platform === 'darwin';
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
export function zipArchiveBackendAvailable({ platform = process.platform, probe = archiveCommandProbe } = {}) {
|
|
1816
|
+
const tools = archiveTool({ platform, probe });
|
|
1817
|
+
if (platform === 'win32') return Boolean(tools.tar);
|
|
1818
|
+
if (platform === 'linux' || platform === 'darwin') return Boolean(tools.zip && tools.unzip);
|
|
1819
|
+
return false;
|
|
1786
1820
|
}
|
|
1787
1821
|
|
|
1788
1822
|
export async function createArchiveTool(args, extra = {}) {
|
|
@@ -1801,7 +1835,10 @@ export async function createArchiveTool(args, extra = {}) {
|
|
|
1801
1835
|
const baseDir = path.dirname(resolved[0]);
|
|
1802
1836
|
const names = resolved.map(entry => path.relative(baseDir, entry));
|
|
1803
1837
|
if (names.some(name => !name || name.startsWith('..') || path.isAbsolute(name))) fail('Archive sources must share one parent directory');
|
|
1804
|
-
const archiveNames = names.map(name =>
|
|
1838
|
+
const archiveNames = names.map(name => {
|
|
1839
|
+
const portable = name.split(path.sep).join('/');
|
|
1840
|
+
return portable.startsWith('-') ? `./${portable}` : portable;
|
|
1841
|
+
});
|
|
1805
1842
|
const suffix = format === 'zip' ? '.zip' : format === 'tar' ? '.tar' : '.tar.gz';
|
|
1806
1843
|
const stagingDirectory = await mkdtemp(path.join(os.tmpdir(), 'remcp-archive-'));
|
|
1807
1844
|
if (resolved.some(entry => !path.relative(entry, stagingDirectory).startsWith('..') && !path.isAbsolute(path.relative(entry, stagingDirectory)))) {
|
|
@@ -1822,9 +1859,15 @@ export async function createArchiveTool(args, extra = {}) {
|
|
|
1822
1859
|
await copyTreeContents(resolved[index], snapshotPath, snapshotState);
|
|
1823
1860
|
}
|
|
1824
1861
|
if (format === 'zip') {
|
|
1825
|
-
if (
|
|
1826
|
-
|
|
1827
|
-
|
|
1862
|
+
if (process.platform === 'win32') {
|
|
1863
|
+
if (!tools.tar) fail('Windows archive backend is unavailable: tar.exe is required');
|
|
1864
|
+
const result = spawnSync(tools.tar, ['-a', '-c', '-f', staging, '--', ...archiveNames], { cwd: snapshotRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
|
|
1865
|
+
if (result.status !== 0) fail(`tar.exe ZIP creation failed: ${(result.stderr || result.stdout || '').trim() || `exit ${result.status}`}`);
|
|
1866
|
+
} else {
|
|
1867
|
+
if (!tools.zip) fail('zip is not installed on this device; use format "tar.gz"');
|
|
1868
|
+
const result = spawnSync(tools.zip, ['-r', '-q', '-y', staging, '--', ...archiveNames], { cwd: snapshotRoot, encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
|
|
1869
|
+
if (result.status !== 0) fail(`zip failed: ${(result.stderr || result.stdout || '').trim() || `exit ${result.status}`}`);
|
|
1870
|
+
}
|
|
1828
1871
|
} else if (format === 'tar' || format === 'tar.gz' || format === 'tgz') {
|
|
1829
1872
|
if (!tools.tar) fail('tar is not installed on this device');
|
|
1830
1873
|
const flags = format === 'tar' ? '-cf' : '-czf';
|
|
@@ -1880,7 +1923,7 @@ export async function extractArchiveTool(args) {
|
|
|
1880
1923
|
await recoverExtractionArtifacts(parent, destination, backupPrefix);
|
|
1881
1924
|
const existingDestination = await lstat(destination).catch(() => null);
|
|
1882
1925
|
if (existingDestination?.isSymbolicLink() || (existingDestination && !existingDestination.isDirectory())) fail('Archive destination is not a safe directory');
|
|
1883
|
-
if (format === 'zip') {
|
|
1926
|
+
if (format === 'zip' && process.platform !== 'win32') {
|
|
1884
1927
|
if (!tools.unzip) fail('unzip is not installed on this device');
|
|
1885
1928
|
const listing = spawnSync(tools.unzip, ['-Z', '-1', archiveSnapshot], { encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
|
|
1886
1929
|
if (listing.error || listing.status !== 0) fail(`Could not inspect ZIP archive: ${(listing.stderr || listing.error?.message || `exit ${listing.status}`).trim()}`);
|
|
@@ -1895,7 +1938,7 @@ export async function extractArchiveTool(args) {
|
|
|
1895
1938
|
const result = spawnSync(tools.unzip, ['-o', '-q', archiveSnapshot, '-d', staging], { encoding: 'utf8', timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
|
|
1896
1939
|
if (result.status !== 0) fail(`unzip failed: ${(result.stderr || result.stdout || '').trim() || `exit ${result.status}`}`);
|
|
1897
1940
|
} else {
|
|
1898
|
-
if (!tools.tar) fail('tar is not installed on this device');
|
|
1941
|
+
if (!tools.tar) fail(process.platform === 'win32' ? 'Windows archive backend is unavailable: tar.exe is required' : 'tar is not installed on this device');
|
|
1899
1942
|
const listFlags = format === 'tar.gz' ? '-tzf' : format === 'tar.bz2' ? '-tjf' : format === 'tar.xz' ? '-tJf' : '-tf';
|
|
1900
1943
|
const listing = spawnSync(tools.tar, [listFlags, archiveSnapshot], { encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
|
|
1901
1944
|
if (listing.error || listing.status !== 0) fail(`Could not inspect archive: ${(listing.stderr || listing.error?.message || `exit ${listing.status}`).trim()}`);
|
package/src/util.mjs
CHANGED
|
@@ -136,10 +136,16 @@ function frameBytes(text) {
|
|
|
136
136
|
return Buffer.byteLength(JSON.stringify(String(text)), 'utf8');
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
export function fitsOutput(text, maxBytes) {
|
|
140
|
+
const limit = maxBytes || liveConfig('maxOutputBytes');
|
|
141
|
+
const value = String(text);
|
|
142
|
+
return Buffer.byteLength(value, 'utf8') <= limit && frameBytes(value) <= limit;
|
|
143
|
+
}
|
|
144
|
+
|
|
139
145
|
export function truncate(text, maxBytes) {
|
|
140
146
|
const limit = maxBytes || liveConfig('maxOutputBytes');
|
|
141
147
|
const value = String(text);
|
|
142
|
-
if (
|
|
148
|
+
if (fitsOutput(value, limit)) return value;
|
|
143
149
|
// Shrink until the escaped frame fits, so the escaped size is what the caller gets is bounded.
|
|
144
150
|
let size = Math.min(Buffer.byteLength(value, 'utf8'), limit);
|
|
145
151
|
let rendered = '';
|