# How ego-lite Resolves and Executes Site-Specific Tools (Node.js and Browser-Side)

> Discover how ego-lite resolves and executes site-specific tools in Node.js and browser environments by scanning for matching manifests and dynamically importing or evaluating tools.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-08-28

---

**ego-lite resolves site-specific tools by scanning a configurable `learnings/` directory for manifests matching a URL's hostname, then dynamically importing Node.js tools or executing browser tools via Chrome DevTools Protocol (CDP) evaluation.**

The **ego-lite** browser automation framework includes a pluggable *site-skill* subsystem that lets agents discover, load, and run custom tools defined for individual websites. This architecture supports both **Node.js tools** (executed in the agent's Node runtime) and **browser-side tools** (executed inside the remote browser page). This article explains the full resolution and execution flow with inline source references to the actual implementation.

## Site-Skill Discovery: Matching URLs to Manifests

ego-lite begins tool resolution by identifying which site-skill, if any, applies to the current URL. This process involves four coordinated steps across the learning subsystem.

### 1. Locate the Site-Skills Root Directory

The `siteSkillsRoot()` function determines where site-specific learnings are stored. It prefers the `EGO_BROWSER_AGENT_WORKSPACE` environment variable when available, falling back to the repository-bundled `learnings` folder:

```javascript
// package/ego-browser/src/learning/check-domain-learning.js
export function siteSkillsRoot(agentWorkspace) {
  return agentWorkspace
    ? resolve(agentWorkspace, "learnings")
    : resolve(__dirname, "..", "..", "learnings");
}

```

This allows deployments to separate code from data, storing per-site customizations outside the main package.

### 2. Enumerate Candidate Site Directories

The `iterLearningDirs(root)` function reads subdirectories under the root, returning each potential site-skill directory for manifest inspection:

```javascript
// package/ego-browser/src/learning/check-domain-learning.js
export async function iterLearningDirs(root) {
  const entries = await readdir(root, { withFileTypes: true });
  return entries.filter((e) => e.isDirectory()).map((e) => join(root, e.name));
}

```

### 3. Load and Parse Manifests

Each site-skill directory must contain a [`manifest.json`](https://github.com/citrolabs/ego-lite/blob/main/manifest.json). The `loadLearningManifest(siteDir)` function reads and parses this file:

```javascript
// package/ego-browser/src/learning/check-domain-learning.js
export async function loadLearningManifest(siteDir) {
  const manifestPath = join(siteDir, "manifest.json");
  const data = await readFile(manifestPath, "utf8");
  return JSON.parse(data);
}

```

A valid manifest includes a `domain` field (the hostname it matches) and tool definitions under `nodeTools` and/or `browserTools` keys.

### 4. Match Hostname to Domain

The `siteSkillsForUrl(url)` function extracts the hostname and returns all manifests whose `domain` field matches:

```javascript
// package/ego-browser/src/learning/check-domain-learning.js
export async function siteSkillsForUrl(url, options = {}) {
  const hostname = urlHostname(url);
  const roots = [siteSkillsRoot(options.agentWorkspace)];
  const matches = [];
  for (const root of roots) {
    for (const siteDir of await iterLearningDirs(root)) {
      const manifest = await loadLearningManifest(siteDir);
      if (manifest.domain && manifest.domain === hostname) {
        matches.push({ id: manifest.id, name: manifest.name, path: siteDir, ...manifest });
      }
    }
  }
  return matches;
}

```

The `urlHostname()` helper safely extracts the hostname using the standard `URL` constructor.

## Aggregating Learned Context

Once matching site-skills are identified, `loadLearnedContext(url)` gathers all relevant information—including notes and tool signatures—into a unified structure passed to the agent:

```typescript
// package/ego-browser/src/learning/index.ts
export async function loadLearnedContext(url: string, options = {}) {
  const skills = await siteSkillsForUrl(url, options);
  const context = {
    notes: [] as string[],
    nodeTools: {} as Record<string, ToolSchema>,
    browserTools: {} as Record<string, ToolSchema>,
  };
  for (const skill of skills) {
    // Load notes from notes/*.md files
    // Merge tool schemas from manifest
  }
  return context;
}

```

At this point, the agent has:

- **`siteId`** — the unique identifier from the manifest
- **`nodeTools`** — a map of tool names to `{path, callable, args, ...}` schemas
- **`browserTools`** — a map of tool names to `{path, args, ...}` schemas

## Executing Node.js Site Tools

Node.js tools are standard JavaScript modules executed directly in the agent's Node runtime. The `runSiteTool()` function handles their resolution and invocation.

### Tool Resolution and Validation

```javascript
// package/ego-browser/src/helpers.ts
export async function runSiteTool(siteId, toolName, args = {}, ctx = {}, options = {}) {
  const { siteDir, manifest } = await findSiteSkill(siteId, options);
  const schema = toolSchemas(manifest, "nodeTools")[toolName];
  const callable = schema?.callable;
  if (!callable) {
    throw new Error(`Node tool ${toolName} not found`);
  }
  // ...
}

```

The `findSiteSkill()` helper locates the manifest by `siteId`, and `toolSchemas()` extracts the tool definitions. The `callable` field specifies which exported function to invoke.

### Path Security and Dynamic Import

Before loading, `relativeSitePath()` validates that the declared path is a safe relative path within the site-skill directory—preventing directory traversal attacks:

```typescript
// package/ego-browser/src/learning/index.ts
export function relativeSitePath(siteDir: string, relPath: string, label: string): string {
  const fullPath = resolve(siteDir, relPath);
  if (!fullPath.startsWith(siteDir)) {
    throw new Error(`${label} path escapes site directory: ${relPath}`);
  }
  return fullPath;
}

```

The tool module is then imported with cache-busting to ensure fresh loads on every call:

```javascript
// package/ego-browser/src/helpers.ts
const toolPath = relativeSitePath(siteDir, schema.path, "Node tool");
const mod = await import(`${pathToFileURL(toolPath).href}?t=${Date.now()}`);
const fn = mod[callable];
if (typeof fn !== "function") {
  throw new Error(`Callable ${callable} not a function`);
}
return fn(ctx, args);

```

The function receives `ctx` (the current browsing context) and `args` (the tool arguments), returning its result directly to the agent.

## Executing Browser-Side Site Tools

Browser tools run **inside the remote page's JavaScript environment**, enabling direct DOM manipulation and access to page JavaScript state. ego-lite implements this via Chrome DevTools Protocol (CDP) evaluation.

### Source Loading and Wrapping

The `runSiteBrowserTool()` function orchestrates browser tool execution:

```javascript
// package/ego-browser/src/helpers.ts
export async function runSiteBrowserTool(siteId, toolName, args = {}, ctx = {}, options = {}) {
  const { siteDir, manifest } = await findSiteSkill(siteId, options);
  const schema = toolSchemas(manifest, "browserTools")[toolName];
  const source = await loadBrowserToolSource(siteId, toolName, options);
  const wrapped = wrapBrowserTool(source, args);
  const result = await cdpEval(wrapped);
  return result;
}

```

The `loadBrowserToolSource()` function reads the tool file from disk with the same path validation as Node tools. Then `wrapBrowserTool()` constructs a self-executing async function that injects arguments as a JSON literal:

```typescript
// package/ego-browser/src/learning/index.ts
export function wrapBrowserTool(source: string, args: Record<string, unknown>): string {
  return `(async () => { const __egoBrowserTool = ${source}; return await __egoBrowserTool(${JSON.stringify(args)}); })()`;
}

```

This wrapper ensures:
- The tool code executes in an async context
- Arguments are safely serialized and available to the tool function
- The return value is properly awaited and returned

### CDP Evaluation in Page Context

The wrapped code is sent to the browser via `cdpEval()`:

```typescript
// package/ego-browser/src/cdp-eval.ts
export async function cdpEval(expression: string): Promise<unknown> {
  const runtime = await getRuntime();
  const result = await runtime.cdp('Runtime.evaluate', { 
    expression, 
    awaitPromise: true 
  });
  if (result.exceptionDetails) {
    throw new Error(`CDP evaluation error: ${result.exceptionDetails.text}`);
  }
  return result.result.value;
}

```

Key aspects of this implementation:
- **`Runtime.evaluate`** — evaluates JavaScript in the page's main execution context
- **`awaitPromise: true`** — automatically awaits returned promises
- **Exception propagation** — CDP exceptions are converted to JavaScript errors

The result is returned to the agent, completing the browser tool execution cycle.

## Practical Usage Examples

```javascript
// 1️⃣ Load learned context for a page
const ctx = await site.learnContext('https://example.com/dashboard');
console.log('Available tools:', ctx.tools);

// 2️⃣ Run a Node-side tool (data extraction, API calls, etc.)
const resultNode = await site.runTool('example-com', 'extractData', { 
  selector: '.item' 
});
console.log('Node tool result →', resultNode);

// 3️⃣ Run a browser-side tool (DOM interaction, page JavaScript)
const resultBrowser = await site.runBrowserTool('example-com', 'clickBuy', { 
  productId: 42 
});
console.log('Browser tool result →', resultBrowser);

```

These calls are thin wrappers around the functions described above, exposed through the `site` helper object in **[`package/ego-browser/src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts)**.

## Architecture Benefits

This dual-runtime design provides significant flexibility:

- **Node.js tools** — Ideal for data processing, external API calls, file operations, and complex logic requiring Node's standard library
- **Browser tools** — Essential for DOM manipulation, triggering JavaScript event handlers, accessing page globals, and working with single-page applications
- **Unified discovery** — Both tool types share the same manifest-based registration and URL matching system
- **Security boundaries** — Path validation prevents tools from escaping their site-skill directory; CDP execution runs in the browser's sandboxed context

## Summary

- **Site-skill discovery** scans the `learnings/` directory, matches manifests by hostname, and aggregates tool schemas and notes
- **Node.js tools** are dynamically imported with cache-busting, validated paths, and invoked as exported functions with context and arguments
- **Browser tools** are read from disk, wrapped as async IIFEs with injected arguments, and executed via CDP's `Runtime.evaluate` in the page context
- **Security** is enforced through path traversal checks and sandboxed browser execution
- **Extensibility** allows new site customizations by simply adding manifest files and associated scripts without modifying core code

## Frequently Asked Questions

### How does ego-lite prevent malicious tools from accessing arbitrary files?

ego-lite uses `relativeSitePath()` in **[`package/ego-browser/src/learning/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/learning/index.ts)** to validate that all tool paths resolve within the site-skill directory. Any path attempting directory traversal (e.g., `../../../etc/passwd`) triggers an error before file access occurs.

### Can browser tools access Node.js APIs?

No. Browser tools execute entirely within the browser's JavaScript context via CDP. They do not have access to Node.js APIs, `require()`, or the file system. This isolation is fundamental to the security model—browser tools can only interact with the page and return serializable values.

### What happens if a browser tool throws an error?

The `cdpEval()` function in **[`package/ego-browser/src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts)** checks `result.exceptionDetails` after CDP evaluation. If the tool throws, the exception details are extracted and propagated as a JavaScript `Error` in the agent's Node runtime, allowing standard try/catch handling.

### How are tool arguments validated?

Currently, tool schemas in manifests define expected arguments, but runtime validation relies on the tool implementation. The agent can use the schema information from `loadLearnedContext()` to pre-validate calls, though this is not enforced automatically in the core execution functions.