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
3 changes: 3 additions & 0 deletions .agents/skills/headless-computer-use/agents/openai.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
83 changes: 78 additions & 5 deletions .agents/skills/headless-computer-use/references/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions .agents/skills/headless-computer-use/scripts/headless-mcp.sh
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions .claude/skills/headless-computer-use
3 changes: 3 additions & 0 deletions .codex/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[mcp_servers.headless]
command = "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh"
args = []
9 changes: 9 additions & 0 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"headless": {
"command": "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh",
"args": [],
"env": {}
}
}
}
9 changes: 9 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"headless": {
"command": "./.agents/skills/headless-computer-use/scripts/headless-mcp.sh",
"args": [],
"env": {}
}
}
}
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 3 additions & 8 deletions apps/web/components/docs-markdown.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
113 changes: 113 additions & 0 deletions apps/web/scripts/validate-harness-onboarding.mjs
Original file line number Diff line number Diff line change
@@ -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");
20 changes: 12 additions & 8 deletions docs/roadmap/improvements-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,14 @@ 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`
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
Expand Down Expand Up @@ -417,8 +419,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);
Expand Down