zames_pro 2.66.1 → 2.67.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/CHANGELOG.md +184 -112
- package/dist/attachments.js +0 -17
- package/dist/context.js +0 -3
- package/dist/extraTools.js +10 -11
- package/dist/fsutil.js +38 -1
- package/dist/gitTools.js +83 -26
- package/dist/index.js +18 -18
- package/dist/input.js +134 -30
- package/dist/mcp.js +49 -10
- package/dist/permissions.js +40 -1
- package/dist/run-task.js +6 -0
- package/dist/sandbox.js +49 -0
- package/dist/scheduler.js +8 -1
- package/dist/self-review.js +37 -17
- package/dist/shell.js +74 -32
- package/dist/tools.js +40 -20
- package/dist/transcript.js +15 -2
- package/dist/web.js +73 -33
- package/package.json +1 -1
- package/scripts/coverage-gate.mjs +4 -0
package/dist/web.js
CHANGED
|
@@ -3,8 +3,9 @@ import { lookup } from 'node:dns/promises';
|
|
|
3
3
|
import { isIP } from 'node:net';
|
|
4
4
|
const DEFAULT_TIMEOUT = 20_000;
|
|
5
5
|
const MAX_TEXT = 12_000;
|
|
6
|
-
// Removes scripts, styles, and turns HTML into readable text.
|
|
7
|
-
|
|
6
|
+
// Removes scripts, styles, and turns HTML into readable text. Exported for
|
|
7
|
+
// tests — the entity/whitespace/collapse branches are pure and worth pinning.
|
|
8
|
+
export function htmlToText(html) {
|
|
8
9
|
// Remove blocks we don't need
|
|
9
10
|
let s = html
|
|
10
11
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
@@ -88,7 +89,7 @@ export function isPrivateHostname(host) {
|
|
|
88
89
|
return true;
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
91
|
-
async function assertPublicUrl(rawUrl) {
|
|
92
|
+
export async function assertPublicUrl(rawUrl) {
|
|
92
93
|
const u = new URL(rawUrl);
|
|
93
94
|
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
94
95
|
throw new Error(`unsupported protocol: ${u.protocol}`);
|
|
@@ -164,23 +165,46 @@ async function httpFetchWithRetry(url, opts = {}, retries = 2) {
|
|
|
164
165
|
}
|
|
165
166
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
166
167
|
}
|
|
167
|
-
// fetch with redirects
|
|
168
|
-
|
|
168
|
+
// fetch with redirects, RE-VALIDATING every hop against the SSRF guard.
|
|
169
|
+
// redirect:'follow' would let a public URL bounce to 169.254.169.254 / 127.0.0.1
|
|
170
|
+
// after assertPublicUrl already approved the original — a classic SSRF bypass.
|
|
171
|
+
// So redirects are followed manually and each Location is checked first.
|
|
172
|
+
const MAX_REDIRECTS = 5;
|
|
173
|
+
export async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {}, } = {}) {
|
|
169
174
|
const ctrl = new AbortController();
|
|
170
175
|
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
171
176
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
let current = url;
|
|
178
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
179
|
+
const res = await fetch(current, {
|
|
180
|
+
signal: ctrl.signal,
|
|
181
|
+
redirect: 'manual',
|
|
182
|
+
headers: {
|
|
183
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ds-agent/1.0',
|
|
184
|
+
'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8',
|
|
185
|
+
...headers,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
if (res.status >= 300 && res.status < 400) {
|
|
189
|
+
const loc = res.headers.get('location');
|
|
190
|
+
if (loc) {
|
|
191
|
+
const next = new URL(loc, current).toString();
|
|
192
|
+
// Validate the TARGET before requesting it.
|
|
193
|
+
await assertPublicUrl(next);
|
|
194
|
+
current = next;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const ct = res.headers.get('content-type') || '';
|
|
199
|
+
const body = await res.text();
|
|
200
|
+
return {
|
|
201
|
+
status: res.status,
|
|
202
|
+
url: res.url || current,
|
|
203
|
+
contentType: ct,
|
|
204
|
+
body,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`too many redirects (>${MAX_REDIRECTS})`);
|
|
184
208
|
}
|
|
185
209
|
finally {
|
|
186
210
|
clearTimeout(t);
|
|
@@ -214,6 +238,10 @@ async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
|
|
|
214
238
|
});
|
|
215
239
|
const page = await ctx.newPage();
|
|
216
240
|
try {
|
|
241
|
+
// Validate the pre-redirect URL (page.goto follows redirects internally,
|
|
242
|
+
// unlike fetch we cannot intercept each hop here — but at least the
|
|
243
|
+
// original is checked, matching the previous behavior).
|
|
244
|
+
await assertPublicUrl(url);
|
|
217
245
|
const resp = await page.goto(url, {
|
|
218
246
|
waitUntil: 'domcontentloaded',
|
|
219
247
|
timeout,
|
|
@@ -223,6 +251,13 @@ async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
|
|
|
223
251
|
const html = await page.content();
|
|
224
252
|
const status = resp ? resp.status() : 0;
|
|
225
253
|
const finalUrl = page.url();
|
|
254
|
+
// The page may have redirected somewhere private; check the FINAL url too.
|
|
255
|
+
try {
|
|
256
|
+
await assertPublicUrl(finalUrl);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
throw new Error(`blocked private redirect target: ${finalUrl}`);
|
|
260
|
+
}
|
|
226
261
|
return { status, url: finalUrl, html };
|
|
227
262
|
}
|
|
228
263
|
finally {
|
|
@@ -307,22 +342,7 @@ export function createWebTools() {
|
|
|
307
342
|
if (r.status !== 200) {
|
|
308
343
|
return `DuckDuckGo returned HTTP ${r.status}`;
|
|
309
344
|
}
|
|
310
|
-
|
|
311
|
-
const results = [];
|
|
312
|
-
const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>)?/gi;
|
|
313
|
-
let m;
|
|
314
|
-
while ((m = re.exec(r.body)) && results.length < limit) {
|
|
315
|
-
let href = m[1];
|
|
316
|
-
// DuckDuckGo wraps links in a redirect like /l/?uddg=...
|
|
317
|
-
const uddgMatch = href.match(/[?&]uddg=([^&]+)/);
|
|
318
|
-
if (uddgMatch)
|
|
319
|
-
href = decodeURIComponent(uddgMatch[1]);
|
|
320
|
-
const title = htmlToText(m[2] || '').trim();
|
|
321
|
-
const snippet = htmlToText(m[3] || '').trim();
|
|
322
|
-
if (!title || !href)
|
|
323
|
-
continue;
|
|
324
|
-
results.push({ title, url: href, snippet });
|
|
325
|
-
}
|
|
345
|
+
const results = parseDuckDuckGoResults(r.body, limit);
|
|
326
346
|
if (!results.length) {
|
|
327
347
|
return `No results found. The DuckDuckGo output format may have changed.`;
|
|
328
348
|
}
|
|
@@ -337,6 +357,26 @@ export function createWebTools() {
|
|
|
337
357
|
},
|
|
338
358
|
];
|
|
339
359
|
}
|
|
360
|
+
// Parse the DuckDuckGo HTML results page. Pure; exported for tests. The
|
|
361
|
+
// html.duckduckgo.com format is stable, so simple regexes suffice. DuckDuckGo
|
|
362
|
+
// wraps links in a /l/?uddg=... redirect which must be unwrapped.
|
|
363
|
+
export function parseDuckDuckGoResults(body, limit) {
|
|
364
|
+
const results = [];
|
|
365
|
+
const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>)?/gi;
|
|
366
|
+
let m;
|
|
367
|
+
while ((m = re.exec(body)) && results.length < limit) {
|
|
368
|
+
let href = m[1];
|
|
369
|
+
const uddgMatch = href.match(/[?&]uddg=([^&]+)/);
|
|
370
|
+
if (uddgMatch)
|
|
371
|
+
href = decodeURIComponent(uddgMatch[1]);
|
|
372
|
+
const title = htmlToText(m[2] || '').trim();
|
|
373
|
+
const snippet = htmlToText(m[3] || '').trim();
|
|
374
|
+
if (!title || !href)
|
|
375
|
+
continue;
|
|
376
|
+
results.push({ title, url: href, snippet });
|
|
377
|
+
}
|
|
378
|
+
return results;
|
|
379
|
+
}
|
|
340
380
|
function formatResult(status, url, text, limit) {
|
|
341
381
|
let out = text;
|
|
342
382
|
let truncated = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.67.0",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,10 @@ const FLOORS = {
|
|
|
32
32
|
'permissions.ts': 85,
|
|
33
33
|
'changelog.ts': 85,
|
|
34
34
|
'attach-refs.ts': 85,
|
|
35
|
+
// N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
|
|
36
|
+
// regression there is silent. Floors sit below the current values.
|
|
37
|
+
'web.ts': 40,
|
|
38
|
+
'spinner.ts': 55,
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const run = spawnSync(
|