@crewhaus/tool-navigate 0.1.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/package.json +45 -0
- package/src/index.test.ts +78 -0
- package/src/index.ts +69 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-navigate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Navigate tool — drives driver.goto(url) for the BROW agent (Section 25 BROW)",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/computer-use-driver": "0.0.0",
|
|
16
|
+
"@crewhaus/errors": "0.0.0",
|
|
17
|
+
"@crewhaus/tool-builder": "0.0.0",
|
|
18
|
+
"@crewhaus/tool-catalog": "0.0.0",
|
|
19
|
+
"zod": "^3.23.8"
|
|
20
|
+
},
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "Max Meier",
|
|
24
|
+
"email": "max@studiomax.io",
|
|
25
|
+
"url": "https://studiomax.io"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
30
|
+
"directory": "packages/tool-navigate"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-navigate#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "restricted"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"NOTICE"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Driver } from "@crewhaus/computer-use-driver";
|
|
3
|
+
import { NavigateError, createNavigateTool } from "./index.js";
|
|
4
|
+
|
|
5
|
+
type GotoRecord = { calls: string[] };
|
|
6
|
+
|
|
7
|
+
function stubDriver(record: GotoRecord, opts: { gotoErr?: Error } = {}): Driver {
|
|
8
|
+
return {
|
|
9
|
+
backend: "chromium",
|
|
10
|
+
async connect() {},
|
|
11
|
+
async goto(url: string) {
|
|
12
|
+
record.calls.push(url);
|
|
13
|
+
if (opts.gotoErr) throw opts.gotoErr;
|
|
14
|
+
},
|
|
15
|
+
async screenshot() {
|
|
16
|
+
return new Uint8Array(0);
|
|
17
|
+
},
|
|
18
|
+
async click() {},
|
|
19
|
+
async type() {},
|
|
20
|
+
async key() {},
|
|
21
|
+
async scroll() {},
|
|
22
|
+
async getViewport() {
|
|
23
|
+
return { width: 800, height: 600, devicePixelRatio: 1 };
|
|
24
|
+
},
|
|
25
|
+
async disconnect() {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("createNavigateTool", () => {
|
|
30
|
+
test("happy path: forwards url to driver.goto and returns a text confirmation", async () => {
|
|
31
|
+
const record: GotoRecord = { calls: [] };
|
|
32
|
+
const tool = createNavigateTool({ driver: stubDriver(record) });
|
|
33
|
+
|
|
34
|
+
const result = await tool.execute({ url: "https://example.com/" }, {});
|
|
35
|
+
expect(record.calls).toEqual(["https://example.com/"]);
|
|
36
|
+
expect(Array.isArray(result)).toBe(true);
|
|
37
|
+
if (!Array.isArray(result)) throw new Error("expected ToolResultContent array");
|
|
38
|
+
expect(result).toHaveLength(1);
|
|
39
|
+
const block = result[0];
|
|
40
|
+
if (block?.type !== "text") throw new Error("expected text block");
|
|
41
|
+
expect(block.text).toBe("navigated to https://example.com/");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("schema rejects non-URL strings (and missing url)", () => {
|
|
45
|
+
const record: GotoRecord = { calls: [] };
|
|
46
|
+
const tool = createNavigateTool({ driver: stubDriver(record) });
|
|
47
|
+
expect(() => tool.inputSchema.parse({ url: "not-a-url" })).toThrow();
|
|
48
|
+
expect(() => tool.inputSchema.parse({})).toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("driver.goto failure surfaces as NavigateError with the URL in the message", async () => {
|
|
52
|
+
const record: GotoRecord = { calls: [] };
|
|
53
|
+
const tool = createNavigateTool({
|
|
54
|
+
driver: stubDriver(record, { gotoErr: new Error("net::ERR_NAME_NOT_RESOLVED") }),
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
await tool.execute({ url: "https://does-not-exist.invalid/" }, {});
|
|
58
|
+
throw new Error("expected NavigateError to be thrown");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
expect(err).toBeInstanceOf(NavigateError);
|
|
61
|
+
expect((err as Error).message).toContain("https://does-not-exist.invalid/");
|
|
62
|
+
expect((err as Error).message).toContain("net::ERR_NAME_NOT_RESOLVED");
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("flag profile: default-allowed, external scope, classifier off", () => {
|
|
67
|
+
const record: GotoRecord = { calls: [] };
|
|
68
|
+
const tool = createNavigateTool({ driver: stubDriver(record) });
|
|
69
|
+
// destructive: false → permission-engine allows in default mode.
|
|
70
|
+
// Navigation is the agent's bootstrap; gating it behind alwaysAllow
|
|
71
|
+
// would silently break every browser spec without explicit rules.
|
|
72
|
+
expect(tool.destructive).toBe(false);
|
|
73
|
+
expect(tool.readOnly).toBe(false);
|
|
74
|
+
expect(tool.classifyOutput).toBe(false);
|
|
75
|
+
expect(tool.scope).toBe("external");
|
|
76
|
+
expect(tool.name).toBe("Navigate");
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog R4 `tool-navigate` — Section 25 BROW.
|
|
3
|
+
*
|
|
4
|
+
* `Navigate(url)` tool. Drives `driver.goto(url)` so the browser agent
|
|
5
|
+
* can bootstrap to a starting page. Returns a short text confirmation
|
|
6
|
+
* the model can read back; surfaces playwright errors verbatim (e.g.
|
|
7
|
+
* unreachable host, navigation timeout) so the model can recover or
|
|
8
|
+
* report the failure.
|
|
9
|
+
*
|
|
10
|
+
* Flag profile: `destructive: false` (default-allowed). Navigation is
|
|
11
|
+
* the agent's only bootstrap path — gating it behind an `alwaysAllow`
|
|
12
|
+
* rule would silently break every spec that doesn't define one. The
|
|
13
|
+
* §3 permission-engine still applies the user's explicit `alwaysDeny`
|
|
14
|
+
* rules; Click/Type/Key/Scroll stay destructive-by-default in
|
|
15
|
+
* `tool-mouse-keyboard`.
|
|
16
|
+
*
|
|
17
|
+
* Scope is `"external"` because the URL request crosses a network
|
|
18
|
+
* boundary; the §3 egress-classifier can hook in here later. Output
|
|
19
|
+
* classification is off — the response is a deterministic short string,
|
|
20
|
+
* not model-generated content.
|
|
21
|
+
*/
|
|
22
|
+
import type { Driver } from "@crewhaus/computer-use-driver";
|
|
23
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
24
|
+
import { buildTool } from "@crewhaus/tool-builder";
|
|
25
|
+
import type { RegisteredTool, ToolResultContent } from "@crewhaus/tool-catalog";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
|
|
28
|
+
export class NavigateError extends CrewhausError {
|
|
29
|
+
override readonly name = "NavigateError";
|
|
30
|
+
constructor(message: string, cause?: unknown) {
|
|
31
|
+
super("tool", message, cause);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const navigateSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
/** Absolute URL to navigate to (http/https). */
|
|
38
|
+
url: z.string().url(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
|
|
42
|
+
export type CreateNavigateToolOptions = {
|
|
43
|
+
readonly driver: Driver;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function createNavigateTool(opts: CreateNavigateToolOptions): RegisteredTool {
|
|
47
|
+
return buildTool({
|
|
48
|
+
name: "Navigate",
|
|
49
|
+
description:
|
|
50
|
+
"Navigate the browser to a URL. Call this first to load a starting page (e.g. a search engine, documentation site, or known landing page) before using Screenshot/FindElement/Click/Type. Returns a short confirmation; pair with Screenshot to see what loaded.",
|
|
51
|
+
inputSchema: navigateSchema,
|
|
52
|
+
readOnly: false,
|
|
53
|
+
destructive: false,
|
|
54
|
+
concurrencySafe: false,
|
|
55
|
+
classifyOutput: false,
|
|
56
|
+
scope: "external",
|
|
57
|
+
execute: async (input): Promise<ToolResultContent> => {
|
|
58
|
+
try {
|
|
59
|
+
await opts.driver.goto(input.url);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
throw new NavigateError(
|
|
62
|
+
`navigation to ${input.url} failed: ${(err as Error).message}`,
|
|
63
|
+
err,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return [{ type: "text", text: `navigated to ${input.url}` }];
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|