[GH-ISSUE #4637] [Bug] list-frida-devices JSON uses deviceId instead of device_id #2418

Closed
opened 2026-10-17 21:04:22 +00:00 by bsakaguchi · 1 comment
Owner

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

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

node scripts/rea.mjs list-frida-devices --json exits 0 but emits each device identifier as deviceId, with no device_id. The production MCP tool list_frida_devices returns the same host inventory with device_id. The CLI result fails the named caller-facing output schema, while MCP passes it. This reproduced on both tested builds.

Reproduction

  1. Use either tested build below with its dependencies installed and a working optional Frida Node binding. For the source build, I ran npm ci and npm run build:cached using the repository-pinned Node and npm. For the release, I installed the exact published package into a disposable prefix and ran its Frida package's node scripts/install.js because npm had skipped that optional dependency's install script.
  2. From the REA package or built checkout root, run node scripts/rea.mjs list-frida-devices --json. The command returned three devices with deviceId in 2/2 real runs per build. No process attachment, script execution, or remote endpoint is required.
  3. The existing CLI provider seam gives a deterministic one-device case. Run this from the same root:
node --input-type=module <<'JS'
import { createCli } from './dist/cli.js';
import { FridaInstrumentationManager } from './dist/frida/FridaInstrumentationManager.js';
import { FRIDA_TOOL_CONTRACTS } from './dist/contracts/fridaToolContracts.js';
const provider = new FridaInstrumentationManager();
provider.listDevices = async () => ({ ok: true, value: {
  devices: [{ deviceId: 'fixture-local', name: 'Fixture', type: 'local' }],
  cleanupError: null
} });
let stdout = '';
await createCli({}, undefined, provider).serve(['list-frida-devices', '--json'], {
  env: {}, exit: code => { process.exitCode = code; },
  stdout: text => { stdout += text; }
});
const schema = FRIDA_TOOL_CONTRACTS.find(x => x.name === 'list_frida_devices').outputSchema;
console.log(JSON.stringify(JSON.parse(stdout)));
console.log('schema_valid:', schema.safeParse(JSON.parse(stdout)).success);
JS
  1. For the production MCP comparison, run this from the same root. It starts the stdio entry point, makes only the device-list request, and closes the client afterward:
node --input-type=module <<'JS'
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { FRIDA_TOOL_CONTRACTS } from './dist/contracts/fridaToolContracts.js';
const client = new Client({ name: 'device-field-comparison', version: '1' });
try {
  await client.connect(new StdioClientTransport({
    command: process.execPath, args: ['scripts/rea.mjs', 'mcp'],
    env: process.env, stderr: 'pipe'
  }));
  const result = await client.callTool({ name: 'list_frida_devices', arguments: {} });
  const schema = FRIDA_TOOL_CONTRACTS.find(x => x.name === 'list_frida_devices').outputSchema;
  console.log(JSON.stringify(result.structuredContent));
  console.log('schema_valid:', schema.safeParse(result.structuredContent).success);
} finally { await client.close(); }
JS

All REA execution used an isolated HOME with client configuration overrides cleared. The synthetic one-device case failed on each build; replacing its device array with [] passed the same schema on each build.

Expected behavior

The CLI should expose each device as device_id, name, and type, with top-level cleanup_error, matching FRIDA_TOOL_CONTRACTS and MCP. docs/cli.md:3-5 says the CLI uses the same application workflows and evidence contracts as MCP, and src/cliCommandNames.ts maps this command to list_frida_devices.

Impact and scope

A caller reading devices[i].device_id receives undefined, and a strict consumer of the shared schema rejects a successful CLI result. The identifier remains available under deviceId. The failure affected every nonempty real CLI result in the bounded sample above. Other platforms, caller-selected remote endpoints, and cleanup-failure paths were not exercised.

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Successful CLI JSON omits the required identifier key and fails the shared result schema, breaking consumers that rely on that contract.
  • Workaround status: available
  • Workaround basis: Production MCP returned the same host inventory with canonical identifiers and passed the output schema on both builds. CLI-specific callers can also read the existing deviceId key.

REA version

Published npm rea-agents@6.5.0; fresh main source at 71929ed37cdc560b6b8aa62b5f7182e417c8a850 (same package version).

Runtime and platform

Node.js v24.18.0; npm 11.16.0; macOS 27.0.1 arm64; optional Frida Node binding 17.23.2.

Target and artifact context

Host device discovery only. Both inventories contained the built-in Local System, Local Socket, and GDB Remote Stub entries. The synthetic fixture uses one local device. No binary or process target was selected.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

  • Expected source: src/contracts/fridaToolContracts.ts:74-84 defines the strict list_frida_devices output; the CLI guide and command mapping cited above establish the shared operation.
  • Failure source: Executed CLI and MCP requests above on both builds; src/cli/fridaCommands.ts:123-126 returns the internal device array unchanged.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The relevant local-device entries from real output were:

{"cli":{"deviceId":"local","name":"Local System","type":"local"},"mcp":{"device_id":"local","name":"Local System","type":"local"}}

The deterministic CLI case printed:

{"devices":[{"deviceId":"fixture-local","name":"Fixture","type":"local"}],"cleanup_error":null}
schema_valid: false

The schema rejected missing devices[0].device_id and unexpected deviceId. The production MCP request returned a successful response with the following structuredContent, which passed the same schema on each build:

{"devices":[{"device_id":"local","name":"Local System","type":"local"},{"device_id":"socket","name":"Local Socket","type":"remote"},{"device_id":"barebone","name":"GDB Remote Stub","type":"remote"}],"cleanup_error":null}

The empty-device control returned {"devices":[],"cleanup_error":null} and passed. The release tag's contract, CLI projection, and shared-contract documentation also contain the same relevant source behavior.

Typed outcome or recovery expected

With the same one-device observation, the successful CLI result should contain {"devices":[{"device_id":"fixture-local","name":"Fixture","type":"local"}],"cleanup_error":null} and pass the canonical schema. Preserve identifier values, names, types, and cleanup status; keep the empty-list control and MCP comparison schema-valid.

Analysis or suspected cause

Source-derived reasoning: src/server/registerFridaTools.ts:50-55 explicitly maps internal deviceId to public device_id, while the CLI returns the internal device array unchanged. This explains the observed result-projection difference; it does not establish a device-discovery failure.

Additional context

The regression range is unknown. The report concerns successful nonempty device-list output only.

Originally created by @coygeek on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4637 ### 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 `node scripts/rea.mjs list-frida-devices --json` exits 0 but emits each device identifier as `deviceId`, with no `device_id`. The production MCP tool `list_frida_devices` returns the same host inventory with `device_id`. The CLI result fails the named caller-facing output schema, while MCP passes it. This reproduced on both tested builds. ### Reproduction 1. Use either tested build below with its dependencies installed and a working optional Frida Node binding. For the source build, I ran `npm ci` and `npm run build:cached` using the repository-pinned Node and npm. For the release, I installed the exact published package into a disposable prefix and ran its Frida package's `node scripts/install.js` because npm had skipped that optional dependency's install script. 2. From the REA package or built checkout root, run `node scripts/rea.mjs list-frida-devices --json`. The command returned three devices with `deviceId` in 2/2 real runs per build. No process attachment, script execution, or remote endpoint is required. 3. The existing CLI provider seam gives a deterministic one-device case. Run this from the same root: ```sh node --input-type=module <<'JS' import { createCli } from './dist/cli.js'; import { FridaInstrumentationManager } from './dist/frida/FridaInstrumentationManager.js'; import { FRIDA_TOOL_CONTRACTS } from './dist/contracts/fridaToolContracts.js'; const provider = new FridaInstrumentationManager(); provider.listDevices = async () => ({ ok: true, value: { devices: [{ deviceId: 'fixture-local', name: 'Fixture', type: 'local' }], cleanupError: null } }); let stdout = ''; await createCli({}, undefined, provider).serve(['list-frida-devices', '--json'], { env: {}, exit: code => { process.exitCode = code; }, stdout: text => { stdout += text; } }); const schema = FRIDA_TOOL_CONTRACTS.find(x => x.name === 'list_frida_devices').outputSchema; console.log(JSON.stringify(JSON.parse(stdout))); console.log('schema_valid:', schema.safeParse(JSON.parse(stdout)).success); JS ``` 4. For the production MCP comparison, run this from the same root. It starts the stdio entry point, makes only the device-list request, and closes the client afterward: ```sh node --input-type=module <<'JS' import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { FRIDA_TOOL_CONTRACTS } from './dist/contracts/fridaToolContracts.js'; const client = new Client({ name: 'device-field-comparison', version: '1' }); try { await client.connect(new StdioClientTransport({ command: process.execPath, args: ['scripts/rea.mjs', 'mcp'], env: process.env, stderr: 'pipe' })); const result = await client.callTool({ name: 'list_frida_devices', arguments: {} }); const schema = FRIDA_TOOL_CONTRACTS.find(x => x.name === 'list_frida_devices').outputSchema; console.log(JSON.stringify(result.structuredContent)); console.log('schema_valid:', schema.safeParse(result.structuredContent).success); } finally { await client.close(); } JS ``` All REA execution used an isolated HOME with client configuration overrides cleared. The synthetic one-device case failed on each build; replacing its device array with `[]` passed the same schema on each build. ### Expected behavior The CLI should expose each device as `device_id`, `name`, and `type`, with top-level `cleanup_error`, matching `FRIDA_TOOL_CONTRACTS` and MCP. `docs/cli.md:3-5` says the CLI uses the same application workflows and evidence contracts as MCP, and `src/cliCommandNames.ts` maps this command to `list_frida_devices`. ### Impact and scope A caller reading `devices[i].device_id` receives `undefined`, and a strict consumer of the shared schema rejects a successful CLI result. The identifier remains available under `deviceId`. The failure affected every nonempty real CLI result in the bounded sample above. Other platforms, caller-selected remote endpoints, and cleanup-failure paths were not exercised. - Impact level: P2 - Assessment status: supported - Impact basis: Successful CLI JSON omits the required identifier key and fails the shared result schema, breaking consumers that rely on that contract. - Workaround status: available - Workaround basis: Production MCP returned the same host inventory with canonical identifiers and passed the output schema on both builds. CLI-specific callers can also read the existing `deviceId` key. ### REA version Published npm `rea-agents@6.5.0`; fresh `main` source at `71929ed37cdc560b6b8aa62b5f7182e417c8a850` (same package version). ### Runtime and platform Node.js v24.18.0; npm 11.16.0; macOS 27.0.1 arm64; optional Frida Node binding 17.23.2. ### Target and artifact context Host device discovery only. Both inventories contained the built-in Local System, Local Socket, and GDB Remote Stub entries. The synthetic fixture uses one local device. No binary or process target was selected. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence - Expected source: `src/contracts/fridaToolContracts.ts:74-84` defines the strict `list_frida_devices` output; the CLI guide and command mapping cited above establish the shared operation. - Failure source: Executed CLI and MCP requests above on both builds; `src/cli/fridaCommands.ts:123-126` returns the internal device array unchanged. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The relevant local-device entries from real output were: ```json {"cli":{"deviceId":"local","name":"Local System","type":"local"},"mcp":{"device_id":"local","name":"Local System","type":"local"}} ``` The deterministic CLI case printed: ```text {"devices":[{"deviceId":"fixture-local","name":"Fixture","type":"local"}],"cleanup_error":null} schema_valid: false ``` The schema rejected missing `devices[0].device_id` and unexpected `deviceId`. The production MCP request returned a successful response with the following `structuredContent`, which passed the same schema on each build: ```json {"devices":[{"device_id":"local","name":"Local System","type":"local"},{"device_id":"socket","name":"Local Socket","type":"remote"},{"device_id":"barebone","name":"GDB Remote Stub","type":"remote"}],"cleanup_error":null} ``` The empty-device control returned `{"devices":[],"cleanup_error":null}` and passed. The release tag's contract, CLI projection, and shared-contract documentation also contain the same relevant source behavior. ### Typed outcome or recovery expected With the same one-device observation, the successful CLI result should contain `{"devices":[{"device_id":"fixture-local","name":"Fixture","type":"local"}],"cleanup_error":null}` and pass the canonical schema. Preserve identifier values, names, types, and cleanup status; keep the empty-list control and MCP comparison schema-valid. ### Analysis or suspected cause Source-derived reasoning: `src/server/registerFridaTools.ts:50-55` explicitly maps internal `deviceId` to public `device_id`, while the CLI returns the internal device array unchanged. This explains the observed result-projection difference; it does not establish a device-discovery failure. ### Additional context The regression range is unknown. The report concerns successful nonempty device-list output only.
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Fixed on main in 4cbcead46.

Root cause: the shared workflow passed provider DTOs through, leaving CLI and MCP to project them independently. Nonempty CLI device results leaked deviceId. The application workflow now owns the complete device/process result shape, typed against each named output contract. Both adapters consume it directly. Projection happens before CLI cleanup, so retained partial observations use the same canonical keys. The adjacent process-list mappings were already correct, but their duplicated interpretation is removed too.

Validation on pinned Node 24.18.0/npm 11.16.0:

  • New boundary cases first failed in four places: two nonempty device inventories and device/process observations retained after final cleanup failure. Empty-device and successful-process controls passed.
  • Final focused run: 103 tests passed across five files, covering service/manager, CLI, MCP discovery schemas and MCP lifecycle behavior. npm run check and npm run docs:check passed.
  • Real Frida 17.23.2, macOS arm64: compiled CLI and production stdio MCP each enumerated Local System, Local Socket and GDB Remote Stub. Before: CLI schema rejected the result, MCP passed. After: both passed, with the same identifiers, names, types, order and cleanup status. No target attachment, spawn or instrumentation was performed. Remote endpoint and Linux/Windows coverage remain unverified; cleanup failures and process inventories use controlled provider seams.

Three fresh local-discovery processes per interface/variant, run before then after with OS caches retained: CLI medians 0.426→0.429s elapsed, 0.590→0.599s CPU, 390.5→393.3MB sampled process-family RSS; MCP 0.811→0.806s, 1.183→1.305s, 750.2→749.4MB. Full CLI reply estimates changed 51→54 cl100k tokens; MCP request/result wrapper stayed 165. These are descriptive measurements, not a speedup claim; brief RSS peaks and full agent context/catalog/initialization costs are unmeasured. Large process-list copy cost is unmeasured.

Adapted the shared projection and boundary-regression approaches reviewed in #4647 and #4684, with verified Git co-author credit. Those PRs were not merged and their human-readiness declarations were not changed. Local evidence: .cache/codebase-audit/frida4637-{baseline-red,focused-final,check-final,docs}.log, frida4637-real-discovery-validation.json, and frida4637-costs.json.

<!-- gh-comment-id:6264178558 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on main in [`4cbcead46`](https://github.com/morluto/rea/commit/4cbcead46cb16b18c9467498e5b4a6778e4b7aa0). Root cause: the shared workflow passed provider DTOs through, leaving CLI and MCP to project them independently. Nonempty CLI device results leaked `deviceId`. The application workflow now owns the complete device/process result shape, typed against each named output contract. Both adapters consume it directly. Projection happens before CLI cleanup, so retained partial observations use the same canonical keys. The adjacent process-list mappings were already correct, but their duplicated interpretation is removed too. Validation on pinned Node 24.18.0/npm 11.16.0: - New boundary cases first failed in four places: two nonempty device inventories and device/process observations retained after final cleanup failure. Empty-device and successful-process controls passed. - Final focused run: **103 tests passed across five files**, covering service/manager, CLI, MCP discovery schemas and MCP lifecycle behavior. `npm run check` and `npm run docs:check` passed. - **Real Frida 17.23.2, macOS arm64:** compiled CLI and production stdio MCP each enumerated Local System, Local Socket and GDB Remote Stub. Before: CLI schema rejected the result, MCP passed. After: both passed, with the same identifiers, names, types, order and cleanup status. No target attachment, spawn or instrumentation was performed. Remote endpoint and Linux/Windows coverage remain unverified; cleanup failures and process inventories use controlled provider seams. Three fresh local-discovery processes per interface/variant, run before then after with OS caches retained: CLI medians 0.426→0.429s elapsed, 0.590→0.599s CPU, 390.5→393.3MB sampled process-family RSS; MCP 0.811→0.806s, 1.183→1.305s, 750.2→749.4MB. Full CLI reply estimates changed 51→54 cl100k tokens; MCP request/result wrapper stayed 165. These are descriptive measurements, not a speedup claim; brief RSS peaks and full agent context/catalog/initialization costs are unmeasured. Large process-list copy cost is unmeasured. Adapted the shared projection and boundary-regression approaches reviewed in #4647 and #4684, with verified Git co-author credit. Those PRs were not merged and their human-readiness declarations were not changed. Local evidence: `.cache/codebase-audit/frida4637-{baseline-red,focused-final,check-final,docs}.log`, `frida4637-real-discovery-validation.json`, and `frida4637-costs.json`.
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#2418
No description provided.