[GH-ISSUE #7677] [Bug] Android and browser CLI schemas omit enforced selector constraints #4598

Open
opened 2026-10-31 00:51:09 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 28, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7677

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

CLI

Observed behavior

The schemas returned by node scripts/rea.mjs inspect-android-class --schema --format json, inspect-android-method --schema --format json, capture-web-screenshot --schema --format json, and inspect-web-event-listeners --schema --format json permit empty selector strings. The Android mappingPath option also permits relative strings despite describing an absolute file path.

Ajv accepted six complete args/options selections against those CLI schemas. Executing the same selections with --json exited 1 with invalid_request / invalid_input, identifying the nonempty or absolute-path constraint. The corresponding schemas obtained from the actual stdio MCP server's tools/list rejected all six equivalent inputs. This is a CLI discovery mismatch; runtime refusal already includes useful field-specific correction details.

Reproduction

  1. Use the built baseline below with its locked dependencies and Node.js 24.22.1 first on PATH. From the checkout root, run this self-contained probe. It isolates state, installs inert launcher fixtures before the inherited PATH, and removes its temporary files afterward. No APK, listening endpoint, browser, device or analysis engine is needed.
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import Ajv2020 from 'ajv/dist/2020.js';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const work = mkdtempSync(join(tmpdir(), 'rea-selector-schema-'));
const env = { REA_LOG_LEVEL: 'silent' };
const bin = join(work, 'bin'); mkdirSync(bin);
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME',
  'XDG_DATA_HOME', 'XDG_STATE_HOME', 'TMPDIR']) {
  env[key] = join(work, key); mkdirSync(env[key]);
}
for (const name of ['npx', 'npm', 'bun', 'adb', 'jadx', 'jadx-mcp']) {
  writeFileSync(join(bin, name),
    '#!/bin/sh\nprintf "blocked\\n" >> "$HOME/blocked.log"\nexit 97\n',
    { mode: 0o755 });
}
env.PATH = bin + ':' + process.env.PATH;
const entry = resolve('scripts/rea.mjs');
const cli = args => {
  const p = spawnSync(process.execPath, [entry, ...args],
    { env, encoding: 'utf8', timeout: 20000 });
  assert.ifError(p.error);
  assert.equal(p.stderr, '');
  return { status: p.status, body: JSON.parse(p.stdout) };
};
const cases = [
  ['inspect-android-class', ['/nonexistent.apk', ''],
    { path: '/nonexistent.apk', className: '' }, {},
    { path: '/nonexistent.apk', class_name: '' }],
  ['inspect-android-method', ['/nonexistent.apk', 'Example', ''],
    { path: '/nonexistent.apk', className: 'Example', methodName: '' }, {},
    { path: '/nonexistent.apk', class_name: 'Example', method_name: '' }],
  ...['', 'mapping.txt'].map(mappingPath =>
    ['inspect-android-class', ['/nonexistent.apk', 'Example', '--mapping-path', mappingPath],
      { path: '/nonexistent.apk', className: 'Example' }, { mappingPath },
      { path: '/nonexistent.apk', class_name: 'Example', mapping_path: mappingPath }]),
  ['capture-web-screenshot', ['http://127.0.0.1:1', ''],
    { endpoint: 'http://127.0.0.1:1', targetId: '' }, {},
    { cdp_endpoint: 'http://127.0.0.1:1', target_id: '' }],
  ['inspect-web-event-listeners', ['http://127.0.0.1:1', 'T', ''],
    { endpoint: 'http://127.0.0.1:1', targetId: 'T', selector: '' }, {},
    { cdp_endpoint: 'http://127.0.0.1:1', target_id: 'T', selector: '' }],
];
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const transport = new StdioClientTransport({ command: process.execPath,
  args: [entry, 'mcp'], env, stderr: 'pipe' });
transport.stderr?.on('data', () => {});
const client = new Client({ name: 'selector-schema-probe', version: '1' });
try {
  await client.connect(transport);
  const tools = (await client.listTools()).tools;
  for (const [command, argv, args, options, input] of cases) {
    const schema = cli([command, '--schema', '--format', 'json']);
    assert.equal(schema.status, 0);
    const cliAccepts = ajv.compile(schema.body.args)(args)
      && ajv.compile(schema.body.options)(options);
    const name = command.replaceAll('-', '_');
    const mcpAccepts = ajv.compile(tools.find(t => t.name === name).inputSchema)(input);
    const reply = cli([command, ...argv, '--json']);
    assert.equal(cliAccepts, true); assert.equal(mcpAccepts, false);
    assert.equal(reply.status, 1); assert.equal(reply.body.code, 'invalid_request');
    console.log(JSON.stringify({ command, options, cliAccepts, mcpAccepts,
      exit: reply.status, issues: reply.body.details.issues }));
    assert.equal(existsSync(join(env.HOME, 'blocked.log')), false);
  }
} finally {
  await client.close(); await transport.close();
  rmSync(work, { recursive: true, force: true });
}
JS
  1. Each case prints cliAccepts:true, mcpAccepts:false, exit 1, and the field issues summarized below.

Each of the six cases reproduced once in the initial verification; the browser cases were then repeated with the valid HTTP endpoint shape above. The complete inline probe was also executed once and passed all assertions for all six cases. Initial browser requests used an invalid WebSocket endpoint and produced an additional endpoint issue; the HTTP reduction removes that confounder.

Expected behavior

Machine-readable CLI discovery should preserve the canonical nonempty selector and absolute mapping-path requirements, so these input shapes fail schema validation before invocation. The repository explicitly requires CLI translations to reuse canonical semantic fields and bounds and to check emitted schemas against parsing, including empty strings. Relevant policy and ownership references are listed under Evidence.

Keep the existing rejection of malformed selections and the useful field diagnostics. This expectation does not require successful analysis merely because a request passes shape validation.

Impact and scope

P3: the demonstrated defect is misleading discovery metadata for malformed selections that are correctly refused. A schema-driven form or wrapper can accept an input shape that the CLI immediately rejects. No valid-request failure, wrong successful result, mutation, data loss, or actual failed caller recovery was demonstrated.

The refusal names the constraint, allowing correction to a nonempty selection or an absolute mapping-file path. Verified scope is the four named CLI commands and six cases on one macOS host. Sibling commands, other platforms and successful provider analysis were not exercised. The MCP advertisement is a comparison control and correctly excludes these inputs.

REA version

Exact node scripts/rea.mjs --version stdout:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64. Existing built runtime and locked dependencies. No external provider version applies to these pre-execution validation checks.

Target and artifact context

A deliberately absent APK path and inert selector strings. Browser endpoint http://127.0.0.1:1 has the accepted endpoint shape but is never contacted because input validation rejects the empty selection first. HOME, XDG directories and TMPDIR are isolated; package-manager and provider launchers are replaced by fixtures that record and refuse invocation. No fixture launcher was invoked.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

The actual CLI schema properties contain type: "string" plus descriptions, with no minLength for className, methodName, targetId or selector. Android mappingPath similarly lacks minLength and any absolute-path pattern. The actual MCP properties have minLength: 1; mapping_path also has the host-absolute pattern.

All six CLI executions returned invalid_request / invalid_input, exit 1 and empty stderr. Reduced details.issues:

CLI selection Returned issue
empty className class_name, out_of_range, minimum: 1
empty methodName method_name, out_of_range, minimum: 1
empty mappingPath mapping_path, out_of_range, minimum: 1; also invalid_format, must be an absolute local filesystem path
mappingPath: "mapping.txt" mapping_path, invalid_format, must be an absolute local filesystem path
empty screenshot targetId target_id, out_of_range, minimum: 1
empty event-listener selector selector, out_of_range, minimum: 1

A separate actual stdio tools/call for inspect_android_class with {"path":"/nonexistent.apk","class_name":""} returned isError:true and invalid_request, with the message The arguments for MCP tool inspect_android_class do not match its advertised input schema.

Source references at the stated baseline:

  • Contract: docs/tool-design.md:339-346 requires canonical CLI semantic fields and schema/parser checks. tests/boundary/cli/cliInputSchemaRequired.test.ts:166-176,319-393,397-413 compares advertised constraints with parsing and the owning workflow for path, array and integer examples.
  • CLI declarations: src/cli/androidCommands.ts:65-73,207-245, src/cli/browserAdvancedCommands.ts:173-202, and src/cli/webRuntimeCommands.ts:78-108 independently declare the reported string fields.
  • Owning constraints: src/domain/android/androidAnalysis.ts:24-53, src/domain/localPath.ts:19-34, src/domain/webScreenshot.ts:38-44, src/domain/webRuntime.ts:33-38, and src/domain/webEventListeners.ts:12-16.
  • Refusal before provider execution: src/application/android/AndroidAnalysisService.ts:35-55, src/cli/browserAdvancedCommands.ts:195-202, and src/application/WebRuntimeService.ts:62-82.

Evidence provenance: observed public CLI and stdio MCP schemas/refusals, supplemented by source inspection. Local verification: reproduced. Reproduction completeness: complete for the stated discovery/validation mismatch, without successful provider execution.

Typed outcome or recovery expected

Advertise and enforce the same selector shape at CLI admission. Empty required selections should be excluded by the CLI schema; a supplied mapping path should satisfy its existing absolute-path requirement. Preserve field-specific invalid-input recovery and ordinary valid selections.

Analysis or suspected cause

Source-derived: the CLI independently declares weaker strings than the owning workflow contracts. The schema describes those weaker declarations, while later canonical validation rejects the same input. Reusing the existing owner constraints would address the mismatch; no repair was implemented or verified.

Additional context

Duplicate searches covered open and closed issues, open/closed/merged PRs, and issue comments using command names, selector/schema, empty values, minLength and mapping-path terms. No same-contract/same-closure report was found.

Open #7572 concerns a different native procedure field and missing correction details. Closed #5971 and #2180 repair Android/browser diagnostics; those diagnostics are present here, and #2180 explicitly separates advertised constraints from its closure. Closed #6467 and merged PR #6759 concern combined required choices, rather than these string bounds. Open #7437 and #7537, and closed #5493, concern MCP path contracts; the corresponding MCP schemas already expose the constraints in this verification.

No last-known-good revision or introducing commit was established, so this report makes no regression claim.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: The observed mismatch affects discovery metadata for malformed selections, while the CLI correctly refuses them and retains field-specific correction information. No consequential change to valid results was demonstrated.
  • Workaround status: available
  • Workaround basis: The returned issues identify the nonempty or absolute-path requirement, and the actual MCP schemas expose it before invocation. Correcting the selected shape avoids this specific validation failure; successful provider analysis was not tested.
Originally created by @coygeek on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7677 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area CLI ### Observed behavior The schemas returned by `node scripts/rea.mjs inspect-android-class --schema --format json`, `inspect-android-method --schema --format json`, `capture-web-screenshot --schema --format json`, and `inspect-web-event-listeners --schema --format json` permit empty selector strings. The Android `mappingPath` option also permits relative strings despite describing an absolute file path. Ajv accepted six complete args/options selections against those CLI schemas. Executing the same selections with `--json` exited 1 with `invalid_request` / `invalid_input`, identifying the nonempty or absolute-path constraint. The corresponding schemas obtained from the actual stdio MCP server's `tools/list` rejected all six equivalent inputs. This is a CLI discovery mismatch; runtime refusal already includes useful field-specific correction details. ### Reproduction 1. Use the built baseline below with its locked dependencies and Node.js 24.22.1 first on PATH. From the checkout root, run this self-contained probe. It isolates state, installs inert launcher fixtures before the inherited PATH, and removes its temporary files afterward. No APK, listening endpoint, browser, device or analysis engine is needed. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import Ajv2020 from 'ajv/dist/2020.js'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; const work = mkdtempSync(join(tmpdir(), 'rea-selector-schema-')); const env = { REA_LOG_LEVEL: 'silent' }; const bin = join(work, 'bin'); mkdirSync(bin); for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME', 'TMPDIR']) { env[key] = join(work, key); mkdirSync(env[key]); } for (const name of ['npx', 'npm', 'bun', 'adb', 'jadx', 'jadx-mcp']) { writeFileSync(join(bin, name), '#!/bin/sh\nprintf "blocked\\n" >> "$HOME/blocked.log"\nexit 97\n', { mode: 0o755 }); } env.PATH = bin + ':' + process.env.PATH; const entry = resolve('scripts/rea.mjs'); const cli = args => { const p = spawnSync(process.execPath, [entry, ...args], { env, encoding: 'utf8', timeout: 20000 }); assert.ifError(p.error); assert.equal(p.stderr, ''); return { status: p.status, body: JSON.parse(p.stdout) }; }; const cases = [ ['inspect-android-class', ['/nonexistent.apk', ''], { path: '/nonexistent.apk', className: '' }, {}, { path: '/nonexistent.apk', class_name: '' }], ['inspect-android-method', ['/nonexistent.apk', 'Example', ''], { path: '/nonexistent.apk', className: 'Example', methodName: '' }, {}, { path: '/nonexistent.apk', class_name: 'Example', method_name: '' }], ...['', 'mapping.txt'].map(mappingPath => ['inspect-android-class', ['/nonexistent.apk', 'Example', '--mapping-path', mappingPath], { path: '/nonexistent.apk', className: 'Example' }, { mappingPath }, { path: '/nonexistent.apk', class_name: 'Example', mapping_path: mappingPath }]), ['capture-web-screenshot', ['http://127.0.0.1:1', ''], { endpoint: 'http://127.0.0.1:1', targetId: '' }, {}, { cdp_endpoint: 'http://127.0.0.1:1', target_id: '' }], ['inspect-web-event-listeners', ['http://127.0.0.1:1', 'T', ''], { endpoint: 'http://127.0.0.1:1', targetId: 'T', selector: '' }, {}, { cdp_endpoint: 'http://127.0.0.1:1', target_id: 'T', selector: '' }], ]; const ajv = new Ajv2020({ strict: false, validateFormats: false }); const transport = new StdioClientTransport({ command: process.execPath, args: [entry, 'mcp'], env, stderr: 'pipe' }); transport.stderr?.on('data', () => {}); const client = new Client({ name: 'selector-schema-probe', version: '1' }); try { await client.connect(transport); const tools = (await client.listTools()).tools; for (const [command, argv, args, options, input] of cases) { const schema = cli([command, '--schema', '--format', 'json']); assert.equal(schema.status, 0); const cliAccepts = ajv.compile(schema.body.args)(args) && ajv.compile(schema.body.options)(options); const name = command.replaceAll('-', '_'); const mcpAccepts = ajv.compile(tools.find(t => t.name === name).inputSchema)(input); const reply = cli([command, ...argv, '--json']); assert.equal(cliAccepts, true); assert.equal(mcpAccepts, false); assert.equal(reply.status, 1); assert.equal(reply.body.code, 'invalid_request'); console.log(JSON.stringify({ command, options, cliAccepts, mcpAccepts, exit: reply.status, issues: reply.body.details.issues })); assert.equal(existsSync(join(env.HOME, 'blocked.log')), false); } } finally { await client.close(); await transport.close(); rmSync(work, { recursive: true, force: true }); } JS ``` 2. Each case prints `cliAccepts:true`, `mcpAccepts:false`, exit 1, and the field issues summarized below. Each of the six cases reproduced once in the initial verification; the browser cases were then repeated with the valid HTTP endpoint shape above. The complete inline probe was also executed once and passed all assertions for all six cases. Initial browser requests used an invalid WebSocket endpoint and produced an additional endpoint issue; the HTTP reduction removes that confounder. ### Expected behavior Machine-readable CLI discovery should preserve the canonical nonempty selector and absolute mapping-path requirements, so these input shapes fail schema validation before invocation. The repository explicitly requires CLI translations to reuse canonical semantic fields and bounds and to check emitted schemas against parsing, including empty strings. Relevant policy and ownership references are listed under Evidence. Keep the existing rejection of malformed selections and the useful field diagnostics. This expectation does not require successful analysis merely because a request passes shape validation. ### Impact and scope P3: the demonstrated defect is misleading discovery metadata for malformed selections that are correctly refused. A schema-driven form or wrapper can accept an input shape that the CLI immediately rejects. No valid-request failure, wrong successful result, mutation, data loss, or actual failed caller recovery was demonstrated. The refusal names the constraint, allowing correction to a nonempty selection or an absolute mapping-file path. Verified scope is the four named CLI commands and six cases on one macOS host. Sibling commands, other platforms and successful provider analysis were not exercised. The MCP advertisement is a comparison control and correctly excludes these inputs. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64. Existing built runtime and locked dependencies. No external provider version applies to these pre-execution validation checks. ### Target and artifact context A deliberately absent APK path and inert selector strings. Browser endpoint `http://127.0.0.1:1` has the accepted endpoint shape but is never contacted because input validation rejects the empty selection first. HOME, XDG directories and TMPDIR are isolated; package-manager and provider launchers are replaced by fixtures that record and refuse invocation. No fixture launcher was invoked. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence The actual CLI schema properties contain `type: "string"` plus descriptions, with no `minLength` for `className`, `methodName`, `targetId` or `selector`. Android `mappingPath` similarly lacks `minLength` and any absolute-path pattern. The actual MCP properties have `minLength: 1`; `mapping_path` also has the host-absolute pattern. All six CLI executions returned `invalid_request` / `invalid_input`, exit 1 and empty stderr. Reduced `details.issues`: | CLI selection | Returned issue | | --- | --- | | empty `className` | `class_name`, `out_of_range`, `minimum: 1` | | empty `methodName` | `method_name`, `out_of_range`, `minimum: 1` | | empty `mappingPath` | `mapping_path`, `out_of_range`, `minimum: 1`; also `invalid_format`, `must be an absolute local filesystem path` | | `mappingPath: "mapping.txt"` | `mapping_path`, `invalid_format`, `must be an absolute local filesystem path` | | empty screenshot `targetId` | `target_id`, `out_of_range`, `minimum: 1` | | empty event-listener `selector` | `selector`, `out_of_range`, `minimum: 1` | A separate actual stdio `tools/call` for `inspect_android_class` with `{"path":"/nonexistent.apk","class_name":""}` returned `isError:true` and `invalid_request`, with the message `The arguments for MCP tool inspect_android_class do not match its advertised input schema.` Source references at the stated baseline: - Contract: `docs/tool-design.md:339-346` requires canonical CLI semantic fields and schema/parser checks. `tests/boundary/cli/cliInputSchemaRequired.test.ts:166-176,319-393,397-413` compares advertised constraints with parsing and the owning workflow for path, array and integer examples. - CLI declarations: `src/cli/androidCommands.ts:65-73,207-245`, `src/cli/browserAdvancedCommands.ts:173-202`, and `src/cli/webRuntimeCommands.ts:78-108` independently declare the reported string fields. - Owning constraints: `src/domain/android/androidAnalysis.ts:24-53`, `src/domain/localPath.ts:19-34`, `src/domain/webScreenshot.ts:38-44`, `src/domain/webRuntime.ts:33-38`, and `src/domain/webEventListeners.ts:12-16`. - Refusal before provider execution: `src/application/android/AndroidAnalysisService.ts:35-55`, `src/cli/browserAdvancedCommands.ts:195-202`, and `src/application/WebRuntimeService.ts:62-82`. Evidence provenance: observed public CLI and stdio MCP schemas/refusals, supplemented by source inspection. Local verification: reproduced. Reproduction completeness: complete for the stated discovery/validation mismatch, without successful provider execution. ### Typed outcome or recovery expected Advertise and enforce the same selector shape at CLI admission. Empty required selections should be excluded by the CLI schema; a supplied mapping path should satisfy its existing absolute-path requirement. Preserve field-specific invalid-input recovery and ordinary valid selections. ### Analysis or suspected cause Source-derived: the CLI independently declares weaker strings than the owning workflow contracts. The schema describes those weaker declarations, while later canonical validation rejects the same input. Reusing the existing owner constraints would address the mismatch; no repair was implemented or verified. ### Additional context Duplicate searches covered open and closed issues, open/closed/merged PRs, and issue comments using command names, selector/schema, empty values, minLength and mapping-path terms. No same-contract/same-closure report was found. Open [#7572](https://github.com/morluto/rea/issues/7572) concerns a different native procedure field and missing correction details. Closed [#5971](https://github.com/morluto/rea/issues/5971) and [#2180](https://github.com/morluto/rea/issues/2180) repair Android/browser diagnostics; those diagnostics are present here, and #2180 explicitly separates advertised constraints from its closure. Closed [#6467](https://github.com/morluto/rea/issues/6467) and merged [PR #6759](https://github.com/morluto/rea/pull/6759) concern combined required choices, rather than these string bounds. Open [#7437](https://github.com/morluto/rea/issues/7437) and [#7537](https://github.com/morluto/rea/issues/7537), and closed [#5493](https://github.com/morluto/rea/issues/5493), concern MCP path contracts; the corresponding MCP schemas already expose the constraints in this verification. No last-known-good revision or introducing commit was established, so this report makes no regression claim. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: The observed mismatch affects discovery metadata for malformed selections, while the CLI correctly refuses them and retains field-specific correction information. No consequential change to valid results was demonstrated. - Workaround status: available - Workaround basis: The returned issues identify the nonempty or absolute-path requirement, and the actual MCP schemas expose it before invocation. Correcting the selected shape avoids this specific validation failure; successful provider analysis was not tested.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4598
No description provided.