@robota-sdk/agent-tool-defaults 3.0.0-beta.100

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 ADDED
@@ -0,0 +1,80 @@
1
+ # @robota-sdk/agent-tool-defaults
2
+
3
+ The Robota SDK's built-in default tool set. `createDefaultTools()` builds the tools an agent session
4
+ gets when its composition root supplies no tool list of its own, using the tool factories from
5
+ `@robota-sdk/agent-tools`. It implements no tool itself. File tools that run on the host are
6
+ contained by the `cwd` you pass; with a `sandboxClient` that has its own filesystem, file paths go to
7
+ the sandbox and `cwd` does not confine them. Adapter-gated tools appear only when you supply their
8
+ adapter. Import it at a composition root (the place that assembles an agent), not from library code.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @robota-sdk/agent-tool-defaults @robota-sdk/agent-core
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```typescript
19
+ import { ConversationAgent } from '@robota-sdk/agent-core';
20
+ import type { IAIProvider } from '@robota-sdk/agent-core';
21
+ import { createDefaultTools } from '@robota-sdk/agent-tool-defaults';
22
+
23
+ declare const provider: IAIProvider;
24
+
25
+ const agent = new ConversationAgent({
26
+ name: 'DevAgent',
27
+ aiProviders: [provider],
28
+ defaultModel: { provider: 'anthropic', model: 'claude-sonnet-4-6' },
29
+ tools: createDefaultTools({ cwd: process.cwd() }),
30
+ });
31
+ ```
32
+
33
+ `createDefaultTools()` is synchronous and performs no I/O, and the same options always produce the
34
+ same set of tools. Most tools are built fresh on each call; `WebFetch` and `WebSearch` are shared
35
+ instances.
36
+
37
+ ## Tools
38
+
39
+ | Tool | Included when |
40
+ | -------------------------- | --------------------------------------------------------------- |
41
+ | `Shell`, `Bash` | Always |
42
+ | `Read`, `Write`, `Edit` | Always |
43
+ | `Glob`, `Grep` | Unless a `sandboxClient` with a separate filesystem is supplied |
44
+ | `WebFetch`, `WebSearch` | Always |
45
+ | `AskUserQuestion` | Always |
46
+ | `CodebaseRetrieval` | `retrievalAdapter` is supplied |
47
+ | `ComputerView`, `Computer` | `computerDriver` is supplied |
48
+
49
+ With a `sandboxClient`, `Shell` and `Bash` run commands through the sandbox. When the sandbox has
50
+ its own filesystem (the default for a client that does not declare `filesystem: 'shared'`), `Read`,
51
+ `Write` and `Edit` also go through it, and `Glob` / `Grep` are left out because they have no
52
+ sandbox path. File paths are then passed to the sandbox as given, and `cwd` does not confine them.
53
+ When the sandbox shares the host filesystem, file tools stay on the host, contained by `cwd`.
54
+
55
+ ## Options
56
+
57
+ `createDefaultTools(options: ICreateDefaultToolsOptions): FunctionTool[]`
58
+
59
+ | Option | Type | Description |
60
+ | ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
+ | `cwd` | `string` | Required. The root that host file tools are contained by (not file tools routed to a separate-filesystem sandbox), and the default working directory for `Shell` / `Bash`. |
62
+ | `sandboxClient` | `ISandboxClient` | Runs commands (and, on a separate filesystem, file tools) in a sandbox. |
63
+ | `shellExecutable` | `string` | Host-selected executable for `Shell` and `Bash`. |
64
+ | `retrievalAdapter` | `IRetrievalAdapter` | Adds the `CodebaseRetrieval` tool. |
65
+ | `computerDriver` | `IComputerDriver` | Adds the `ComputerView` and `Computer` tools. There is no host fallback. |
66
+
67
+ `ISandboxClient`, `IRetrievalAdapter` and `IComputerDriver` are exported by
68
+ `@robota-sdk/agent-tools`.
69
+
70
+ ## Related packages
71
+
72
+ - [`@robota-sdk/agent-tools`](../agent-tools/README.md): the tool factories, the tool registry and
73
+ the sandbox clients.
74
+ - [`@robota-sdk/agent-core`](../agent-core/README.md): the `ConversationAgent` agent and the tool contract.
75
+
76
+ See [docs/SPEC.md](docs/SPEC.md) for the package contract.
77
+
78
+ ## License
79
+
80
+ This package is dual-licensed under the [GNU AGPL-3.0](../../LICENSE) or a [commercial license](../../COMMERCIAL.md). See [LICENSING.md](../../LICENSING.md).
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@robota-sdk/agent-tools");function t(t){let n=(0,e.routesFilesThroughSandbox)(t.sandboxClient),{editorFiles:r,...i}=t,{sandboxClient:a,...o}=t,s=n?i:o;return[(0,e.createShellTool)(i),(0,e.createBashTool)(i),(0,e.createReadTool)(s),(0,e.createWriteTool)(s),(0,e.createEditTool)(s),...n?[]:[(0,e.createGlobTool)(o),(0,e.createGrepTool)(o)],(0,e.createWebFetchTool)({userAgent:t.httpUserAgent}),(0,e.createWebSearchTool)(),(0,e.createAskUserQuestionTool)(),...t.retrievalAdapter?[(0,e.createRetrievalTool)({adapter:t.retrievalAdapter})]:[],...t.computerDriver?(0,e.createComputerTool)({driver:t.computerDriver}):[]]}exports.createDefaultTools=t;
@@ -0,0 +1,49 @@
1
+ import { FunctionTool } from "@robota-sdk/agent-core";
2
+ import { IComputerDriver, IEditorFileAccess, IRetrievalAdapter, ISandboxClient } from "@robota-sdk/agent-tools";
3
+ //#region src/create-default-tools.d.ts
4
+ /**
5
+ * Create the default set of CLI tools.
6
+ * Returns the standard tools.
7
+ */
8
+ interface ICreateDefaultToolsOptions {
9
+ sandboxClient?: ISandboxClient;
10
+ /** Host-selected executable for Shell and Bash. */
11
+ shellExecutable?: string;
12
+ httpUserAgent?: string;
13
+ /**
14
+ * The execution root every file tool is contained by. REQUIRED — ARCH-010.
15
+ *
16
+ * It was optional, and `child-process-subagent-worker.ts` called this factory with no argument at
17
+ * all: `cwd` was `undefined`, the path guard was fail-open, and the subagent's `Read` returned
18
+ * `/etc/hostname`. Requiring it means a construction site that forgets does not compile, which is
19
+ * the only way a containment root stays a contract rather than a convention.
20
+ */
21
+ cwd: string;
22
+ /**
23
+ * An editor that has the workspace open, asked at each call. The host file tools read and write
24
+ * through what it offers and the searches say they read the disk; commands never use it, and a
25
+ * sandbox with its own filesystem keeps every file tool, since its files are not the editor's.
26
+ */
27
+ editorFiles?: () => IEditorFileAccess | undefined;
28
+ /** SELFHOST-003: when present, adds the adapter-gated `CodebaseRetrieval` tool (absent otherwise). */
29
+ retrievalAdapter?: IRetrievalAdapter;
30
+ /**
31
+ * SELFHOST-010: when present, adds the adapter-gated `ComputerView` + `Computer` tools. Unlike the
32
+ * shell tool's host `spawn` fallback, computer-use has NO host fallback — with no driver the tools are
33
+ * simply ABSENT (there is no safe library-side "local" screen to fall back to).
34
+ */
35
+ computerDriver?: IComputerDriver;
36
+ }
37
+ /**
38
+ * ARCH-035: returns `FunctionTool[]`, which is what every factory below actually returns.
39
+ *
40
+ * An earlier revision cast each one to `IToolWithEventService` because session assembly consumes that
41
+ * interface. The cast was lossy in the direction that mattered: `ICapabilityPack.tools` is
42
+ * `readonly FunctionTool[]`, so a pack CONSUMING this set could not assign it without a second cast
43
+ * back — one erasure creating the need for another. `FunctionTool` satisfies `IToolWithEventService`,
44
+ * so session assembly is unaffected and no cast is needed at either end.
45
+ */
46
+ declare function createDefaultTools(options: ICreateDefaultToolsOptions): FunctionTool[];
47
+ //#endregion
48
+ export { type ICreateDefaultToolsOptions, createDefaultTools };
49
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/create-default-tools.ts"],"mappings":";;;;;;;UA+CiB;EACf,gBAAgB;;EAEhB;EACA;;;;;;;;;EASA;;;;;;EAMA,oBAAoB;;EAEpB,mBAAmB;;;;;;EAMnB,iBAAiB;;;;;;;;;;;iBAYH,mBAAmB,SAAS,6BAA6B"}
@@ -0,0 +1,49 @@
1
+ import { IComputerDriver, IEditorFileAccess, IRetrievalAdapter, ISandboxClient } from "@robota-sdk/agent-tools";
2
+ import { FunctionTool } from "@robota-sdk/agent-core";
3
+ //#region src/create-default-tools.d.ts
4
+ /**
5
+ * Create the default set of CLI tools.
6
+ * Returns the standard tools.
7
+ */
8
+ interface ICreateDefaultToolsOptions {
9
+ sandboxClient?: ISandboxClient;
10
+ /** Host-selected executable for Shell and Bash. */
11
+ shellExecutable?: string;
12
+ httpUserAgent?: string;
13
+ /**
14
+ * The execution root every file tool is contained by. REQUIRED — ARCH-010.
15
+ *
16
+ * It was optional, and `child-process-subagent-worker.ts` called this factory with no argument at
17
+ * all: `cwd` was `undefined`, the path guard was fail-open, and the subagent's `Read` returned
18
+ * `/etc/hostname`. Requiring it means a construction site that forgets does not compile, which is
19
+ * the only way a containment root stays a contract rather than a convention.
20
+ */
21
+ cwd: string;
22
+ /**
23
+ * An editor that has the workspace open, asked at each call. The host file tools read and write
24
+ * through what it offers and the searches say they read the disk; commands never use it, and a
25
+ * sandbox with its own filesystem keeps every file tool, since its files are not the editor's.
26
+ */
27
+ editorFiles?: () => IEditorFileAccess | undefined;
28
+ /** SELFHOST-003: when present, adds the adapter-gated `CodebaseRetrieval` tool (absent otherwise). */
29
+ retrievalAdapter?: IRetrievalAdapter;
30
+ /**
31
+ * SELFHOST-010: when present, adds the adapter-gated `ComputerView` + `Computer` tools. Unlike the
32
+ * shell tool's host `spawn` fallback, computer-use has NO host fallback — with no driver the tools are
33
+ * simply ABSENT (there is no safe library-side "local" screen to fall back to).
34
+ */
35
+ computerDriver?: IComputerDriver;
36
+ }
37
+ /**
38
+ * ARCH-035: returns `FunctionTool[]`, which is what every factory below actually returns.
39
+ *
40
+ * An earlier revision cast each one to `IToolWithEventService` because session assembly consumes that
41
+ * interface. The cast was lossy in the direction that mattered: `ICapabilityPack.tools` is
42
+ * `readonly FunctionTool[]`, so a pack CONSUMING this set could not assign it without a second cast
43
+ * back — one erasure creating the need for another. `FunctionTool` satisfies `IToolWithEventService`,
44
+ * so session assembly is unaffected and no cast is needed at either end.
45
+ */
46
+ declare function createDefaultTools(options: ICreateDefaultToolsOptions): FunctionTool[];
47
+ //#endregion
48
+ export { type ICreateDefaultToolsOptions, createDefaultTools };
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/create-default-tools.ts"],"mappings":";;;;;;;UA+CiB;EACf,gBAAgB;;EAEhB;EACA;;;;;;;;;EASA;;;;;;EAMA,oBAAoB;;EAEpB,mBAAmB;;;;;;EAMnB,iBAAiB;;;;;;;;;;;iBAYH,mBAAmB,SAAS,6BAA6B"}
@@ -0,0 +1,2 @@
1
+ import{createAskUserQuestionTool as e,createBashTool as t,createComputerTool as n,createEditTool as r,createGlobTool as i,createGrepTool as a,createReadTool as o,createRetrievalTool as s,createShellTool as c,createWebFetchTool as l,createWebSearchTool as u,createWriteTool as d,routesFilesThroughSandbox as f}from"@robota-sdk/agent-tools";function p(p){let m=f(p.sandboxClient),{editorFiles:h,...g}=p,{sandboxClient:_,...v}=p,y=m?g:v;return[c(g),t(g),o(y),d(y),r(y),...m?[]:[i(v),a(v)],l({userAgent:p.httpUserAgent}),u(),e(),...p.retrievalAdapter?[s({adapter:p.retrievalAdapter})]:[],...p.computerDriver?n({driver:p.computerDriver}):[]]}export{p as createDefaultTools};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/create-default-tools.ts"],"sourcesContent":["/**\n * The DEFAULT TOOL SET — a composition leaf.\n *\n * ARCH-035 moved this out of `@robota-sdk/agent-framework`. It is a defaults aggregator, and\n * `.agents/project-structure.md` classifies that family as a composition leaf \"imported only at\n * composition roots (entry-point-only)\" — not something a mid-layer assembly library owns, publishes\n * and self-consumes.\n *\n * The move is what carries the guarantee. `agent-subagent-runner` may depend on `agent-framework`\n * (it needs `createSubagentSession` and friends) but has no manifest edge to THIS package, so\n * `import { createDefaultTools } from '@robota-sdk/agent-tool-defaults'` does not resolve there at\n * all. A neutral runner composing the product's tool surface is now a compile error rather than a\n * scan finding — the same shape ARCH-021 achieved on the provider axis with\n * `@robota-sdk/agent-builtin-providers`.\n *\n * `DEFAULT_TOOL_DESCRIPTIONS` deliberately did NOT come along: its only consumer builds the system\n * prompt synchronously, and dragging it here would force either a static edge back into this leaf or\n * a second async hop through prompt building. It stays beside its consumer in `agent-framework`.\n */\nimport {\n createAskUserQuestionTool,\n createShellTool,\n createBashTool,\n createReadTool,\n createWriteTool,\n createEditTool,\n createRetrievalTool,\n createComputerTool,\n createGlobTool,\n createGrepTool,\n createWebFetchTool,\n createWebSearchTool,\n routesFilesThroughSandbox,\n} from '@robota-sdk/agent-tools';\n\nimport type { FunctionTool } from '@robota-sdk/agent-core';\nimport type {\n IComputerDriver,\n IEditorFileAccess,\n IRetrievalAdapter,\n ISandboxClient,\n} from '@robota-sdk/agent-tools';\n\n/**\n * Create the default set of CLI tools.\n * Returns the standard tools.\n */\nexport interface ICreateDefaultToolsOptions {\n sandboxClient?: ISandboxClient;\n /** Host-selected executable for Shell and Bash. */\n shellExecutable?: string;\n httpUserAgent?: string;\n /**\n * The execution root every file tool is contained by. REQUIRED — ARCH-010.\n *\n * It was optional, and `child-process-subagent-worker.ts` called this factory with no argument at\n * all: `cwd` was `undefined`, the path guard was fail-open, and the subagent's `Read` returned\n * `/etc/hostname`. Requiring it means a construction site that forgets does not compile, which is\n * the only way a containment root stays a contract rather than a convention.\n */\n cwd: string;\n /**\n * An editor that has the workspace open, asked at each call. The host file tools read and write\n * through what it offers and the searches say they read the disk; commands never use it, and a\n * sandbox with its own filesystem keeps every file tool, since its files are not the editor's.\n */\n editorFiles?: () => IEditorFileAccess | undefined;\n /** SELFHOST-003: when present, adds the adapter-gated `CodebaseRetrieval` tool (absent otherwise). */\n retrievalAdapter?: IRetrievalAdapter;\n /**\n * SELFHOST-010: when present, adds the adapter-gated `ComputerView` + `Computer` tools. Unlike the\n * shell tool's host `spawn` fallback, computer-use has NO host fallback — with no driver the tools are\n * simply ABSENT (there is no safe library-side \"local\" screen to fall back to).\n */\n computerDriver?: IComputerDriver;\n}\n\n/**\n * ARCH-035: returns `FunctionTool[]`, which is what every factory below actually returns.\n *\n * An earlier revision cast each one to `IToolWithEventService` because session assembly consumes that\n * interface. The cast was lossy in the direction that mattered: `ICapabilityPack.tools` is\n * `readonly FunctionTool[]`, so a pack CONSUMING this set could not assign it without a second cast\n * back — one erasure creating the need for another. `FunctionTool` satisfies `IToolWithEventService`,\n * so session assembly is unaffected and no cast is needed at either end.\n */\nexport function createDefaultTools(options: ICreateDefaultToolsOptions): FunctionTool[] {\n // Issue #3081: the sandbox's filesystem relation decides where file tools read and write. On a\n // SEPARATE filesystem every file tool goes through the sandbox — and `Glob`/`Grep`, which have no\n // sandbox path, are withheld, so a search can never read the host while an edit writes the\n // sandbox. On a SHARED filesystem (commands confined over the host's) file tools stay on the host\n // under the path guard, and only commands run through the sandbox.\n const separate = routesFilesThroughSandbox(options.sandboxClient);\n // An editor holds the host's files, so it reaches only the tools that work on them; commands keep\n // the agent's own sandbox.\n const { editorFiles: _hostFilesOnly, ...commandOptions } = options;\n const { sandboxClient: _commandsOnly, ...hostFileOptions } = options;\n const fileOptions = separate ? commandOptions : hostFileOptions;\n return [\n createShellTool(commandOptions),\n createBashTool(commandOptions),\n createReadTool(fileOptions),\n createWriteTool(fileOptions),\n createEditTool(fileOptions),\n // SEC-007: built per call, NOT the module-level singletons. A singleton is context-free by\n // construction, so registering one meant `Glob`/`Grep` could enumerate outside the session's\n // working directory while `Read`/`Write`/`Edit` were contained.\n ...(separate ? [] : [createGlobTool(hostFileOptions), createGrepTool(hostFileOptions)]),\n // MUST 4: also built per call, for the same reason — `webFetchTool`/`webSearchTool` are\n // module-level singletons in `agent-tools`, so two concurrently open sessions built from this\n // set (e.g. two `sessionParticipant`s) shared them and tripped `resource-reused` even though\n // nothing about a WebFetch/WebSearch tool actually needs to be shared across sessions.\n createWebFetchTool({ userAgent: options.httpUserAgent }),\n createWebSearchTool(),\n createAskUserQuestionTool(),\n // Retrieval is adapter-gated: absent when no adapter is supplied (there is no host fallback).\n ...(options.retrievalAdapter\n ? [createRetrievalTool({ adapter: options.retrievalAdapter })]\n : []),\n // Computer-use is adapter-gated on the driver: absent when no driver is supplied. There is NO host\n // fallback (unlike the shell tool's host spawn) — no library-side \"local\" screen exists.\n ...(options.computerDriver ? createComputerTool({ driver: options.computerDriver }) : []),\n ];\n}\n"],"mappings":"mVAsFA,SAAgB,EAAmB,EAAqD,CAMtF,IAAM,EAAW,EAA0B,EAAQ,aAAa,EAG1D,CAAE,YAAa,EAAgB,GAAG,GAAmB,EACrD,CAAE,cAAe,EAAe,GAAG,GAAoB,EACvD,EAAc,EAAW,EAAiB,EAChD,MAAO,CACL,EAAgB,CAAc,EAC9B,EAAe,CAAc,EAC7B,EAAe,CAAW,EAC1B,EAAgB,CAAW,EAC3B,EAAe,CAAW,EAI1B,GAAI,EAAW,CAAC,EAAI,CAAC,EAAe,CAAe,EAAG,EAAe,CAAe,CAAC,EAKrF,EAAmB,CAAE,UAAW,EAAQ,aAAc,CAAC,EACvD,EAAoB,EACpB,EAA0B,EAE1B,GAAI,EAAQ,iBACR,CAAC,EAAoB,CAAE,QAAS,EAAQ,gBAAiB,CAAC,CAAC,EAC3D,CAAC,EAGL,GAAI,EAAQ,eAAiB,EAAmB,CAAE,OAAQ,EAAQ,cAAe,CAAC,EAAI,CAAC,CACzF,CACF"}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@robota-sdk/agent-tool-defaults",
3
+ "version": "3.0.0-beta.100",
4
+ "description": "The SDK's built-in default tool set (createDefaultTools) as a composition leaf over agent-tools",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "dist/node/index.js",
10
+ "types": "dist/node/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "source": "./src/index.ts",
14
+ "node": {
15
+ "import": {
16
+ "types": "./dist/node/index.d.ts",
17
+ "default": "./dist/node/index.js"
18
+ },
19
+ "require": {
20
+ "types": "./dist/node/index.d.cts",
21
+ "default": "./dist/node/index.cjs"
22
+ }
23
+ },
24
+ "default": {
25
+ "import": {
26
+ "types": "./dist/node/index.d.ts",
27
+ "default": "./dist/node/index.js"
28
+ },
29
+ "require": {
30
+ "types": "./dist/node/index.d.cts",
31
+ "default": "./dist/node/index.cjs"
32
+ }
33
+ }
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "engines": {
38
+ "node": ">=22.12.0"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "src",
43
+ "CHANGELOG.md"
44
+ ],
45
+ "devDependencies": {
46
+ "@types/node": "^22.19.21",
47
+ "rimraf": "^5.0.10",
48
+ "tsdown": "^0.22.14",
49
+ "vitest": "^3.2.6"
50
+ },
51
+ "license": "AGPL-3.0-only OR LicenseRef-Commercial",
52
+ "dependencies": {
53
+ "@robota-sdk/agent-core": "3.0.0-beta.100",
54
+ "@robota-sdk/agent-tools": "3.0.0-beta.100"
55
+ },
56
+ "volta": {
57
+ "extends": "../../package.json"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/woojubb/robota"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/woojubb/robota/issues"
65
+ },
66
+ "homepage": "https://robota.io/",
67
+ "scripts": {
68
+ "build": "rimraf dist && tsdown",
69
+ "build:js": "pnpm run build",
70
+ "build:types": "pnpm run build",
71
+ "typecheck": "tsgo --noEmit",
72
+ "test": "vitest run --passWithNoTests",
73
+ "test:coverage": "vitest run --coverage --passWithNoTests",
74
+ "lint": "eslint src/ --ext .ts",
75
+ "clean": "rimraf dist"
76
+ }
77
+ }
@@ -0,0 +1,289 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { InMemorySandboxClient } from '@robota-sdk/agent-tools';
6
+ import { afterAll, describe, expect, it } from 'vitest';
7
+
8
+ import { createDefaultTools } from '../create-default-tools.js';
9
+
10
+ import type { IRetrievalAdapter, IComputerDriver } from '@robota-sdk/agent-tools';
11
+
12
+ /**
13
+ * ARCH-010 made `cwd` a required field of `ICreateDefaultToolsOptions`. The cases that assert the tool
14
+ * LIST never execute a tool and so never reach a path guard; this root is named once, here, so that
15
+ * inertness is visible rather than implied. The containment cases below choose their own roots.
16
+ * Every root sits, unmade, inside one private per-run directory rather than at a fixed name under
17
+ * /tmp that another local user could create first.
18
+ */
19
+ const PRIVATE_BASE = mkdtempSync(join(tmpdir(), 'agent-test-create-tools-'));
20
+ afterAll(() => rmSync(PRIVATE_BASE, { recursive: true, force: true }));
21
+ const ASSEMBLY_ROOT = join(PRIVATE_BASE, 'create-tools-assembly-root');
22
+
23
+ describe('createDefaultTools', () => {
24
+ it('assembles all default local tools and describes web tools as local tools', () => {
25
+ expect(createDefaultTools({ cwd: ASSEMBLY_ROOT }).map((tool) => tool.getName())).toEqual([
26
+ 'Shell',
27
+ 'Bash',
28
+ 'Read',
29
+ 'Write',
30
+ 'Edit',
31
+ 'Glob',
32
+ 'Grep',
33
+ 'WebFetch',
34
+ 'WebSearch',
35
+ 'AskUserQuestion',
36
+ ]);
37
+ });
38
+
39
+ it('accepts a separate-filesystem sandbox client, withholding the host-only search tools', () => {
40
+ const sandboxClient = new InMemorySandboxClient();
41
+
42
+ expect(
43
+ createDefaultTools({ sandboxClient, cwd: ASSEMBLY_ROOT }).map((tool) => tool.getName()),
44
+ ).toEqual([
45
+ 'Shell',
46
+ 'Bash',
47
+ 'Read',
48
+ 'Write',
49
+ 'Edit',
50
+ 'WebFetch',
51
+ 'WebSearch',
52
+ 'AskUserQuestion',
53
+ ]);
54
+ });
55
+
56
+ // SEC-007 — the reachability floor for the ENUMERATING tools. `Glob`/`Grep` used to be registered
57
+ // here as the module-level singletons, which take no `cwd` at all: the assembly's containment root
58
+ // reached `Read`/`Write`/`Edit` and stopped. A per-call instance is the whole fix, so the property
59
+ // asserted is that the assembly produces a CONTAINED tool, not merely that a factory can accept a
60
+ // `cwd` nobody passes.
61
+ it('SEC-007: Glob and Grep are bound to the assembly cwd, not context-free singletons', async () => {
62
+ const contained = createDefaultTools({ cwd: join(PRIVATE_BASE, 'sec007-assembly-scope') });
63
+ // ARCH-010 — this used to be `createDefaultTools()` with no root at all, which is no longer
64
+ // constructible. A SECOND root serves the same purpose and states it better: one shared singleton
65
+ // could not carry two different roots either way.
66
+ const otherRoot = createDefaultTools({ cwd: join(PRIVATE_BASE, 'sec007-other-scope') });
67
+
68
+ for (const name of ['Glob', 'Grep']) {
69
+ const tool = contained.find((candidate) => candidate.getName() === name);
70
+ expect(tool, `${name} must be part of the default set`).toBeDefined();
71
+ // A fresh instance per call — a shared singleton could not carry two different roots.
72
+ expect(tool).not.toBe(otherRoot.find((candidate) => candidate.getName() === name));
73
+
74
+ const parameters = { pattern: name === 'Grep' ? 'root' : '*', path: '/etc' };
75
+ const outcome = await tool!.execute(
76
+ parameters as never,
77
+ {
78
+ toolName: name,
79
+ parameters,
80
+ } as never,
81
+ );
82
+ const result = JSON.parse(String((outcome as { data?: unknown }).data)) as {
83
+ success: boolean;
84
+ error?: string;
85
+ };
86
+
87
+ expect(result.success, `${name} must refuse an out-of-root search root`).toBe(false);
88
+ expect(result.error).toContain('outside the working directory');
89
+ }
90
+ });
91
+
92
+ // MUST 4: `WebFetch`/`WebSearch` used to be the module-level singletons `webFetchTool` /
93
+ // `webSearchTool` from `@robota-sdk/agent-tools` — the SAME object on every call. Two
94
+ // concurrently open sessions each built from `createDefaultTools({cwd})` then shared them and
95
+ // tripped a resource guard's `resource-reused` even though nothing about either tool needs
96
+ // sharing across sessions. A fresh instance per call is the fix, mirroring Glob/Grep (SEC-007).
97
+ it('MUST 4: WebFetch and WebSearch are fresh instances per call, not context-free singletons', () => {
98
+ const first = createDefaultTools({ cwd: ASSEMBLY_ROOT });
99
+ const second = createDefaultTools({ cwd: ASSEMBLY_ROOT });
100
+ for (const name of ['WebFetch', 'WebSearch']) {
101
+ const a = first.find((tool) => tool.getName() === name);
102
+ const b = second.find((tool) => tool.getName() === name);
103
+ expect(a, `${name} must be part of the default set`).toBeDefined();
104
+ expect(b).toBeDefined();
105
+ expect(a).not.toBe(b);
106
+ }
107
+ });
108
+
109
+ // SELFHOST-003 TC-03: the retrieval adapter is threaded through assembly and the tool is
110
+ // adapter-gated — absent with no adapter, present (and only then) when an adapter is supplied.
111
+ it('TC-03: CodebaseRetrieval joins the default set only when a retrieval adapter is supplied', () => {
112
+ expect(createDefaultTools({ cwd: ASSEMBLY_ROOT }).map((tool) => tool.getName())).not.toContain(
113
+ 'CodebaseRetrieval',
114
+ );
115
+
116
+ const retrievalAdapter: IRetrievalAdapter = {
117
+ retrieve: async () => ({ symbols: [], totalTokens: 0 }),
118
+ };
119
+ expect(
120
+ createDefaultTools({ retrievalAdapter, cwd: ASSEMBLY_ROOT }).map((tool) => tool.getName()),
121
+ ).toContain('CodebaseRetrieval');
122
+ });
123
+
124
+ // SELFHOST-010 TC-04: the computer driver is threaded through assembly and the ComputerView/Computer
125
+ // tools are adapter-gated — ABSENT with no driver (no host fallback), present only when a driver is
126
+ // supplied.
127
+ it('TC-04: ComputerView/Computer join the default set only when a computer driver is supplied', () => {
128
+ const withoutDriver = createDefaultTools({ cwd: ASSEMBLY_ROOT }).map((tool) => tool.getName());
129
+ expect(withoutDriver).not.toContain('ComputerView');
130
+ expect(withoutDriver).not.toContain('Computer');
131
+
132
+ const computerDriver: IComputerDriver = {
133
+ screenshot: async () => ({ data: 'x', mediaType: 'image/png' }),
134
+ act: async () => ({ screenshot: { data: 'x', mediaType: 'image/png' } }),
135
+ };
136
+ const withDriver = createDefaultTools({ computerDriver, cwd: ASSEMBLY_ROOT }).map((tool) =>
137
+ tool.getName(),
138
+ );
139
+ expect(withDriver).toContain('ComputerView');
140
+ expect(withDriver).toContain('Computer');
141
+ });
142
+ });
143
+
144
+ /**
145
+ * CLI-1990 TC-11 — the default tier is RESIDENT.
146
+ *
147
+ * Residency is declared by omission: a tool that says nothing about `deferLoading` is sent on every
148
+ * request, which is what every built-in must remain. Anthropic's own guidance is that standard tool
149
+ * calling beats a search under ten tools, so deferring any of these would be a measurable
150
+ * regression rather than a saving — and the invariant that at least one tool stays resident is
151
+ * satisfied by this tier alone.
152
+ */
153
+ describe('CLI-1990 TC-11 — default tool residency', () => {
154
+ it('declares no deferLoading on any built-in — the whole tier is resident', () => {
155
+ const deferred = createDefaultTools({ cwd: ASSEMBLY_ROOT })
156
+ .filter((tool) => tool.schema.deferLoading !== undefined)
157
+ .map((tool) => tool.getName());
158
+ expect(deferred).toEqual([]);
159
+ });
160
+
161
+ it('keeps the ten built-ins ten — the search tool is added by session assembly, not here', () => {
162
+ const names = createDefaultTools({ cwd: ASSEMBLY_ROOT }).map((tool) => tool.getName());
163
+ expect(names).toEqual([
164
+ 'Shell',
165
+ 'Bash',
166
+ 'Read',
167
+ 'Write',
168
+ 'Edit',
169
+ 'Glob',
170
+ 'Grep',
171
+ 'WebFetch',
172
+ 'WebSearch',
173
+ 'AskUserQuestion',
174
+ ]);
175
+ expect(names).not.toContain('ToolSearch');
176
+ });
177
+
178
+ it('stays resident under the adapter gates too', () => {
179
+ const retrievalAdapter: IRetrievalAdapter = {
180
+ retrieve: async () => ({ symbols: [], totalTokens: 0 }),
181
+ };
182
+ const withRetrieval = createDefaultTools({ retrievalAdapter, cwd: ASSEMBLY_ROOT });
183
+ expect(withRetrieval.every((tool) => tool.schema.deferLoading === undefined)).toBe(true);
184
+ });
185
+ });
186
+
187
+ describe('file tools follow the sandbox filesystem relation (issue #3081)', () => {
188
+ it('a separate filesystem withholds Glob and Grep, and routes Read through the sandbox', async () => {
189
+ const sandboxClient = new InMemorySandboxClient();
190
+ await sandboxClient.writeFile('/sandbox/only.txt', 'inside the sandbox');
191
+ const tools = createDefaultTools({ cwd: '/sandbox', sandboxClient });
192
+ const names = tools.map((tool) => tool.getName());
193
+ expect(names).not.toContain('Glob');
194
+ expect(names).not.toContain('Grep');
195
+ const read = tools.find((tool) => tool.getName() === 'Read')!;
196
+ const result = await read.execute({ filePath: '/sandbox/only.txt' });
197
+ expect(JSON.stringify(result)).toContain('inside the sandbox');
198
+ });
199
+
200
+ it('a shared filesystem keeps every file tool on the host', async () => {
201
+ const shared = Object.assign(new InMemorySandboxClient(), { filesystem: 'shared' as const });
202
+ await shared.writeFile('/sandbox/only.txt', 'inside the sandbox');
203
+ const tools = createDefaultTools({ cwd: '/sandbox', sandboxClient: shared });
204
+ const names = tools.map((tool) => tool.getName());
205
+ expect(names).toContain('Glob');
206
+ expect(names).toContain('Grep');
207
+ const read = tools.find((tool) => tool.getName() === 'Read')!;
208
+ const result = await read.execute({ filePath: '/sandbox/only.txt' });
209
+ // The host has no /sandbox root, so the host read fails; it never consults the sandbox's files.
210
+ expect(JSON.stringify(result)).not.toContain('inside the sandbox');
211
+ });
212
+ });
213
+
214
+ /** Issue #3550 item 4 — an editor reaches the host file tools and the searches, never the commands. */
215
+ describe('an editor reaches the host file tools only', () => {
216
+ function recordingEditor() {
217
+ const asked: string[] = [];
218
+ const requests: string[] = [];
219
+ const access = {
220
+ readTextFile: async (path: string) => {
221
+ requests.push(`read ${path}`);
222
+ return 'EDITOR TEXT\n';
223
+ },
224
+ writeTextFile: async (path: string) => {
225
+ requests.push(`write ${path}`);
226
+ },
227
+ };
228
+ return {
229
+ requests,
230
+ asked,
231
+ editorFiles: () => {
232
+ asked.push('asked');
233
+ return access;
234
+ },
235
+ };
236
+ }
237
+ const parse = (outcome: unknown) =>
238
+ JSON.parse(String((outcome as { data?: unknown }).data)) as { output: string };
239
+
240
+ it('Read and Write go through the editor and the searches say they read the disk', async () => {
241
+ const root = mkdtempSync(join(PRIVATE_BASE, 'editor-'));
242
+ writeFileSync(join(root, 'probe.txt'), 'DISK TEXT\n');
243
+ const editor = recordingEditor();
244
+ const tools = createDefaultTools({ cwd: root, editorFiles: editor.editorFiles });
245
+ const byName = (name: string) => tools.find((tool) => tool.getName() === name)!;
246
+
247
+ expect(parse(await byName('Read').execute({ filePath: 'probe.txt' })).output).toContain(
248
+ 'EDITOR TEXT',
249
+ );
250
+ await byName('Write').execute({ filePath: 'out.txt', content: 'x' });
251
+ expect(editor.requests).toEqual([`read ${join(root, 'probe.txt')}`, `write ${join(root, 'out.txt')}`]);
252
+ expect(parse(await byName('Grep').execute({ pattern: 'DISK' })).output).toContain(
253
+ '[Searched the files on disk.',
254
+ );
255
+ });
256
+
257
+ it('Shell and Bash never ask for the editor', async () => {
258
+ const root = mkdtempSync(join(PRIVATE_BASE, 'editor-shell-'));
259
+ const editor = recordingEditor();
260
+ const tools = createDefaultTools({ cwd: root, editorFiles: editor.editorFiles });
261
+ for (const name of ['Shell', 'Bash']) {
262
+ const outcome = await tools
263
+ .find((tool) => tool.getName() === name)!
264
+ .execute({ command: 'echo shell-ok' });
265
+ expect(JSON.stringify(outcome)).toContain('shell-ok');
266
+ }
267
+ expect(editor.asked).toEqual([]);
268
+ });
269
+
270
+ it('a separate sandbox filesystem keeps every file tool, so the editor is not asked', async () => {
271
+ const sandboxClient = new InMemorySandboxClient();
272
+ await sandboxClient.writeFile('/sandbox/only.txt', 'inside the sandbox');
273
+ const editor = recordingEditor();
274
+ const tools = createDefaultTools({
275
+ cwd: '/sandbox',
276
+ sandboxClient,
277
+ editorFiles: editor.editorFiles,
278
+ });
279
+ const read = tools.find((tool) => tool.getName() === 'Read')!;
280
+ expect(JSON.stringify(await read.execute({ filePath: '/sandbox/only.txt' }))).toContain(
281
+ 'inside the sandbox',
282
+ );
283
+ await tools.find((tool) => tool.getName() === 'Write')!.execute({
284
+ filePath: '/sandbox/out.txt',
285
+ content: 'x',
286
+ });
287
+ expect(editor.asked).toEqual([]);
288
+ });
289
+ });