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
42 changes: 19 additions & 23 deletions app/domain-skills/harness-test/self-edit.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
# Harness self-edit test
# Legacy Harness Self-Edit Test

Notes from a run where the agent was asked to modify its own tools.
Historical notes from the pre-`browser-harness-js` helper/`TOOLS.json` harness.
This is not runtime guidance for app-spawned agents.

## Files that matter

- `helpers.js` — tool implementations. Every helper is dispatched from the `module.exports.dispatch.<name>` object at the bottom. Adding a key there is enough to register a new tool.
- `TOOLS.json` — tool schemas exposed to the LLM. Must be valid JSON; validate with `node -e 'JSON.parse(require("fs").readFileSync(path))'` before trusting it.
Browser Harness JS should cover normal browser automation. Treat harness edits
as an escape hatch only when the user explicitly asks for them, or when a
confirmed harness/runtime defect blocks the task.

## Adding a new tool

1. Write/append the handler in `helpers.js`. For trivial pure-JS tools you can inline the body directly in the dispatch map, e.g.
```js
reverse_string: (_ctx, a) => ({ input: str(a, 'text'), reversed: str(a, 'text').split('').reverse().join('') }),
```
2. Add a matching entry to `TOOLS.json` (insert *before* the `done` entry — that's the conventional last tool).
3. Both files hot-reload on the next tool call; no restart needed.
## Files that matter

## Arg validation helpers already in scope
- `helpers.js` — now a small compatibility bridge to `browser-harness-js`, not
a normal extension surface.
- `AGENTS.md` — the app-specific browser harness manual.
- `browser-harness-js/` — bundled runtime; app launches may replace it.

- `str(a, 'key')` — required string
- `num(a, 'key')` — required finite number
- `optNum(a, 'key', default)` — optional number
## Legacy notes

Use them so bad LLM args produce clean errors instead of silent `undefined`.
The old model used `helpers.js` implementations plus `TOOLS.json` schemas.
That model has been removed from the desktop runtime. Do not revive it for
ordinary browser tasks.

## Gotchas
## If an edit is unavoidable

- Edits take effect on the *very next* tool call, so if you call the new tool in the same batch as the file write it will 404. Sequence the calls.
- `shell` output is sometimes truncated in the middle of long lines — prefer `awk 'NR>=A && NR<=B'` over `sed -n` and avoid piping through `cat -n` when inspecting.
- `patch_file` only replaces the first occurrence; make `old_str` unique.
- Keep the patch minimal and task-scoped.
- Prefer fixing app source stock files over editing generated userData copies.
- Mention the edited file and reason in the final answer.
1 change: 1 addition & 0 deletions app/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = [
'dist/**',
'node_modules/**',
'docker/agent/dist/**',
'src/main/hl/stock/browser-harness-js/sdk/**',
// JS files were never linted under the old --ext .ts,.tsx flag
'**/*.js',
'**/*.mjs',
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/hl/engines/browserHarnessEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createHash } from 'node:crypto';
import path from 'node:path';
import type { SpawnContext } from './types';

function replPort(sessionId: string): string {
const n = createHash('sha256').update(sessionId).digest().readUInt16BE(0);
return String(18_000 + (n % 20_000));
}

export function applyBrowserHarnessEnv(ctx: SpawnContext, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const sdkDir = path.join(ctx.harnessDir, 'browser-harness-js', 'sdk');
env.PATH = env.PATH ? `${sdkDir}${path.delimiter}${env.PATH}` : sdkDir;
env.CDP_REPL_PORT = env.CDP_REPL_PORT ?? replPort(ctx.sessionId);
env.CDP_REPL_LOG = env.CDP_REPL_LOG ?? path.join(ctx.harnessDir, `browser-harness-js-${ctx.sessionId}.log`);
return env;
}
11 changes: 7 additions & 4 deletions app/src/main/hl/engines/browsercode/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
*
* BrowserCode inherits opencode's provider/model registry. This adapter uses
* BrowserCode only as the headless model runtime; native `browser_execute` is
* disabled so agents keep using this app's Electron-scoped helpers.js harness.
* disabled so agents use this app's Electron-scoped browser-harness-js runtime.
*/

import { register } from '../registry';
import { applyBrowserHarnessEnv } from '../browserHarnessEnv';
import { enrichedEnv } from '../pathEnrich';
import { runCliCapture } from '../cliSpawn';
import type {
Expand Down Expand Up @@ -154,7 +155,7 @@ const browserCodeAdapter: EngineAdapter = {
},
} : {}),
});
return env;
return applyBrowserHarnessEnv(ctx, env);
},

wrapPrompt(ctx: SpawnContext): string {
Expand All @@ -169,8 +170,10 @@ const browserCodeAdapter: EngineAdapter = {
'You are running inside Browser Use Desktop through BrowserCode.',
'You are driving a specific Chromium browser view on this machine.',
`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
'Do not use BrowserCode browser_execute. Read `./AGENTS.md` and use `./helpers.js` from this working directory for browser actions.',
'Always read `./helpers.js` before writing scripts. Edit it only if a helper is missing.',
'Do not use BrowserCode browser_execute. Read `./AGENTS.md` and use Browser Harness JS from this working directory for browser actions.',
"Use the `browser-harness-js` CLI for browser actions. Start with `browser-harness-js 'await connectToAssignedTarget()'`.",
'Do not use old helpers.js convenience APIs for browser control.',
'Do not edit harness files unless the user asks or a confirmed Browser Harness JS defect blocks the task.',
'For terminal commands, use BrowserCode/OpenCode\'s Bash tool and write commands for the current OS/shell it reports.',
'When producing files, save them to `./outputs/' + ctx.sessionId + '/` and mention the filename in the final answer.',
...attachmentLines,
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/hl/engines/claude-code/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import { mainLogger } from '../../../logger';
import { register } from '../registry';
import { applyBrowserHarnessEnv } from '../browserHarnessEnv';
import { enrichedEnv } from '../pathEnrich';
import { runCliCapture, spawnCli } from '../cliSpawn';
import type {
Expand Down Expand Up @@ -118,8 +119,10 @@ const claudeCodeAdapter: EngineAdapter = {
const lines: string[] = [
'You are driving a specific Chromium browser view on this machine.',
`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
'Read `./AGENTS.md` for how to drive the browser in this harness.',
'Always read `./helpers.js` before writing scripts — that is where the functions live. Edit it if a helper is missing.',
'Read `./AGENTS.md` for how to drive the browser with Browser Harness JS.',
"Use the `browser-harness-js` CLI for browser actions. Start with `browser-harness-js 'await connectToAssignedTarget()'`.",
'Do not use old helpers.js convenience APIs for browser control.',
'Do not edit harness files unless the user asks or a confirmed Browser Harness JS defect blocks the task.',
];
if (ctx.attachmentRefs.length > 0) {
lines.push('', 'The user attached these files for this task. Read each with your Read tool before acting:');
Expand Down Expand Up @@ -160,7 +163,7 @@ const claudeCodeAdapter: EngineAdapter = {
if (ctx.savedApiKey) env.ANTHROPIC_API_KEY = ctx.savedApiKey;
env.BU_TARGET_ID = ctx.targetId;
env.BU_CDP_PORT = String(ctx.cdpPort);
return env;
return applyBrowserHarnessEnv(ctx, env);
},

parseLine(line: string, ctx: ParseContext): ParseResult {
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/hl/engines/codex/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import os from 'node:os';
import path from 'node:path';
import { mainLogger } from '../../../logger';
import { register } from '../registry';
import { applyBrowserHarnessEnv } from '../browserHarnessEnv';
import { enrichedEnv } from '../pathEnrich';
import { runCliCapture } from '../cliSpawn';
import { runCodexDeviceLogin } from '../../../identity/codexLogin';
Expand Down Expand Up @@ -109,8 +110,10 @@ const codexAdapter: EngineAdapter = {
const lines: string[] = [
'You are driving a specific Chromium browser view on this machine.',
`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
'Read `./AGENTS.md` for how to drive the browser in this harness.',
'Always read `./helpers.js` before writing scripts — that is where the functions live. Edit it if a helper is missing.',
'Read `./AGENTS.md` for how to drive the browser with Browser Harness JS.',
"Use the `browser-harness-js` CLI for browser actions. Start with `browser-harness-js 'await connectToAssignedTarget()'`.",
'Do not use old helpers.js convenience APIs for browser control.',
'Do not edit harness files unless the user asks or a confirmed Browser Harness JS defect blocks the task.',
];
if (ctx.attachmentRefs.length > 0) {
lines.push('', 'The user attached these files for this task. Read each one before acting:');
Expand Down Expand Up @@ -160,7 +163,7 @@ const codexAdapter: EngineAdapter = {
}
env.BU_TARGET_ID = ctx.targetId;
env.BU_CDP_PORT = String(ctx.cdpPort);
return env;
return applyBrowserHarnessEnv(ctx, env);
},

parseLine(line: string, ctx: ParseContext): ParseResult {
Expand Down
8 changes: 3 additions & 5 deletions app/src/main/hl/engines/runEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import path from 'node:path';
import { engineLogger } from '../../logger';
import { resolveAuth, loadOpenAIKey, loadClaudeSubscriptionType, loadBrowserCodeConfig } from '../../identity/authStore';
import { helpersPath, toolsPath, skillPath } from '../harness';
import { helpersPath, skillPath } from '../harness';
import { get as getAdapter } from './registry';
import { spawnCli } from './cliSpawn';
import type {
Expand Down Expand Up @@ -260,12 +260,10 @@
const stdinMode: 'pipe' | 'ignore' = stdinPayload != null ? 'pipe' : 'ignore';

const harnessHelpersAbs = path.resolve(helpersPath());
const harnessToolsAbs = path.resolve(toolsPath());
const harnessSkillAbs = path.resolve(skillPath());

const watchedHarnessFiles: HarnessFileWatch[] = [
{ path: harnessHelpersAbs, basename: path.basename(harnessHelpersAbs), target: 'helpers', hash: hashFile(harnessHelpersAbs) ?? null },
{ path: harnessToolsAbs, basename: path.basename(harnessToolsAbs), target: 'tools', hash: hashFile(harnessToolsAbs) ?? null },
{ path: harnessSkillAbs, basename: path.basename(harnessSkillAbs), target: 'tools', hash: hashFile(harnessSkillAbs) ?? null },
];

Expand Down Expand Up @@ -312,7 +310,7 @@
const prevHash = file.hash;
file.hash = nextHash;
const action = prevHash === null && nextHash !== null ? 'write' : 'patch';
let bytes: number | null = null;

Check warning on line 313 in app/src/main/hl/engines/runEngine.ts

View workflow job for this annotation

GitHub Actions / Lint (TS)

The value assigned to 'bytes' is not used in subsequent statements
try {
bytes = fs.statSync(file.path).size;
} catch {
Expand Down Expand Up @@ -418,7 +416,7 @@
const extra: HlEvent[] = [];
if (isWrite) {
const action = /edit|patch/i.test(e.name) ? 'patch' : 'write';
if (resolved !== harnessHelpersAbs && resolved !== harnessToolsAbs && resolved !== harnessSkillAbs) {
if (resolved !== harnessHelpersAbs && resolved !== harnessSkillAbs) {
const m = resolved.match(skillPathRe);
if (m) extra.push({ type: 'skill_written', path: resolved, domain: m[1], topic: m[2], bytes: 0, action });
}
Expand All @@ -433,7 +431,7 @@
iter: 0,
pendingTools: new Map(),
harnessHelpersPath: harnessHelpersAbs,
harnessToolsPath: harnessToolsAbs,
harnessToolsPath: '',
harnessSkillPath: harnessSkillAbs,
};

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/hl/engines/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { HlEvent } from '../../../shared/session-schemas';
export interface SpawnContext {
/** User prompt to feed to the CLI. Adapters may wrap with seed/system text. */
prompt: string;
/** Absolute path to <userData>/harness/ (AGENTS.md + helpers.js live here). */
/** Absolute path to <userData>/harness/ (AGENTS.md + browser-harness-js live here). */
harnessDir: string;
/** App session id (used for naming uploads/outputs dirs + env injection). */
sessionId: string;
Expand Down
Loading
Loading