skyloom 1.14.4 → 1.14.6
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/dist/cli/loom.d.ts +2 -0
- package/dist/cli/loom.d.ts.map +1 -1
- package/dist/cli/loom.js +76 -7
- package/dist/cli/loom.js.map +1 -1
- package/dist/cli/main.js +12 -3
- package/dist/cli/main.js.map +1 -1
- package/dist/core/agent/guard.d.ts.map +1 -1
- package/dist/core/agent/guard.js +1 -2
- package/dist/core/agent/guard.js.map +1 -1
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +12 -9
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +3 -3
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +7 -3
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +12 -2
- package/dist/core/memory.js.map +1 -1
- package/dist/core/model_config.d.ts.map +1 -1
- package/dist/core/model_config.js +7 -2
- package/dist/core/model_config.js.map +1 -1
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +27 -0
- package/dist/plugins/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts +6 -0
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +160 -17
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/computer.d.ts.map +1 -1
- package/dist/tools/computer.js +18 -7
- package/dist/tools/computer.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +35 -2
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/loom.ts +66 -7
- package/src/cli/main.ts +6 -3
- package/src/core/agent/guard.ts +1 -2
- package/src/core/agent.ts +12 -8
- package/src/core/agent_helpers.ts +7 -3
- package/src/core/config.ts +5 -2
- package/src/core/memory.ts +9 -2
- package/src/core/model_config.ts +4 -2
- package/src/plugins/loader.ts +91 -66
- package/src/tools/builtin.ts +119 -16
- package/src/tools/computer.ts +279 -269
- package/src/web/server.ts +35 -2
- package/tests/fence_plugin.test.ts +52 -0
- package/tests/loom.test.ts +89 -0
- package/tests/ssrf.test.ts +38 -0
- package/tsconfig.json +1 -0
package/tests/loom.test.ts
CHANGED
|
@@ -246,3 +246,92 @@ describe("palette ↑↓ navigation + Enter execution", () => {
|
|
|
246
246
|
expect(ui.inputGlyphs.length).toBe(0);
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
describe("mouse wheel scrolling", () => {
|
|
251
|
+
// Replay an SGR mouse sequence the way Node's keypress parser fragments it:
|
|
252
|
+
// ESC[< as one event, then every remaining char separately.
|
|
253
|
+
function wheel(ui: any, code: number) {
|
|
254
|
+
ui.onKey("", { sequence: "\x1b[<" });
|
|
255
|
+
for (const ch of `${code};10;5M`) ui.onKey(ch, { name: ch });
|
|
256
|
+
}
|
|
257
|
+
// Fill the viewport past one screen so there is something to scroll through.
|
|
258
|
+
function fillUI() {
|
|
259
|
+
const out = { columns: 80, rows: 24, isTTY: false, write: (_: string) => true };
|
|
260
|
+
const ui = new LoomUI({ out, inp: null, headless: true }) as any;
|
|
261
|
+
ui.start();
|
|
262
|
+
for (let i = 0; i < 60; i++) ui.line(`line ${i}`);
|
|
263
|
+
return ui;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
it("wheel up scrolls into history, wheel down returns toward the tail", () => {
|
|
267
|
+
const ui = fillUI();
|
|
268
|
+
expect(ui.scrollOff).toBe(0);
|
|
269
|
+
wheel(ui, 64); // wheel up
|
|
270
|
+
expect(ui.scrollOff).toBeGreaterThan(0);
|
|
271
|
+
const up = ui.scrollOff;
|
|
272
|
+
wheel(ui, 65); // wheel down
|
|
273
|
+
expect(ui.scrollOff).toBeLessThan(up);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("mouse fragments never leak into the input line", () => {
|
|
277
|
+
const ui = fillUI();
|
|
278
|
+
wheel(ui, 64);
|
|
279
|
+
wheel(ui, 65);
|
|
280
|
+
// A click (button 0) must also be swallowed whole, not typed.
|
|
281
|
+
ui.onKey("", { sequence: "\x1b[<" });
|
|
282
|
+
for (const ch of "0;3;4M") ui.onKey(ch, { name: ch });
|
|
283
|
+
expect(ui.inputGlyphs.join("")).toBe("");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not yank a scrolled-up reader to the tail on new content", () => {
|
|
287
|
+
const ui = fillUI();
|
|
288
|
+
wheel(ui, 64);
|
|
289
|
+
wheel(ui, 64);
|
|
290
|
+
const before = ui.scrollOff;
|
|
291
|
+
expect(before).toBeGreaterThan(0);
|
|
292
|
+
ui.line("a fresh tool event arrives");
|
|
293
|
+
ui.blank();
|
|
294
|
+
expect(ui.scrollOff).toBe(before); // position preserved
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("submitting a turn snaps back to the tail", async () => {
|
|
298
|
+
const ui = fillUI();
|
|
299
|
+
wheel(ui, 64);
|
|
300
|
+
expect(ui.scrollOff).toBeGreaterThan(0);
|
|
301
|
+
const p = ui.readInput();
|
|
302
|
+
for (const ch of "hi") ui.onKey(ch, { name: ch });
|
|
303
|
+
ui.onKey("", { name: "return" });
|
|
304
|
+
await p;
|
|
305
|
+
expect(ui.scrollOff).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("wheel navigates the slash palette while it is open", () => {
|
|
309
|
+
const ui = fillUI();
|
|
310
|
+
for (const ch of "/") ui.onKey(ch, { name: ch });
|
|
311
|
+
expect(ui.paletteMatches().length).toBeGreaterThan(1);
|
|
312
|
+
expect(ui.paletteIdx).toBe(0);
|
|
313
|
+
wheel(ui, 65); // wheel down → next command
|
|
314
|
+
expect(ui.paletteIdx).toBe(1);
|
|
315
|
+
wheel(ui, 64); // wheel up → previous command
|
|
316
|
+
expect(ui.paletteIdx).toBe(0);
|
|
317
|
+
expect(ui.scrollOff).toBe(0); // palette nav must not scroll the viewport
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe("input cursor — Home/End", () => {
|
|
322
|
+
function makeInput() {
|
|
323
|
+
const out = { columns: 80, rows: 24, isTTY: false, write: (_: string) => true };
|
|
324
|
+
const ui = new LoomUI({ out, inp: null, headless: true }) as any;
|
|
325
|
+
ui.start();
|
|
326
|
+
for (const ch of "hello") ui.onKey(ch, { name: ch });
|
|
327
|
+
return ui;
|
|
328
|
+
}
|
|
329
|
+
it("Home jumps to the start, End to the end", () => {
|
|
330
|
+
const ui = makeInput();
|
|
331
|
+
expect(ui.cursor).toBe(5);
|
|
332
|
+
ui.onKey("", { name: "home" });
|
|
333
|
+
expect(ui.cursor).toBe(0);
|
|
334
|
+
ui.onKey("", { name: "end" });
|
|
335
|
+
expect(ui.cursor).toBe(5);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { isPrivateIp, assertFetchAllowed } from "../src/tools/builtin";
|
|
3
|
+
|
|
4
|
+
describe("SSRF guard — isPrivateIp", () => {
|
|
5
|
+
it("flags loopback, private, link-local and metadata addresses", () => {
|
|
6
|
+
for (const ip of ["127.0.0.1", "10.0.0.5", "172.16.3.4", "172.31.255.255",
|
|
7
|
+
"192.168.1.1", "169.254.169.254", "100.64.0.1", "0.0.0.0",
|
|
8
|
+
"::1", "::", "fc00::1", "fd12::3", "fe80::1", "::ffff:127.0.0.1"]) {
|
|
9
|
+
expect(isPrivateIp(ip), ip).toBe(true);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
it("allows public addresses", () => {
|
|
13
|
+
for (const ip of ["8.8.8.8", "1.1.1.1", "172.32.0.1", "192.169.0.1", "2606:4700::1"]) {
|
|
14
|
+
expect(isPrivateIp(ip), ip).toBe(false);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("SSRF guard — assertFetchAllowed", () => {
|
|
20
|
+
afterEach(() => { delete process.env.SKYLOOM_ALLOW_PRIVATE_FETCH; });
|
|
21
|
+
|
|
22
|
+
it("rejects non-http(s) schemes", async () => {
|
|
23
|
+
await expect(assertFetchAllowed("file:///etc/passwd")).rejects.toThrow(/scheme/);
|
|
24
|
+
await expect(assertFetchAllowed("gopher://x")).rejects.toThrow(/scheme/);
|
|
25
|
+
});
|
|
26
|
+
it("rejects IP-literal private targets", async () => {
|
|
27
|
+
await expect(assertFetchAllowed("http://169.254.169.254/latest/meta-data/")).rejects.toThrow(/private/);
|
|
28
|
+
await expect(assertFetchAllowed("http://127.0.0.1:6379/")).rejects.toThrow(/private/);
|
|
29
|
+
await expect(assertFetchAllowed("http://[::1]/")).rejects.toThrow(/private/);
|
|
30
|
+
});
|
|
31
|
+
it("honors the opt-out env var", async () => {
|
|
32
|
+
process.env.SKYLOOM_ALLOW_PRIVATE_FETCH = "1";
|
|
33
|
+
await expect(assertFetchAllowed("http://127.0.0.1/")).resolves.toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it("rejects an invalid URL", async () => {
|
|
36
|
+
await expect(assertFetchAllowed("not a url")).rejects.toThrow(/invalid URL/);
|
|
37
|
+
});
|
|
38
|
+
});
|