@agent-sh/agent-workspace-linux 0.3.3 → 0.4.0
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 +6 -0
- package/package.json +8 -3
- package/scripts/postinstall.js +123 -56
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@ Release downloads accept absolute or relative HTTPS redirect locations, with
|
|
|
23
23
|
a maximum of five redirect hops. Malformed URLs and non-HTTPS redirect targets
|
|
24
24
|
fail the download; redirects do not bypass the required checksum verification.
|
|
25
25
|
|
|
26
|
+
Each binary or checksum download has a 60-second inactivity timeout while waiting
|
|
27
|
+
for response headers or body progress. The timer resets whenever bytes arrive, so
|
|
28
|
+
a slow transfer that is still making progress can continue. A separate 30-minute
|
|
29
|
+
hard ceiling only bounds pathological transfers. Timeout failures close the active
|
|
30
|
+
output and remove its partial file before a retry can begin.
|
|
31
|
+
|
|
26
32
|
Checksum sidecars must contain a valid 64-hex-digit SHA-256 digest and name the
|
|
27
33
|
expected release asset. Space and tab separators are accepted. A bare digest or
|
|
28
34
|
an entry naming a different asset is rejected.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-sh/agent-workspace-linux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Isolated Linux desktop workspaces for AI agents — MCP server binary wrapper.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,8 +23,13 @@
|
|
|
23
23
|
"workspace",
|
|
24
24
|
"agent"
|
|
25
25
|
],
|
|
26
|
-
"os": [
|
|
27
|
-
|
|
26
|
+
"os": [
|
|
27
|
+
"linux"
|
|
28
|
+
],
|
|
29
|
+
"cpu": [
|
|
30
|
+
"x64",
|
|
31
|
+
"arm64"
|
|
32
|
+
],
|
|
28
33
|
"engines": {
|
|
29
34
|
"node": ">=18"
|
|
30
35
|
},
|
package/scripts/postinstall.js
CHANGED
|
@@ -117,80 +117,147 @@ function sweepStaleStagingDirs() {
|
|
|
117
117
|
|
|
118
118
|
// ── Download helper (follows redirects, max 5 hops) ─────────────────────────
|
|
119
119
|
|
|
120
|
+
// Fail a download after a full minute without useful network progress. This
|
|
121
|
+
// catches missing headers and stalled response bodies without penalizing a slow
|
|
122
|
+
// connection that is still delivering bytes. Keep a separate generous ceiling
|
|
123
|
+
// so a pathological transfer cannot keep npm install alive forever.
|
|
124
|
+
const DOWNLOAD_IDLE_TIMEOUT_MS = 60 * 1000;
|
|
125
|
+
const DOWNLOAD_MAX_DURATION_MS = 30 * 60 * 1000;
|
|
126
|
+
|
|
120
127
|
/**
|
|
121
128
|
* Download `url` to `tmpFile`, following up to `maxRedirects` 3xx responses.
|
|
122
|
-
* Resolves when the file is fully written
|
|
129
|
+
* Resolves when the file is fully written. Rejects on transport/status errors,
|
|
130
|
+
* after DOWNLOAD_IDLE_TIMEOUT_MS without headers/body progress, or when the
|
|
131
|
+
* generous DOWNLOAD_MAX_DURATION_MS hard ceiling is reached.
|
|
123
132
|
*/
|
|
124
133
|
function download(url, tmpFile, maxRedirects = 5) {
|
|
125
134
|
return new Promise((resolve, reject) => {
|
|
135
|
+
let settled = false;
|
|
136
|
+
let out = null;
|
|
137
|
+
let idleTimer = null;
|
|
138
|
+
let maxDurationTimer = null;
|
|
139
|
+
const requests = new Set();
|
|
140
|
+
const responses = new Set();
|
|
141
|
+
|
|
142
|
+
function settle(err) {
|
|
143
|
+
if (settled) return;
|
|
144
|
+
settled = true;
|
|
145
|
+
clearTimeout(idleTimer);
|
|
146
|
+
clearTimeout(maxDurationTimer);
|
|
147
|
+
if (!err) return resolve();
|
|
148
|
+
|
|
149
|
+
for (const req of requests) req.destroy();
|
|
150
|
+
for (const res of responses) res.destroy();
|
|
151
|
+
|
|
152
|
+
const removePartial = () => fs.unlink(tmpFile, () => reject(err));
|
|
153
|
+
if (out && !out.closed) {
|
|
154
|
+
out.once("close", removePartial);
|
|
155
|
+
out.destroy();
|
|
156
|
+
} else {
|
|
157
|
+
removePartial();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function resetIdleTimer() {
|
|
162
|
+
if (settled) return;
|
|
163
|
+
clearTimeout(idleTimer);
|
|
164
|
+
idleTimer = setTimeout(
|
|
165
|
+
() =>
|
|
166
|
+
settle(
|
|
167
|
+
new Error(
|
|
168
|
+
`No download progress for ${DOWNLOAD_IDLE_TIMEOUT_MS} ms downloading ${url}`
|
|
169
|
+
)
|
|
170
|
+
),
|
|
171
|
+
DOWNLOAD_IDLE_TIMEOUT_MS
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
function get(currentUrl, hopsLeft) {
|
|
176
|
+
if (settled) return;
|
|
127
177
|
try {
|
|
128
|
-
https
|
|
129
|
-
|
|
130
|
-
|
|
178
|
+
const req = https.get(
|
|
179
|
+
currentUrl,
|
|
180
|
+
{ headers: { "User-Agent": "node-fetch/postinstall" } },
|
|
181
|
+
(res) => {
|
|
182
|
+
responses.add(res);
|
|
183
|
+
res.once("close", () => responses.delete(res));
|
|
184
|
+
res.on("error", settle);
|
|
185
|
+
if (settled) {
|
|
186
|
+
res.destroy();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Response headers are progress. A successful body must continue to
|
|
191
|
+
// reset this timer whenever bytes arrive.
|
|
192
|
+
resetIdleTimer();
|
|
131
193
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
194
|
+
try {
|
|
195
|
+
const { statusCode, headers } = res;
|
|
196
|
+
|
|
197
|
+
if (statusCode >= 300 && statusCode < 400 && headers.location) {
|
|
135
198
|
res.resume();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
199
|
+
if (hopsLeft === 0) {
|
|
200
|
+
settle(new Error(`Too many redirects downloading ${url}`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Keep #98 intact: resolve relative redirects against the
|
|
205
|
+
// current URL and never permit a downgrade away from HTTPS.
|
|
206
|
+
let nextUrl;
|
|
207
|
+
try {
|
|
208
|
+
nextUrl = new URL(headers.location, currentUrl);
|
|
209
|
+
} catch (_) {
|
|
210
|
+
settle(new Error(`Invalid redirect location downloading ${url}`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (nextUrl.protocol !== "https:") {
|
|
214
|
+
settle(new Error(`Refusing non-HTTPS redirect downloading ${url}`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
get(nextUrl.href, hopsLeft - 1);
|
|
218
|
+
return;
|
|
150
219
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
220
|
+
|
|
221
|
+
if (statusCode !== 200) {
|
|
222
|
+
res.resume();
|
|
223
|
+
settle(
|
|
224
|
+
new Error(
|
|
225
|
+
`Failed to download ${url}: HTTP ${statusCode}. ` +
|
|
226
|
+
"Check that the release exists, includes the binary and .sha256 sidecar, " +
|
|
227
|
+
"and the version in package.json matches."
|
|
228
|
+
)
|
|
154
229
|
);
|
|
230
|
+
return;
|
|
155
231
|
}
|
|
156
|
-
return get(nextUrl.href, hopsLeft - 1);
|
|
157
|
-
}
|
|
158
232
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
);
|
|
233
|
+
out = fs.createWriteStream(tmpFile);
|
|
234
|
+
out.on("error", settle);
|
|
235
|
+
out.on("finish", () => out.close((err) => settle(err || null)));
|
|
236
|
+
res.on("data", resetIdleTimer);
|
|
237
|
+
res.pipe(out);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
settle(err);
|
|
168
240
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
fs.unlink(tmpFile, () => {}); // best-effort cleanup
|
|
175
|
-
reject(err);
|
|
176
|
-
});
|
|
177
|
-
res.on("error", (err) => {
|
|
178
|
-
fs.unlink(tmpFile, () => {});
|
|
179
|
-
reject(err);
|
|
180
|
-
});
|
|
181
|
-
})
|
|
182
|
-
.on("error", (err) => {
|
|
183
|
-
fs.unlink(tmpFile, () => {});
|
|
184
|
-
reject(err);
|
|
185
|
-
});
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
requests.add(req);
|
|
244
|
+
req.once("close", () => requests.delete(req));
|
|
245
|
+
req.on("error", settle);
|
|
186
246
|
} catch (err) {
|
|
187
|
-
|
|
188
|
-
// response callback that would be an uncaught exception, so reject.
|
|
189
|
-
fs.unlink(tmpFile, () => {});
|
|
190
|
-
reject(err);
|
|
247
|
+
settle(err);
|
|
191
248
|
}
|
|
192
249
|
}
|
|
193
250
|
|
|
251
|
+
maxDurationTimer = setTimeout(
|
|
252
|
+
() =>
|
|
253
|
+
settle(
|
|
254
|
+
new Error(
|
|
255
|
+
`Download exceeded ${DOWNLOAD_MAX_DURATION_MS} ms downloading ${url}`
|
|
256
|
+
)
|
|
257
|
+
),
|
|
258
|
+
DOWNLOAD_MAX_DURATION_MS
|
|
259
|
+
);
|
|
260
|
+
resetIdleTimer();
|
|
194
261
|
get(url, maxRedirects);
|
|
195
262
|
});
|
|
196
263
|
}
|