Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,894 changes: 947 additions & 947 deletions packages/cli/src/commands/_lib/apply/__tests__/diff.test.ts

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions packages/cli/src/commands/_lib/connector-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,22 @@ const bundledFileCache = new Map<string, string | null>();
export function findBundledConnectorFile(key: string): string | null {
const cached = bundledFileCache.get(key);
if (cached !== undefined) return cached;
const fileName = `${key.replace(/\./g, "_")}.ts`;
// Mirror the resolver in @lobu/connector-worker's compile-connector.ts:
// subdir layout (`browser.evaluate` → `browser/evaluate.ts`) first, then
// the flat underscore convention (`chrome.tabs` → `chrome_tabs.ts`).
const candidates = [
`${key.replace(/\./g, "/")}.ts`,
`${key.replace(/\./g, "_")}.ts`,
];
let found: string | null = null;
for (const candidate of SOURCE_DIR_CANDIDATES) {
const filePath = resolve(candidate, fileName);
if (existsSync(filePath)) {
found = filePath;
break;
outer: for (const dir of SOURCE_DIR_CANDIDATES) {
for (const fileName of candidates) {
const filePath = resolve(dir, fileName);
if (!filePath.startsWith(`${dir}/`)) continue;
if (existsSync(filePath)) {
found = filePath;
break outer;
}
}
}
bundledFileCache.set(key, found);
Expand Down
27 changes: 20 additions & 7 deletions packages/connector-worker/src/compile-connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,26 @@ const CONNECTOR_KEY_RE = /^[a-z][a-z0-9]*(?:[._][a-z0-9]+)*$/;

export function findBundledConnectorFile(key: string): string | null {
if (!CONNECTOR_KEY_RE.test(key)) return null;
const fileName = `${key.replace(/\./g, '_')}.ts`;
for (const candidate of WORKER_CONNECTOR_DIR_CANDIDATES) {
const filePath = resolve(candidate, fileName);
// Belt-and-braces: assert the resolved path stays under the candidate
// dir even though CONNECTOR_KEY_RE already forbids the dangerous chars.
if (!filePath.startsWith(`${candidate}/`)) continue;
if (existsSync(filePath)) return filePath;
// Two filename conventions:
// - Subdirectory layout (preferred for grouped primitives): the dot in
// `browser.evaluate` maps to `browser/evaluate.ts`. Lets us co-locate
// related connectors without renaming the key.
// - Flat-with-underscores (existing convention): `chrome.tabs` →
// `chrome_tabs.ts`, `apple_health` → `apple_health.ts`.
// Try subdirectory first so newer primitives win when both happen to exist.
const candidates = [
`${key.replace(/\./g, '/')}.ts`,
`${key.replace(/\./g, '_')}.ts`,
];
for (const dir of WORKER_CONNECTOR_DIR_CANDIDATES) {
for (const fileName of candidates) {
const filePath = resolve(dir, fileName);
// Belt-and-braces: the resolved path must stay under the candidate
// dir. CONNECTOR_KEY_RE already forbids `..`, but the regex doesn't
// know about our path-joining choices.
if (!filePath.startsWith(`${dir}/`)) continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Path validation breaks on Windows due to hardcoded forward slash.

The check uses a hardcoded / in the template literal, but resolve() returns platform-specific separators. On Windows, dir is C:\app\connectors and filePath is C:\app\connectors\browser\evaluate.ts, so the check becomes !filePath.startsWith("C:\app\connectors/") (mixed separators) which always fails.

🔧 Proposed fix using path.sep for cross-platform compatibility
-      if (!filePath.startsWith(`${dir}/`)) continue;
+      if (!filePath.startsWith(dir + path.sep)) continue;

Alternatively, use path.join() for clarity:

+import { join, resolve, sep } from 'node:path';
...
-      if (!filePath.startsWith(`${dir}/`)) continue;
+      if (!filePath.startsWith(join(dir, ''))) continue;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!filePath.startsWith(`${dir}/`)) continue;
if (!filePath.startsWith(dir + path.sep)) continue;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connector-worker/src/compile-connector.ts` at line 75, The current
path check in compile-connector.ts uses a hardcoded "/" which breaks on Windows;
update the validation so it builds a platform-correct prefix and compares using
that (e.g., create a prefix from dir with path.sep or use path.join(dir,
path.sep) or use path.relative(dir, filePath) and ensure the relative path does
not start with "..") and then use that normalized comparison instead of
filePath.startsWith(`${dir}/`) to correctly detect files under dir across OSes;
adjust the logic surrounding the filePath and dir variables accordingly.

if (existsSync(filePath)) return filePath;
}
}
return null;
}
Expand Down
120 changes: 120 additions & 0 deletions packages/connectors/src/browser/evaluate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Browser Evaluate Connector — Owletto for Chrome only.
*
* Runs on the Owletto Chrome extension, which advertises capability
* `browser.debugger`. The extension attaches `chrome.debugger` to a tab,
* optionally navigates + waits for a selector, runs the supplied JS via
* `Runtime.evaluate`, and emits one event with the JSON-serialised result.
*
* This is the generic "agent runs JS in a user's signed-in Chrome" primitive
* — most bridge connectors (Revolut feed, banking, sites that fingerprint
* a managed Chromium) compose on top of `browser.evaluate` rather than
* shipping their own connector. The trust boundary is `config.script`: only
* the gateway-side connector author should mint it. The extension defaults
* to opening a fresh background tab so a compromised gateway / leaked token
* can't drive the tab a user is actively using; see executor.js in
* owletto-web for the full threat model.
*
* Cloud-side `sync()` / `execute()` throw — actual work happens in the
* extension's service worker (lobu-ai/owletto: apps/chrome/executor.js).
*/

import {
type ActionResult,
type ConnectorDefinition,
ConnectorRuntime,
type SyncContext,
type SyncResult,
} from '@lobu/connector-sdk';

const BRIDGE_ONLY =
'browser.evaluate runs only on a worker advertising capability "browser.debugger" (Owletto for Chrome).';

export default class BrowserEvaluateConnector extends ConnectorRuntime {
readonly definition: ConnectorDefinition = {
key: 'browser.evaluate',
name: 'Browser Evaluate',
description:
'Runs a JS snippet in a page via chrome.debugger and emits the result. The primitive most bridge connectors build on.',
version: '0.1.0',
faviconDomain: 'google.com',
requiredCapability: 'browser.debugger',
runtime: { platforms: ['chrome-extension'] },
authSchema: { methods: [{ type: 'none' }] },
feeds: {
evaluate: {
key: 'evaluate',
name: 'Evaluate JS',
description:
'Executes a JS expression in the page and emits one event with the JSON-serialised return value.',
// `script` is required and gateway-author-supplied. Auto-wire would
// insert a feed row with config=NULL and produce a runs-but-fails
// loop. Bridge connectors (Revolut, banking, etc.) compose by
// creating explicit feed instances per call site.
userManaged: true,
configSchema: {
type: 'object',
required: ['script'],
Comment on lines +45 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark the evaluate feed user-managed

Because this device connector declares runtime + requiredCapability, getBundledDeviceConnectors() includes every feed that is not userManaged, and ensureDeviceConnectorWired() auto-creates those feeds with config = NULL and next_run_at = NOW(). For a Chrome extension advertising browser.debugger, this evaluate feed will therefore be scheduled immediately without the required script, causing recurring failed browser-evaluate syncs (default every 6 hours) until someone manually edits or pauses the auto-wired feed. Mark this feed userManaged: true (as local.directory does for required per-feed config) or don't expose it as an auto-wired feed.

Useful? React with 👍 / 👎.

properties: {
url: {
type: 'string',
format: 'uri',
description: 'If set, navigate the tab here before evaluating.',
},
script: {
type: 'string',
description:
'JS expression evaluated with Runtime.evaluate(awaitPromise: true). Return value is JSON-serialised — keep it small.',
},
wait_for_selector: {
type: 'string',
description:
'CSS selector to wait for before evaluating (polled every 200ms via Runtime.evaluate).',
},
wait_timeout_ms: {
type: 'integer',
minimum: 100,
maximum: 60_000,
description: 'Timeout for wait_for_selector. Default 10000.',
},
open_in_new_tab: {
type: 'boolean',
description:
'Open a fresh background tab instead of driving the active tab. DEFAULT TRUE — opt out only when you specifically need the user-active tab.',
},
close_tab_after: {
type: 'boolean',
description:
'Close the tab when the run completes. Defaults to true when open_in_new_tab is true.',
},
},
},
eventKinds: {
browser_evaluate: {
description:
'One event per run with the JSON-serialised Runtime.evaluate result.',
metadataSchema: {
type: 'object',
required: ['source', 'origin_id'],
properties: {
source: { type: 'string', const: 'browser_evaluate' },
origin_id: { type: 'string' },
url: { type: 'string' },
title: { type: 'string' },
tab_id: { type: 'integer' },
},
},
},
},
},
},
};

async sync(_ctx: SyncContext): Promise<SyncResult> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Drop the unused sync parameter instead of underscore-prefixing it.

Use sync(): Promise<SyncResult> here to match repository lint/style rules for unused params.

Suggested change
-  async sync(_ctx: SyncContext): Promise<SyncResult> {
+  async sync(): Promise<SyncResult> {
     throw new Error(BRIDGE_ONLY);
   }

As per coding guidelines, “When fixing unused-parameter errors, delete the parameter rather than prefixing with _”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async sync(_ctx: SyncContext): Promise<SyncResult> {
async sync(): Promise<SyncResult> {
throw new Error(BRIDGE_ONLY);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/browser_evaluate.ts` at line 108, The method
declaration async sync(_ctx: SyncContext): Promise<SyncResult> uses an
underscore-prefixed unused parameter; remove the parameter to satisfy lint rules
by changing the signature to async sync(): Promise<SyncResult>. Update any
references inside the function (if present) that use _ctx (they should be
removed) and ensure the function still returns a valid SyncResult; keep the
function name sync and types SyncContext/SyncResult intact elsewhere.

throw new Error(BRIDGE_ONLY);
}

async execute(): Promise<ActionResult> {
throw new Error(BRIDGE_ONLY);
}
}
107 changes: 107 additions & 0 deletions packages/connectors/src/browser/fill_form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Browser Fill Form Connector — Owletto for Chrome only.
*
* Thin wrapper around browser.evaluate that bakes in a "fill these inputs
* by selector and dispatch the right input/change events" script.
*
* The extension's executor branch for `browser.fill_form` substitutes the
* canonical fill-form script when this connector_key is dispatched. The
* server-side definition just exposes the URL + fields config to the
* admin UI.
*
* Cloud-side `sync()` / `execute()` throw — actual work happens in the
* extension's service worker (lobu-ai/owletto: apps/chrome/executor.js).
*/

import {
type ActionResult,
type ConnectorDefinition,
ConnectorRuntime,
type SyncContext,
type SyncResult,
} from '@lobu/connector-sdk';

const BRIDGE_ONLY =
'browser.fill_form runs only on a worker advertising capability "browser.debugger" (Owletto for Chrome).';

export default class BrowserFillFormConnector extends ConnectorRuntime {
readonly definition: ConnectorDefinition = {
key: 'browser.fill_form',
name: 'Browser Fill Form',
description:
'Fills inputs on a page by CSS selector and dispatches input/change events. Returns the filled field count.',
version: '0.1.0',
faviconDomain: 'google.com',
requiredCapability: 'browser.debugger',
runtime: { platforms: ['chrome-extension'] },
authSchema: { methods: [{ type: 'none' }] },
feeds: {
fill: {
key: 'fill',
name: 'Fill form',
description:
'Sets values on input/textarea/select elements matched by CSS selector.',
// Required url + fields; instances are minted by composing bridge
// connectors, not auto-wired by device-reconcile.
userManaged: true,
configSchema: {
type: 'object',
required: ['url', 'fields'],
properties: {
url: {
type: 'string',
format: 'uri',
description: 'Page to load before filling.',
},
fields: {
type: 'object',
description:
'Map of CSS selector → value to set. e.g. { "#email": "x@y.com", "#submit": "click" } — the literal string "click" triggers a click instead of a value set.',
additionalProperties: { type: 'string' },
},
wait_for_selector: {
type: 'string',
description:
'CSS selector to wait for before filling (defaults to the first key of fields).',
},
wait_timeout_ms: {
type: 'integer',
minimum: 100,
maximum: 60_000,
},
submit_selector: {
type: 'string',
description:
'Optional selector to click after filling all fields (e.g. "button[type=submit]").',
},
},
},
eventKinds: {
form_filled: {
description:
'One event per run with the count of fields filled + whether submit was clicked.',
metadataSchema: {
type: 'object',
required: ['source', 'origin_id', 'url', 'filled_count'],
properties: {
source: { type: 'string', const: 'browser_fill_form' },
origin_id: { type: 'string' },
url: { type: 'string', format: 'uri' },
filled_count: { type: 'integer' },
submitted: { type: 'boolean' },
},
},
},
},
},
},
};

async sync(_ctx: SyncContext): Promise<SyncResult> {
throw new Error(BRIDGE_ONLY);
}
Comment on lines +100 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused sync parameter instead of underscore-prefixing it.

_ctx is unused; drop it to comply with repo rules.

Proposed fix
-  async sync(_ctx: SyncContext): Promise<SyncResult> {
+  async sync(): Promise<SyncResult> {
     throw new Error(BRIDGE_ONLY);
   }

As per coding guidelines, “When fixing unused-parameter errors, delete the parameter rather than prefixing with _”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/connectors/src/browser/fill_form.ts` around lines 97 - 99, The
function async sync currently declares an unused parameter `_ctx`; remove the
parameter from the signature (change async sync(_ctx: SyncContext):
Promise<SyncResult> to async sync(): Promise<SyncResult>) and ensure no
references to `_ctx` remain in the function body; keep the existing throw new
Error(BRIDGE_ONLY) behavior and update any local type annotations or interface
implementations if necessary to match the new no-argument signature.


async execute(): Promise<ActionResult> {
throw new Error(BRIDGE_ONLY);
}
}
Loading
Loading