@agent-sh/agent-workspace-linux 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -8,7 +8,9 @@ npm distribution wrapper for the [`agent-workspace-linux`](https://github.com/ag
8
8
  npm install -g @agent-sh/agent-workspace-linux
9
9
  ```
10
10
 
11
- The installer automatically downloads the prebuilt binary for your architecture from the matching [GitHub Release](https://github.com/agent-sh/agent-workspace-linux/releases).
11
+ The installer automatically downloads the prebuilt binary for your architecture
12
+ from the matching [GitHub Release](https://github.com/agent-sh/agent-workspace-linux/releases),
13
+ then verifies the required `<asset>.sha256` sidecar before installing it.
12
14
 
13
15
  **Linux only.** `x64` (x86_64) and `arm64` (aarch64) are supported.
14
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-sh/agent-workspace-linux",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Isolated Linux desktop workspaces for AI agents — MCP server binary wrapper.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -7,16 +7,16 @@
7
7
  // error at runtime. To recover, re-run installation with scripts enabled or
8
8
  // invoke this file directly: `node node_modules/@agent-sh/agent-workspace-linux/scripts/postinstall.js`
9
9
  //
10
- // Checksum verification is not wired in because the release workflow contract
11
- // does not publish .sha256 sidecar files. If that changes, add a step here that
12
- // downloads <asset>.sha256 and verifies it against the tmp file before rename.
10
+ // Release contract: every prebuilt binary asset must have a matching
11
+ // `<asset>.sha256` sidecar, generated by the GitHub release workflow. The
12
+ // installer verifies the sidecar before moving the binary into place.
13
13
 
14
14
  "use strict";
15
15
 
16
+ const crypto = require("crypto");
16
17
  const https = require("https");
17
18
  const fs = require("fs");
18
19
  const path = require("path");
19
- const { execFileSync } = require("child_process");
20
20
 
21
21
  // ── Platform / arch guard ────────────────────────────────────────────────────
22
22
 
@@ -50,10 +50,12 @@ const assetName = `agent-workspace-linux-${rustTarget}`;
50
50
  const downloadUrl =
51
51
  `https://github.com/agent-sh/agent-workspace-linux/releases/download/` +
52
52
  `v${version}/${assetName}`;
53
+ const checksumUrl = `${downloadUrl}.sha256`;
53
54
 
54
55
  const binDir = path.join(__dirname, "..", "bin");
55
56
  const destPath = path.join(binDir, "agent-workspace-linux");
56
57
  const tmpPath = destPath + ".tmp";
58
+ const tmpChecksumPath = tmpPath + ".sha256";
57
59
 
58
60
  // ── Download helper (follows redirects, max 5 hops) ─────────────────────────
59
61
 
@@ -83,7 +85,8 @@ function download(url, tmpFile, maxRedirects = 5) {
83
85
  return reject(
84
86
  new Error(
85
87
  `Failed to download ${url}: HTTP ${statusCode}. ` +
86
- "Check that the release exists and the version in package.json matches."
88
+ "Check that the release exists, includes the binary and .sha256 sidecar, " +
89
+ "and the version in package.json matches."
87
90
  )
88
91
  );
89
92
  }
@@ -110,17 +113,83 @@ function download(url, tmpFile, maxRedirects = 5) {
110
113
  });
111
114
  }
112
115
 
116
+ function removeFileQuietly(file) {
117
+ try {
118
+ fs.unlinkSync(file);
119
+ } catch (_) {}
120
+ }
121
+
122
+ function parseSha256Sidecar(contents, expectedAssetName) {
123
+ const lines = contents
124
+ .split(/\r?\n/)
125
+ .map((line) => line.trim())
126
+ .filter(Boolean);
127
+
128
+ if (lines.length === 0) {
129
+ throw new Error("checksum sidecar is empty");
130
+ }
131
+
132
+ const candidates = [];
133
+ for (const line of lines) {
134
+ const match = line.match(/^([a-fA-F0-9]{64})(?:\s+(?:\*)?(.+))?$/);
135
+ if (!match) {
136
+ throw new Error(`invalid checksum sidecar line: ${line}`);
137
+ }
138
+
139
+ const [, checksum, fileName] = match;
140
+ candidates.push({
141
+ checksum: checksum.toLowerCase(),
142
+ fileName: fileName ? fileName.trim() : null,
143
+ });
144
+ }
145
+
146
+ const namedMatch = candidates.find(
147
+ ({ fileName }) => fileName && path.basename(fileName) === expectedAssetName
148
+ );
149
+ if (namedMatch) {
150
+ return namedMatch.checksum;
151
+ }
152
+
153
+ throw new Error(
154
+ `checksum sidecar does not contain an entry for ${expectedAssetName}`
155
+ );
156
+ }
157
+
158
+ function fileSha256(file) {
159
+ return new Promise((resolve, reject) => {
160
+ const hash = crypto.createHash("sha256");
161
+ const input = fs.createReadStream(file);
162
+
163
+ input.on("error", reject);
164
+ input.on("data", (chunk) => hash.update(chunk));
165
+ input.on("end", () => resolve(hash.digest("hex")));
166
+ });
167
+ }
168
+
169
+ async function verifyChecksum(binaryFile, sidecarFile, expectedAssetName) {
170
+ const expected = parseSha256Sidecar(
171
+ fs.readFileSync(sidecarFile, "utf8"),
172
+ expectedAssetName
173
+ );
174
+ const actual = await fileSha256(binaryFile);
175
+
176
+ if (actual !== expected) {
177
+ throw new Error(
178
+ `checksum mismatch for ${expectedAssetName}: expected ${expected}, got ${actual}`
179
+ );
180
+ }
181
+ }
182
+
113
183
  // ── Main ─────────────────────────────────────────────────────────────────────
114
184
 
115
- (async () => {
185
+ async function main() {
116
186
  // Ensure bin/ directory exists (it should, tracked by git via .gitkeep, but
117
187
  // be defensive for edge-case installs).
118
188
  fs.mkdirSync(binDir, { recursive: true });
119
189
 
120
190
  // Remove stale tmp file from a previous interrupted install.
121
- try {
122
- fs.unlinkSync(tmpPath);
123
- } catch (_) {}
191
+ removeFileQuietly(tmpPath);
192
+ removeFileQuietly(tmpChecksumPath);
124
193
 
125
194
  console.log(
126
195
  `agent-workspace-linux: downloading ${assetName} v${version}…`
@@ -131,13 +200,35 @@ function download(url, tmpFile, maxRedirects = 5) {
131
200
  await download(downloadUrl, tmpPath);
132
201
  } catch (err) {
133
202
  // Clean up in case the file was partially created.
134
- try {
135
- fs.unlinkSync(tmpPath);
136
- } catch (_) {}
137
- console.error(`\nagent-workspace-linux: download failed — ${err.message}`);
203
+ removeFileQuietly(tmpPath);
204
+ // The download error text embeds the request URL (built from
205
+ // package.json's version + the target triple). Log a fixed message rather
206
+ // than interpolating that value, to avoid a log-injection sink; the URL
207
+ // itself is already printed above for debugging.
208
+ console.error(
209
+ "\nagent-workspace-linux: download failed. Verify the release exists " +
210
+ "for your platform and that the version in package.json matches " +
211
+ "(see the URL logged above)."
212
+ );
138
213
  process.exit(1);
139
214
  }
140
215
 
216
+ console.log(`agent-workspace-linux: downloading checksum for ${assetName}`);
217
+ console.log(` URL: ${checksumUrl}`);
218
+
219
+ try {
220
+ await download(checksumUrl, tmpChecksumPath);
221
+ await verifyChecksum(tmpPath, tmpChecksumPath, assetName);
222
+ } catch (err) {
223
+ removeFileQuietly(tmpPath);
224
+ removeFileQuietly(tmpChecksumPath);
225
+ console.error(
226
+ `\nagent-workspace-linux: checksum verification failed — ${err.message}`
227
+ );
228
+ process.exit(1);
229
+ }
230
+ removeFileQuietly(tmpChecksumPath);
231
+
141
232
  // Atomic rename: avoids leaving a half-written binary.
142
233
  try {
143
234
  fs.renameSync(tmpPath, destPath);
@@ -160,4 +251,17 @@ function download(url, tmpFile, maxRedirects = 5) {
160
251
  }
161
252
 
162
253
  console.log(`agent-workspace-linux: binary installed at ${destPath}`);
163
- })();
254
+ }
255
+
256
+ if (require.main === module) {
257
+ main().catch((err) => {
258
+ console.error(`agent-workspace-linux: postinstall failed — ${err.message}`);
259
+ process.exit(1);
260
+ });
261
+ }
262
+
263
+ module.exports = {
264
+ parseSha256Sidecar,
265
+ fileSha256,
266
+ verifyChecksum,
267
+ };