From 80fdb12f6115d12e631dd7417118d865f4e9734d Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 20:50:27 +0530 Subject: [PATCH 1/2] docs: add portable harness onboarding --- .../headless-computer-use/agents/openai.yaml | 3 + .../headless-computer-use/references/setup.md | 83 ++++++++++++- .../scripts/headless-mcp.sh | 38 ++++++ .claude/skills/headless-computer-use | 1 + .codex/config.toml | 3 + .cursor/mcp.json | 9 ++ .mcp.json | 9 ++ CLAUDE.md | 6 +- apps/web/components/docs-markdown.ts | 11 +- apps/web/package.json | 2 +- .../scripts/validate-harness-onboarding.mjs | 113 ++++++++++++++++++ docs/roadmap/improvements-backlog.md | 31 +++-- 12 files changed, 279 insertions(+), 30 deletions(-) create mode 100755 .agents/skills/headless-computer-use/scripts/headless-mcp.sh create mode 120000 .claude/skills/headless-computer-use create mode 100644 .codex/config.toml create mode 100644 .cursor/mcp.json create mode 100644 .mcp.json create mode 100644 apps/web/scripts/validate-harness-onboarding.mjs diff --git a/.agents/skills/headless-computer-use/agents/openai.yaml b/.agents/skills/headless-computer-use/agents/openai.yaml index aa9c590..96aa37c 100644 --- a/.agents/skills/headless-computer-use/agents/openai.yaml +++ b/.agents/skills/headless-computer-use/agents/openai.yaml @@ -2,3 +2,6 @@ interface: display_name: "Headless Computer Use" short_description: "Launch and automate Headless browser workflows" default_prompt: "Use $headless-computer-use to launch the repository browser and validate a web flow end to end." + +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/headless-computer-use/references/setup.md b/.agents/skills/headless-computer-use/references/setup.md index 775c581..6902f48 100644 --- a/.agents/skills/headless-computer-use/references/setup.md +++ b/.agents/skills/headless-computer-use/references/setup.md @@ -119,15 +119,78 @@ a remote-debugging port to make it visible. ## Configure the stdio MCP adapter -Start the browser host first. Configure an MCP client to launch the native -`headless-mcp` executable over stdio: +Start the browser host first. The repository includes project-scoped configs +for the supported harnesses: + +- Claude Code reads `.mcp.json` and discovers the canonical skill through + `.claude/skills/headless-computer-use`. +- Cursor reads `.cursor/mcp.json`. +- Codex reads `.codex/config.toml` after the project is trusted. + +All three configs launch +`.agents/skills/headless-computer-use/scripts/headless-mcp.sh`. The launcher +selects a native repository build or an installed `headless-mcp` without +opening a TCP listener. Build Headless before opening a fresh harness session: + +```sh +./apps/headless/build.sh +./apps/headless/build/bin/headless start +``` + +If the executable is installed elsewhere, set an absolute executable path for +the harness process: + +```sh +HEADLESS_MCP_EXECUTABLE=/absolute/path/to/headless-mcp +``` + +Use these snippets when configuring another checkout or a global client. + +### Claude Code + +Save as `.mcp.json` in the project root and approve the project server when +Claude Code prompts: + +```json +{ + "mcpServers": { + "headless": { + "command": "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh", + "args": [], + "env": {} + } + } +} +``` + +### Cursor + +Save as `.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "headless": { + "command": "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh", + "args": [], + "env": {} + } + } +} +``` + +### Codex + +Save as `.codex/config.toml`: ```toml [mcp_servers.headless] -command = "/absolute/path/to/headless-mcp" +command = "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh" +args = [] ``` -For the Docker sandbox, configure the wrapper after `$SANDBOX start`: +For the Docker sandbox, start the named container and point any stdio MCP +client at the wrapper instead: ```toml [mcp_servers.headless] @@ -139,7 +202,17 @@ The server exposes one `headless` tool. Supply the normal CLI arguments without the executable name: ```json -{"argv":["--session","agent-qa","inspect","--context","actions","--task","find primary action"]} +{ + "argv": [ + "--session", + "agent-qa", + "inspect", + "--context", + "actions", + "--task", + "find primary action" + ] +} ``` Keep MCP on stdio locally or invoke it through SSH. Do not expose the Unix socket, diff --git a/.agents/skills/headless-computer-use/scripts/headless-mcp.sh b/.agents/skills/headless-computer-use/scripts/headless-mcp.sh new file mode 100755 index 0000000..6202de2 --- /dev/null +++ b/.agents/skills/headless-computer-use/scripts/headless-mcp.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../../../.." && pwd)" + +die() { + echo "headless mcp: $*" >&2 + exit 1 +} + +[ "$#" -eq 0 ] || die "this launcher does not accept arguments" + +if [ -n "${HEADLESS_MCP_EXECUTABLE:-}" ]; then + case "$HEADLESS_MCP_EXECUTABLE" in + /*) ;; + *) die "HEADLESS_MCP_EXECUTABLE must be an absolute path" ;; + esac + [ -f "$HEADLESS_MCP_EXECUTABLE" ] || die "override is not a regular file: $HEADLESS_MCP_EXECUTABLE" + [ -x "$HEADLESS_MCP_EXECUTABLE" ] || die "override is not executable: $HEADLESS_MCP_EXECUTABLE" + exec "$HEADLESS_MCP_EXECUTABLE" +fi + +for candidate in \ + "$REPO_ROOT/apps/headless/build/bin/headless-mcp" \ + "$REPO_ROOT/apps/headless/Headless.app/Contents/Resources/bin/headless-mcp" \ + "$REPO_ROOT/apps/headless/build/linux/headless-mcp" +do + if [ -f "$candidate" ] && [ -x "$candidate" ]; then + exec "$candidate" + fi +done + +if command -v headless-mcp >/dev/null 2>&1; then + exec headless-mcp +fi + +die "headless-mcp was not found; build Headless or set HEADLESS_MCP_EXECUTABLE" diff --git a/.claude/skills/headless-computer-use b/.claude/skills/headless-computer-use new file mode 120000 index 0000000..c9d2db3 --- /dev/null +++ b/.claude/skills/headless-computer-use @@ -0,0 +1 @@ +../../.agents/skills/headless-computer-use \ No newline at end of file diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..b3cfe1d --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,3 @@ +[mcp_servers.headless] +command = "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh" +args = [] diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..c9f6a22 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "headless": { + "command": "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh", + "args": [], + "env": {} + } + } +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..c9f6a22 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "headless": { + "command": "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh", + "args": [], + "env": {} + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 3a661ca..8e80e1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,8 +2,8 @@ Claude Code specific notes: -- The repository's browser-use skill lives at - `.agents/skills/headless-computer-use/SKILL.md` (not auto-discovered); - read it before driving Headless as a tool. +- The browser-use skill is auto-discovered at + `.claude/skills/headless-computer-use` and symlinked to the canonical + `.agents/skills/headless-computer-use` source. - `pnpm test:e2e:mac` opens real windows and mutates `com.headless.app` user defaults — don't run it in a background/headless session. diff --git a/apps/web/components/docs-markdown.ts b/apps/web/components/docs-markdown.ts index 784da53..e0ae683 100644 --- a/apps/web/components/docs-markdown.ts +++ b/apps/web/components/docs-markdown.ts @@ -1,3 +1,5 @@ +import cursorConfig from "../../../.cursor/mcp.json"; + export const pageMarkdown = `# Headless documentation Headless gives an agent a persistent browser through a small, safe CLI. Use it to test a page, capture evidence, and inspect a failure. @@ -54,11 +56,4 @@ Headless uses a private Unix socket, not a public DevTools port. Only HTTP(S) na - Linux: use the supplied Docker runtime or native Chromium. Ubuntu Snap Chromium is not supported for repeated navigation. - macOS: build with Xcode Command Line Tools and use the visible WKWebView host through the same CLI.`; -export const cursorMcpConfig = `{ - "mcpServers": { - "headless": { - "command": "ssh", - "args": ["hermes-vm", "headless-mcp"] - } - } -}`; +export const cursorMcpConfig = JSON.stringify(cursorConfig, null, 2); diff --git a/apps/web/package.json b/apps/web/package.json index 763f0d7..96243e4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint .", + "lint": "node scripts/validate-harness-onboarding.mjs && eslint .", "brand": "node scripts/render-brand.mjs" }, "dependencies": { diff --git a/apps/web/scripts/validate-harness-onboarding.mjs b/apps/web/scripts/validate-harness-onboarding.mjs new file mode 100644 index 0000000..13e85de --- /dev/null +++ b/apps/web/scripts/validate-harness-onboarding.mjs @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + chmod, + lstat, + mkdtemp, + readFile, + realpath, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const launcher = + "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh"; +const expectedJsonConfig = { + mcpServers: { + headless: { + command: launcher, + args: [], + env: {}, + }, + }, +}; + +async function read(relativePath) { + return readFile(join(repoRoot, relativePath), "utf8"); +} + +for (const configPath of [".mcp.json", ".cursor/mcp.json"]) { + const config = JSON.parse(await read(configPath)); + assert.deepEqual( + config, + expectedJsonConfig, + `${configPath} must use the repository launcher`, + ); +} + +const codexConfig = await read(".codex/config.toml"); +assert.equal( + codexConfig, + `[mcp_servers.headless]\ncommand = "${launcher}"\nargs = []\n`, + ".codex/config.toml must use the repository launcher", +); + +const claudeSkill = join(repoRoot, ".claude/skills/headless-computer-use"); +assert.equal( + (await lstat(claudeSkill)).isSymbolicLink(), + true, + "Claude skill mirror must be a symlink", +); +assert.equal( + await realpath(claudeSkill), + await realpath(join(repoRoot, ".agents/skills/headless-computer-use")), + "Claude skill mirror must target the canonical skill", +); + +const launcherScript = await read( + ".agents/skills/headless-computer-use/scripts/headless-mcp.sh", +); +assert.doesNotMatch( + launcherScript, + /(?:--remote-debugging-port|\bnc\b|\bsocat\b)/, +); + +const launcherPath = join(repoRoot, launcher.slice(2)); +const testDirectory = await mkdtemp(join(tmpdir(), "headless-mcp-launcher-")); +try { + const stubPath = join(testDirectory, "headless-mcp"); + await writeFile(stubPath, "#!/bin/sh\nprintf 'mcp-stdio-ready\\n'\n", "utf8"); + await chmod(stubPath, 0o700); + + const launched = spawnSync(launcherPath, [], { + encoding: "utf8", + env: { ...process.env, HEADLESS_MCP_EXECUTABLE: stubPath }, + }); + assert.equal(launched.status, 0, launched.stderr); + assert.equal(launched.stdout, "mcp-stdio-ready\n"); + + const relativeOverride = spawnSync(launcherPath, [], { + encoding: "utf8", + env: { ...process.env, HEADLESS_MCP_EXECUTABLE: "headless-mcp" }, + }); + assert.equal(relativeOverride.status, 1); + assert.match(relativeOverride.stderr, /must be an absolute path/); + + const unexpectedArgument = spawnSync(launcherPath, ["--listen"], { + encoding: "utf8", + }); + assert.equal(unexpectedArgument.status, 1); + assert.match(unexpectedArgument.stderr, /does not accept arguments/); +} finally { + await rm(testDirectory, { force: true, recursive: true }); +} + +const websiteDocs = await read("apps/web/components/docs-markdown.ts"); +assert.doesNotMatch(websiteDocs, /hermes-vm|"command":\s*"ssh"/); +assert.match(websiteDocs, /\.cursor\/mcp\.json/); + +const claudeInstructions = await read("CLAUDE.md"); +assert.match(claudeInstructions, /\.claude\/skills\/headless-computer-use/); +assert.doesNotMatch(claudeInstructions, /not auto-discovered/); + +const openAiMetadata = await read( + ".agents/skills/headless-computer-use/agents/openai.yaml", +); +assert.match(openAiMetadata, /default_prompt: "Use \$headless-computer-use /); +assert.match(openAiMetadata, /policy:\n allow_implicit_invocation: true/); + +console.log("Harness onboarding configuration is consistent"); diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index 3aebea1..a3ece7c 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -74,9 +74,9 @@ burning a core while silently refusing every agent. map is reset on each `snapshot()` (`HP/AgentRuntime.swift:376`), so a `--context summary` (max 8 elements) invalidates all refs from a prior `full`; the agent later gets a bare `ELEMENT_NOT_FOUND`. Meanwhile -`currentRegions` is *never* reset and grows for the page lifetime. ~~Decide the +`currentRegions` is _never_ reset and grows for the page lifetime. ~~Decide the contract (likely: refs from the latest inspection only — already the skill's -teaching), then (a) make the error say *why* ("ref expired; re-inspect"), +teaching), then (a) make the error say _why_ ("ref expired; re-inspect"), (b) reset regions consistently on navigation, (c) document in P1.md. Test: inspect-full → inspect-summary → click stale `@eN` asserts the new error.~~ **Done.** The contract is now explicit and asymmetric on purpose: `@eN` is @@ -115,6 +115,7 @@ message buffer scans each appended region once and amortizes prefix compaction; protocol coverage feeds it a 30 MiB message in the host's 8 KiB read chunks. **A9. Misc hardening (smaller, same phase).** ([#20](https://github.com/LockInTime/headless/issues/20)) + - ~~`ChromiumChildProcess.stop()` can busy-wait forever post-SIGKILL (`LinuxHost/BrowserProcess.swift:38`); bound it.~~ - ~~`SO_PEERCRED` hard-coded as `17` + hand-rolled `ucred` @@ -198,7 +199,7 @@ branch, and hid the backward-compatible no-op `--json` parser flag from help. **B5. `pruneToBudget` quality.** ([#25](https://github.com/LockInTime/headless/issues/25)) ~~Hand-rolled 2-pass fixed point (`HP/AgentRuntime.swift:348-352`), O(n²) re-encoding per trim, pop-largest- -*last*-element heuristic misses large mid-array items +_last_-element heuristic misses large mid-array items (`AgentRuntime.swift:367-369`), text-chop fallback untested. Rework with a size-estimating single pass; add unit tests in the jsdom suite.~~ **Done:** each candidate is measured once, largest entries are pruned regardless of @@ -269,14 +270,16 @@ oversized line / local-command rejection (`MCP/main.swift:64-66`).~~ **Done:** uses a private local socket server to verify a real browser-command round trip. **C4. Machine-accurate `capabilities`** ([#32](https://github.com/LockInTime/headless/issues/32)) — generate from `CommandName.allCases` -+ engine matrix (see B6) so agents can trust it. +and the engine matrix (see B6) so agents can trust it. -**C5. Harness onboarding [exists: skill content].** ([#33](https://github.com/LockInTime/headless/issues/33)) Root `AGENTS.md` + -`CLAUDE.md` (added with this doc set); mirror the skill into `.claude/skills/` -or symlink so Claude Code auto-discovers; ship `.mcp.json` example + per- -client snippets (Claude Code, Cursor, Codex TOML — replacing the site's -hardcoded `ssh hermes-vm` config, `apps/web/components/docs-markdown.ts:57-64`); -flesh out `agents/openai.yaml` beyond its 4-line stub or remove it. +**C5. Harness onboarding [exists: skill content].** ([#33](https://github.com/LockInTime/headless/issues/33)) ~~Root `AGENTS.md` + +`CLAUDE.md`; mirror the skill into `.claude/skills/` so Claude Code +auto-discovers it; ship project-scoped Claude Code, Cursor, and Codex MCP +configs; replace the site's hardcoded personal SSH target; and complete the +OpenAI skill metadata.~~ **Done:** the canonical skill is symlinked into +Claude's discovery path, all three clients use one repository-relative stdio +launcher, the setup guide includes native and Docker variants, and web lint +checks the configs for drift. **C6. Input fidelity (Phase 4).** ([#34](https://github.com/LockInTime/headless/issues/34)) Real CDP input on Linux (`Input.dispatchKeyEvent`/`dispatchMouseEvent`) behind the same verbs; today @@ -384,7 +387,7 @@ Owner-decided scope: package managers, no hosted service. ## §F — Website & docs (Phase 5) -- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site *is* live at +- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at `https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's GitHub integration, but nothing in the tree records that: no `vercel.json`, no deploy docs, no preview-URL comment on PRs, and the temporary @@ -414,8 +417,10 @@ Owner-decided scope: package managers, no hosted service. reduced-motion rule; Recharts (~150 KB) for 4 static bars. Also `scan-frame.tsx` hardcodes pixel bounds tied to a committed github.com screenshot (drift + trademark question). -- **F6. Fix the Cursor config snippet** ([#52](https://github.com/LockInTime/headless/issues/52)) — hardcodes `ssh hermes-vm` - (`components/docs-markdown.ts:57-64`), unusable by anyone else. +- **F6. Fix the Cursor config snippet** ([#52](https://github.com/LockInTime/headless/issues/52)) — ~~hardcodes `ssh hermes-vm` + (`components/docs-markdown.ts:57-64`), unusable by anyone else.~~ **Done:** + the website serializes the checked-in project config that uses the portable + repository launcher. - **F7. Docs debt in-repo:** ([#53](https://github.com/LockInTime/headless/issues/53)) README states P1/P2 features but there is no single command reference doc; P1.md should document the `@eN` invalidation contract (A5) and the shared-profile session model (architecture §11); From b8aa6d90d311da0bbd2acf35a39fd7bbdec3bdc9 Mon Sep 17 00:00:00 2001 From: SarthakWade Date: Wed, 12 Aug 2026 21:24:51 +0530 Subject: [PATCH 2/2] fix: clarify portable launcher environment --- .agents/skills/headless-computer-use/scripts/headless-mcp.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/headless-computer-use/scripts/headless-mcp.sh b/.agents/skills/headless-computer-use/scripts/headless-mcp.sh index 6202de2..7767eb9 100755 --- a/.agents/skills/headless-computer-use/scripts/headless-mcp.sh +++ b/.agents/skills/headless-computer-use/scripts/headless-mcp.sh @@ -1,8 +1,8 @@ #!/bin/sh set -eu -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -REPO_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../../../.." && pwd)" +SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" +REPO_ROOT="$(CDPATH='' cd -- "$SCRIPT_DIR/../../../.." && pwd)" die() { echo "headless mcp: $*" >&2