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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"start": "node dist/cli.js",
"validate:skill": "node scripts/validate-skill.mjs openclaw/vibebrowser/SKILL.md",
"prepublishOnly": "npm run build",
"test": "npm run test:e2e:docs-contract && npm run test:e2e:relay-race && npm run test:e2e:relay-roundtrip && npm run test:e2e:relay-heartbeat-pong && npm run test:e2e:cli-relay && npm run test:e2e:cli-autospawn && npm run test:e2e:http && npm run test:e2e:uuid-only-auth && npm run test:e2e:tool-annotations && npm run test:e2e:tools-list-budget && npm run test:e2e:browser-cli && npm run test:e2e:cli-package && npm run test:e2e:devtools-flag",
"test:ci": "npm run test:e2e:docs-contract && npm run test:e2e:relay-race && npm run test:e2e:relay-roundtrip && npm run test:e2e:relay-heartbeat-pong && npm run test:e2e:cli-relay && npm run test:e2e:cli-autospawn && npm run test:e2e:http && npm run test:e2e:uuid-only-auth && npm run test:e2e:tool-annotations && npm run test:e2e:tools-list-budget && npm run test:e2e:browser-cli && npm run test:e2e:cli-package && npm run test:e2e:devtools-flag",
"test": "npm run test:e2e:docs-contract && npm run test:e2e:relay-race && npm run test:e2e:relay-roundtrip && npm run test:e2e:relay-heartbeat-pong && npm run test:e2e:cli-relay && npm run test:e2e:cli-autospawn && npm run test:e2e:http && npm run test:e2e:uuid-only-auth && npm run test:e2e:remote-lifecycle && npm run test:e2e:tool-annotations && npm run test:e2e:tools-list-budget && npm run test:e2e:browser-cli && npm run test:e2e:cli-package && npm run test:e2e:devtools-flag",
"test:ci": "npm run test:e2e:docs-contract && npm run test:e2e:relay-race && npm run test:e2e:relay-roundtrip && npm run test:e2e:relay-heartbeat-pong && npm run test:e2e:cli-relay && npm run test:e2e:cli-autospawn && npm run test:e2e:http && npm run test:e2e:uuid-only-auth && npm run test:e2e:remote-lifecycle && npm run test:e2e:tool-annotations && npm run test:e2e:tools-list-budget && npm run test:e2e:browser-cli && npm run test:e2e:cli-package && npm run test:e2e:devtools-flag",
"test:e2e:docs-contract": "node scripts/e2e-docs-contract.mjs",
"test:e2e:tool-annotations": "node scripts/e2e-tool-annotations.mjs",
"test:e2e:tools-list-budget": "node scripts/e2e-tools-list-startup-budget.mjs",
Expand All @@ -34,6 +34,7 @@
"test:e2e:cli-autospawn": "node scripts/e2e-cli-autospawn.mjs",
"test:e2e:http": "node scripts/e2e-http-streamable.mjs",
"test:e2e:uuid-only-auth": "node scripts/e2e-uuid-only-auth.mjs",
"test:e2e:remote-lifecycle": "node scripts/e2e-remote-lifecycle.mjs",
"test:e2e:devtools-flag": "node scripts/e2e-devtools-flag.mjs",
"test:e2e:cli-package": "node scripts/e2e-cli-package-smoke.mjs",
"test:e2e:browser-cli-live": "node scripts/e2e-browser-cli-live.mjs",
Expand Down
45 changes: 40 additions & 5 deletions scripts/e2e-browser-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { fileURLToPath } from 'node:url';
import { WebSocketServer } from 'ws';

const HOST = '127.0.0.1';
const REMOTE_UUID = 'browser-cli-test-uuid';
const REMOTE_UUID = '11111111-1111-4111-8111-111111111111';
const REDACTED_REMOTE_ID = '[redacted]';
const RELAY_PORT = await findFreePort();
const RELAY_URL = `ws://${HOST}:${RELAY_PORT}`;
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
Expand All @@ -28,6 +29,8 @@ let cliInvocation = null;
let delayToolResultMs = 0;
let missingPageIdMode = false;
let fillCompatibilityErrorMode = false;
let credentialErrorMode = false;
let credentialToolResultMode = false;

const ONE_PIXEL_PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5W6n8AAAAASUVORK5CYII=';
Expand Down Expand Up @@ -106,6 +109,27 @@ try {
}

if (message.type === 'call_tool') {
if (credentialToolResultMode) {
ws.send(JSON.stringify({
type: 'tool_result',
requestId: message.requestId,
data: {
success: false,
isError: true,
detail: `Relay rejected ${REMOTE_UUID.toUpperCase()}`,
content: [{ type: 'text', text: `Relay rejected ${REMOTE_UUID.toUpperCase()}` }],
},
}));
return;
}
if (credentialErrorMode) {
ws.send(JSON.stringify({
type: 'error',
requestId: message.requestId,
error: `Relay rejected ${REMOTE_UUID.toUpperCase()}`,
}));
return;
}
if (
missingPageIdMode
&& (message.data?.name === 'navigate_page' || message.data?.name === 'click')
Expand Down Expand Up @@ -159,14 +183,15 @@ try {
assert(status.ok === true, `status failed: ${JSON.stringify(status)}`);
assert(status.extensionConnected === true, `expected extension connected: ${JSON.stringify(status)}`);
assert(Number(status.toolCount) >= 10, `expected toolCount >= 10: ${JSON.stringify(status)}`);
assert(status.sessionId === REMOTE_UUID, `expected status sessionId=${REMOTE_UUID}: ${JSON.stringify(status)}`);
assert(status.sessionId === REDACTED_REMOTE_ID, `expected redacted remote session: ${JSON.stringify(status)}`);
assert(!JSON.stringify(status).includes(REMOTE_UUID), `status leaked remote UUID: ${JSON.stringify(status)}`);

const uuidStatus = await runCli(['status'], { remoteValue: REMOTE_UUID });
if (E2E_BROWSER_CLI_SOURCE === 'local') {
assert(uuidStatus.ok === true, `UUID-only remote should return structured status locally: ${JSON.stringify(uuidStatus)}`);
assert(uuidStatus.mode === 'remote', `UUID-only status should still be remote mode: ${JSON.stringify(uuidStatus)}`);
assert(uuidStatus.extensionConnected === false, `UUID-only remote without public relay should report disconnected extension locally: ${JSON.stringify(uuidStatus)}`);
assert(uuidStatus.sessionId === REMOTE_UUID, `UUID-only status should parse UUID as sessionId: ${JSON.stringify(uuidStatus)}`);
assert(uuidStatus.sessionId === REDACTED_REMOTE_ID, `UUID-only status should redact sessionId: ${JSON.stringify(uuidStatus)}`);
}

const envStatus = await runCli(['status'], {
Expand All @@ -175,11 +200,11 @@ try {
});
assert(envStatus.ok === true, `status via VIBE_REMOTE_URL failed: ${JSON.stringify(envStatus)}`);
assert(envStatus.extensionConnected === true, `VIBE_REMOTE_URL should connect extension: ${JSON.stringify(envStatus)}`);
assert(envStatus.sessionId === REMOTE_UUID, `VIBE_REMOTE_URL should target ${REMOTE_UUID}: ${JSON.stringify(envStatus)}`);
assert(envStatus.sessionId === REDACTED_REMOTE_ID, `VIBE_REMOTE_URL should redact sessionId: ${JSON.stringify(envStatus)}`);

const sessions = await runCli(['sessions']);
assert(Array.isArray(sessions.sessions) && sessions.sessions.length === 1, `sessions missing session list: ${JSON.stringify(sessions)}`);
assert(sessions.sessions[0].sessionId === REMOTE_UUID, `wrong remote session id: ${JSON.stringify(sessions)}`);
assert(sessions.sessions[0].sessionId === REDACTED_REMOTE_ID, `remote session id should be redacted: ${JSON.stringify(sessions)}`);

const tabs = await runCli(['tabs']);
assert(Array.isArray(tabs.pages) && tabs.pages.length === 2, `tabs missing pages: ${JSON.stringify(tabs)}`);
Expand Down Expand Up @@ -310,6 +335,16 @@ try {
assert(/"profile":\s*"user"/.test(missingPageMessage), `error payload should report user profile: ${missingPageMessage}`);
missingPageIdMode = false;

credentialErrorMode = true;
const redactedRelayError = await expectCliFailure(['tabs'], /\[redacted\]/);
assert(!redactedRelayError.toLowerCase().includes(REMOTE_UUID.toLowerCase()), `CLI error leaked remote UUID: ${redactedRelayError}`);
credentialErrorMode = false;

credentialToolResultMode = true;
const redactedToolResult = await expectCliFailure(['tabs'], /\[redacted\]/);
assert(!redactedToolResult.toLowerCase().includes(REMOTE_UUID.toLowerCase()), `CLI tool_result leaked remote UUID: ${redactedToolResult}`);
credentialToolResultMode = false;

console.log('browser cli e2e ok');
} finally {
if (wss) {
Expand Down
6 changes: 3 additions & 3 deletions scripts/e2e-cli-autospawn.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function attachFakeExtension(extensionPort, sessionId) {
// Fake remote relay (agent-facing protocol) for the --remote bypass test
// ---------------------------------------------------------------------------

const REMOTE_UUID = 'autospawn-remote-uuid';
const REMOTE_UUID = '55555555-5555-4555-8555-555555555555';

function startFakeRemoteRelay(port) {
const wss = new WebSocketServer({ host: HOST, port });
Expand Down Expand Up @@ -366,13 +366,13 @@ async function main() {
assert(data.ok === true, `D: remote status not ok: ${JSON.stringify(data)}`);
assert(data.mode === 'remote', `D: expected remote mode: ${JSON.stringify(data)}`);
assert(data.extensionConnected === true, `D: remote extension should be connected: ${JSON.stringify(data)}`);
assert(data.sessionId === REMOTE_UUID, `D: expected sessionId=${REMOTE_UUID}: ${JSON.stringify(data)}`);
assert(data.sessionId === '[redacted]', `D: expected redacted remote session: ${JSON.stringify(data)}`);

// Proof of bypass: no local relay was spawned → no local pidfile, and the
// local agent port stays closed.
assert(!existsSync(pidFileD), `D: --remote must NOT create a local pidfile, found ${pidFileD}`);
assert(!(await probePort(remoteAgentPort)), `D: --remote must NOT open a local relay agent port ${remoteAgentPort}`);
console.log(` D remote bypass: no local relay/pidfile, connected to remote ${REMOTE_UUID} ✓`);
console.log(' D remote bypass: no local relay/pidfile, connected to configured remote target ✓');

rmSync(stateDirD, { recursive: true, force: true });
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/e2e-devtools-flag.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const PACKAGE_ROOT = resolve(SCRIPT_DIR, '..');
const BROWSER_CLI = resolve(PACKAGE_ROOT, 'dist', 'browser-main.js');
const MCP_CLI = resolve(PACKAGE_ROOT, 'dist', 'cli.js');
const REMOTE_UUID = 'devtools-mode-test-uuid';
const REMOTE_UUID = '44444444-4444-4444-8444-444444444444';

function assert(condition, message) {
if (!condition) throw new Error(message);
Expand Down
34 changes: 31 additions & 3 deletions scripts/e2e-http-streamable.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ RESERVED_PORTS.add(RELAY_PORT);
const RELAY_PORT_B = await findFreePort(RESERVED_PORTS);
const RELAY_URL = `ws://${RELAY_HOST}:${RELAY_PORT}`;
const RELAY_URL_B = `ws://${RELAY_HOST}:${RELAY_PORT_B}`;
const REMOTE_UUID = 'test-http-relay-uuid';
const REMOTE_UUID_B = 'test-http-relay-uuid-b';
const REMOTE_UUID = '22222222-2222-4222-8222-222222222222';
const REMOTE_UUID_B = '33333333-3333-4333-8333-333333333333';
const SESSION_ID = REMOTE_UUID;
const SESSION_ID_B = REMOTE_UUID_B;
const MCP_URL = `http://${RELAY_HOST}:${MCP_HTTP_PORT}/mcp`;
Expand Down Expand Up @@ -314,9 +314,12 @@ async function main() {
throw new Error(`Expected set_remote text result: ${JSON.stringify(setRemoteResult)}`);
}
const setRemotePayload = JSON.parse(setRemoteText.text);
if (setRemotePayload.ok !== true || setRemotePayload.mode !== 'remote' || setRemotePayload.relayUrl !== RELAY_URL_B || setRemotePayload.uuid !== REMOTE_UUID_B) {
if (setRemotePayload.ok !== true || setRemotePayload.mode !== 'remote' || setRemotePayload.target !== '[redacted]') {
throw new Error(`Unexpected set_remote payload: ${JSON.stringify(setRemotePayload)}`);
}
if (setRemoteText.text.includes(REMOTE_UUID_B) || setRemoteText.text.includes(RELAY_URL_B)) {
throw new Error(`set_remote payload leaked relay credential: ${setRemoteText.text}`);
}

const listToolsBPromise = waitForWebSocketMessage(
extensionWsB,
Expand Down Expand Up @@ -379,6 +382,31 @@ async function main() {
throw new Error(`Unexpected relay B tool result: ${JSON.stringify(callResultB)}`);
}

const redactedErrorRequestPromise = waitForWebSocketMessage(
extensionWsB,
(message) => message.type === 'call_tool' && message.data?.name === 'echo_b',
);
const redactedErrorResultPromise = client.callTool({
name: 'echo_b',
arguments: { text: 'reject' },
});
const redactedErrorRequest = await redactedErrorRequestPromise;
extensionWsB.send(JSON.stringify({
type: 'tool_result',
requestId: redactedErrorRequest.requestId,
data: {
success: false,
isError: true,
detail: `Relay rejected ${REMOTE_UUID_B.toUpperCase()}`,
content: [{ type: 'text', text: `Relay rejected ${REMOTE_UUID_B.toUpperCase()}` }],
},
}));
const redactedErrorResult = await withTimeout(redactedErrorResultPromise, 'redacted relay error response');
const redactedErrorText = redactedErrorResult.content.find((item) => item.type === 'text')?.text ?? '';
if (redactedErrorResult.isError !== true || !redactedErrorText.includes('[redacted]') || redactedErrorText.toLowerCase().includes(REMOTE_UUID_B.toLowerCase())) {
throw new Error(`MCP error leaked relay credential: ${JSON.stringify(redactedErrorResult)}`);
}

await withTimeout(client.close(), 'MCP client close');
console.log('http e2e ok');
} finally {
Expand Down
10 changes: 8 additions & 2 deletions scripts/e2e-local-startup-fallback.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,12 @@ async function main() {
throw new Error(`Expected JSON response from set_remote with full URL, got: ${JSON.stringify(seedResult)}`);
}
const seedJson = JSON.parse(seedPayload.text);
if (!seedJson.ok || seedJson.uuid !== seedUuid || seedJson.relayUrl !== `ws://${HOST}:${remoteRelayPort}`) {
if (!seedJson.ok || seedJson.target !== '[redacted]') {
throw new Error(`Unexpected set_remote full URL payload: ${seedPayload.text}`);
}
if (seedPayload.text.includes(seedUuid) || seedPayload.text.includes(String(remoteRelayPort))) {
throw new Error(`set_remote full URL payload leaked relay credential: ${seedPayload.text}`);
}

const uuidResult = await client.callTool({
name: 'set_remote',
Expand All @@ -111,9 +114,12 @@ async function main() {
throw new Error(`Expected JSON response from set_remote with UUID, got: ${JSON.stringify(uuidResult)}`);
}
const uuidJson = JSON.parse(uuidPayload.text);
if (!uuidJson.ok || uuidJson.uuid !== switchedUuid || uuidJson.relayUrl !== `ws://${HOST}:${remoteRelayPort}`) {
if (!uuidJson.ok || uuidJson.target !== '[redacted]') {
throw new Error(`Unexpected set_remote UUID payload: ${uuidPayload.text}`);
}
if (uuidPayload.text.includes(switchedUuid) || uuidPayload.text.includes(String(remoteRelayPort))) {
throw new Error(`set_remote UUID payload leaked relay credential: ${uuidPayload.text}`);
}

console.log(PASS_MARKER);
} finally {
Expand Down
Loading
Loading