[GH-ISSUE #6459] [Bug] list-electron-targets and inspect-electron-page discard usable siblings after malformed attachment state #3871

Closed
opened 2026-10-31 00:44:48 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

A string attached value on one /json/list row makes both list-electron-targets and inspect-electron-page fail for the entire endpoint. A well-formed sibling selected by exact ID cannot be inspected. Both commands exit 1 with unreadable_output / invalid_endpoint_response, before any WebSocket opens. The same calls succeed when only the sibling's malformed value is changed to null or omitted.

Reproduction

From a fresh checkout, prepare the tested revision:

git checkout 9eb829aca34be36ff112826337996f6139f82eff
npm ci && npm run build:cached

The observed runs used the existing locked dependencies and built runtime at this revision; the installation/build commands above are preparation instructions.

From the repository root, run this self-contained fixture. It creates an HTML file and isolated home, starts a literal-loopback HTTP/WebSocket endpoint, runs both public CLI commands, prints reduced results, and closes its servers. WORK selects the fixture/output directory outside the checkout.

export WORK="$(mktemp -d)"
node --input-type=module <<'JS'
import {createServer} from 'node:http';
import {mkdir,writeFile} from 'node:fs/promises';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';
import {createRequire} from 'node:module';
import {spawn} from 'node:child_process';
const root=resolve(process.env.WORK);
const {WebSocketServer}=createRequire(resolve('package.json'))('ws');
await mkdir(root+'/home',{recursive:true});
await writeFile(root+'/page.html','<main>fixture</main>');
const page=pathToFileURL(root+'/page.html').href;
let attached='unknown', selectedType='page', commands=[], sockets=[];
const server=createServer((req,res)=>{
 const port=server.address().port;
 const socket=id=>`ws://127.0.0.1:${port}/devtools/page/${id}`;
 res.setHeader('content-type','application/json');
 res.end(JSON.stringify(req.url==='/json/version'?{
  Browser:'Chrome/140.0.0.0','Protocol-Version':'1.3','User-Agent':'fixture',
  'V8-Version':'14.0','WebKit-Version':'537.36',webSocketDebuggerUrl:socket('t1')
 }:[
  {id:'t1',type:selectedType,title:'Selected',url:page,attached:false,webSocketDebuggerUrl:socket('t1')},
  {id:'t2',type:'page',title:'Sibling',url:page,attached,webSocketDebuggerUrl:socket('t2')}
 ]));
});
const ws=new WebSocketServer({server});
ws.on('connection',(client,req)=>{
 sockets.push(req.url);
 client.on('message',raw=>{
  const c=JSON.parse(String(raw));commands.push(c.method);
  let result={};
  if(c.method==='Page.getFrameTree')result={frameTree:{frame:{id:'F1',url:page}}};
  if(c.method==='Page.getResourceTree')result={frameTree:{frame:{id:'F1',url:page},resources:[]}};
  if(c.method==='DOMSnapshot.captureSnapshot')result={strings:[],documents:[]};
  if(c.method==='Target.getTargets')result={targetInfos:[]};
  client.send(JSON.stringify({id:c.id,result}));
 });
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));
const endpoint=`http://127.0.0.1:${server.address().port}`;
const results=[];
try{
 for(const mode of ['unknown-1','unknown-2','null','omitted','nonpage']){
  attached=mode.startsWith('unknown')?'unknown':mode==='omitted'?undefined:null;
  selectedType=mode==='nonpage'?'worker':'page';
  for(const command of ['list-electron-targets','inspect-electron-page']){
   commands=[];sockets=[];
   const args=['scripts/rea.mjs',command,endpoint];
   if(command==='inspect-electron-page')args.push('t1','--observation-ms','0');
   args.push('--json');
   const result=await new Promise((done,reject)=>{
    const child=spawn(process.execPath,args,{env:{...process.env,HOME:root+'/home'}});
    let stdout='',stderr='';
    child.stdout.on('data',x=>stdout+=x);child.stderr.on('data',x=>stderr+=x);
    child.on('error',reject);child.on('close',code=>done({code,stdout,stderr}));
   });
   await writeFile(`${root}/${mode}-${command}.json`,result.stdout);
   const output=JSON.parse(result.stdout), n=output.normalized_result;
   const row={mode,command,exit:result.code,code:output.code,details:output.details,
    targets:n?.targets?.map(t=>({id:t.target_id,attached:t.attached})),target:n?.target?.target_id,
    commands:[...commands],sockets:[...sockets]};
   results.push(row);console.log(JSON.stringify(row));
  }
 }
 await writeFile(root+'/results.json',JSON.stringify(results,null,2));
}finally{
 for(const client of ws.clients)client.terminate();
 await new Promise(r=>ws.close(r));await new Promise(r=>server.close(r));
}
JS

The invoked commands are node scripts/rea.mjs list-electron-targets ENDPOINT --json and node scripts/rea.mjs inspect-electron-page ENDPOINT t1 --observation-ms 0 --json. Each child receives HOME=$WORK/home. Both targets have valid same-port page sockets and point to the same existing regular HTML file. Only t2.attached changes between malformed and successful controls. The final non-page control changes the selected target's type to verify that an ineligible target is still refused.

Expected behavior

Preserve the eligible sibling in listing and allow inspection by its ID. Keep malformed metadata explicit as unknown or a target-scoped exclusion/diagnostic, without weakening file or socket authorization.

The Electron target boundary accepts local regular-file pages; the workflow promises every eligible target inline and rediscovery of the selected ID. Boundary guidance requires partial facts and meaningful failure reasons to be preserved. The target result schema already represents unknown attachment state as null; it does not explicitly prescribe coercing a string.

Impact and scope

P2: one malformed optional metadata field blocks inventory and inspection of usable siblings at the selected endpoint. Each command failed in both malformed-input iterations (four failing CLI calls). The null and omitted controls each restored both commands. A caller controlling the discovery producer can omit the field or return null; whether ordinary callers can change their producer's discovery output is unknown.

The evidence covers a synthetic CDP endpoint through the public CLI. NOT_ENOUGH_INFO: no real Electron/Chromium producer emitting this string or production occurrence rate was established. No security boundary was crossed.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64. No native-analysis provider or installed Electron application was used.

Target and artifact context

A local HTTP/WebSocket CDP fixture advertises two page targets backed by one generated regular HTML file. The selected t1 has attached: false; sibling t2 has a string, null, or omitted attachment field. No external target service is contacted. Full CLI JSON and the reduced result matrix are written under $WORK.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Observed results, reduced to the discriminating fields:

t2.attached="unknown", twice:
  list-electron-targets -> exit 1, unreadable_output / invalid_endpoint_response
  inspect-electron-page t1 -> exit 1, unreadable_output / invalid_endpoint_response
  all four calls: sockets=[], commands=[]
t2.attached=null:
  list -> exit 0, t1 attached=false and t2 attached=null
  inspect t1 -> exit 0, target=t1, only /devtools/page/t1 connected
t2.attached omitted:
  list -> exit 0, t1 attached=false and t2 attached=null
  inspect t1 -> exit 0, target=t1, only /devtools/page/t1 connected
valid metadata, t1.type="worker":
  list -> exit 0, only t2 retained
  inspect t1 -> exit 1, target_unavailable / target_not_allowed, no socket

The failing response gives generic rea doctor recovery. The successful inspection sends page/frame, runtime/debugger, resource, DOM, worker-inventory, final frame-check, and cleanup commands. The fixture waits for all CLI children and closes its sockets and servers.

Typed outcome or recovery expected

Return the valid sibling in inventory and successfully inspect it despite unrelated malformed attachment metadata. Preserve an explicit diagnostic for rejected metadata or rows. Keep the demonstrated target_not_allowed refusal for a selected non-page target.

Analysis or suspected cause

Source-derived: the CDP endpoint target schema restricts attached to boolean/null/omitted and wraps all rows in a single array schema. Discovery parses that whole array before any per-target processing. Both listing and inspection discover before selecting or authorizing targets. A schema failure becomes invalid_endpoint_response, which projects to unreadable_output.

Additional context

Related: #5437 reports the same attachment trigger for V8 Inspector discovery, whose separate row schema is not used by these Electron commands. Fixing only that Inspector schema would leave the reproduced Electron failure. Closed #2537 addressed missing Inspector sockets, and #3379 addressed malformed post-discovery Electron collection replies; neither addresses this discovery-field validation.

NOT_ENOUGH_INFO: the last known good version and real-world frequency are unknown.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6459 ### 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 Process capture or runtime observation ### Observed behavior A string `attached` value on one `/json/list` row makes both `list-electron-targets` and `inspect-electron-page` fail for the entire endpoint. A well-formed sibling selected by exact ID cannot be inspected. Both commands exit 1 with `unreadable_output` / `invalid_endpoint_response`, before any WebSocket opens. The same calls succeed when only the sibling's malformed value is changed to `null` or omitted. ### Reproduction From a fresh checkout, prepare the tested revision: ```sh git checkout 9eb829aca34be36ff112826337996f6139f82eff npm ci && npm run build:cached ``` The observed runs used the existing locked dependencies and built runtime at this revision; the installation/build commands above are preparation instructions. From the repository root, run this self-contained fixture. It creates an HTML file and isolated home, starts a literal-loopback HTTP/WebSocket endpoint, runs both public CLI commands, prints reduced results, and closes its servers. `WORK` selects the fixture/output directory outside the checkout. ```sh export WORK="$(mktemp -d)" node --input-type=module <<'JS' import {createServer} from 'node:http'; import {mkdir,writeFile} from 'node:fs/promises'; import {resolve} from 'node:path'; import {pathToFileURL} from 'node:url'; import {createRequire} from 'node:module'; import {spawn} from 'node:child_process'; const root=resolve(process.env.WORK); const {WebSocketServer}=createRequire(resolve('package.json'))('ws'); await mkdir(root+'/home',{recursive:true}); await writeFile(root+'/page.html','<main>fixture</main>'); const page=pathToFileURL(root+'/page.html').href; let attached='unknown', selectedType='page', commands=[], sockets=[]; const server=createServer((req,res)=>{ const port=server.address().port; const socket=id=>`ws://127.0.0.1:${port}/devtools/page/${id}`; res.setHeader('content-type','application/json'); res.end(JSON.stringify(req.url==='/json/version'?{ Browser:'Chrome/140.0.0.0','Protocol-Version':'1.3','User-Agent':'fixture', 'V8-Version':'14.0','WebKit-Version':'537.36',webSocketDebuggerUrl:socket('t1') }:[ {id:'t1',type:selectedType,title:'Selected',url:page,attached:false,webSocketDebuggerUrl:socket('t1')}, {id:'t2',type:'page',title:'Sibling',url:page,attached,webSocketDebuggerUrl:socket('t2')} ])); }); const ws=new WebSocketServer({server}); ws.on('connection',(client,req)=>{ sockets.push(req.url); client.on('message',raw=>{ const c=JSON.parse(String(raw));commands.push(c.method); let result={}; if(c.method==='Page.getFrameTree')result={frameTree:{frame:{id:'F1',url:page}}}; if(c.method==='Page.getResourceTree')result={frameTree:{frame:{id:'F1',url:page},resources:[]}}; if(c.method==='DOMSnapshot.captureSnapshot')result={strings:[],documents:[]}; if(c.method==='Target.getTargets')result={targetInfos:[]}; client.send(JSON.stringify({id:c.id,result})); }); }); await new Promise(r=>server.listen(0,'127.0.0.1',r)); const endpoint=`http://127.0.0.1:${server.address().port}`; const results=[]; try{ for(const mode of ['unknown-1','unknown-2','null','omitted','nonpage']){ attached=mode.startsWith('unknown')?'unknown':mode==='omitted'?undefined:null; selectedType=mode==='nonpage'?'worker':'page'; for(const command of ['list-electron-targets','inspect-electron-page']){ commands=[];sockets=[]; const args=['scripts/rea.mjs',command,endpoint]; if(command==='inspect-electron-page')args.push('t1','--observation-ms','0'); args.push('--json'); const result=await new Promise((done,reject)=>{ const child=spawn(process.execPath,args,{env:{...process.env,HOME:root+'/home'}}); let stdout='',stderr=''; child.stdout.on('data',x=>stdout+=x);child.stderr.on('data',x=>stderr+=x); child.on('error',reject);child.on('close',code=>done({code,stdout,stderr})); }); await writeFile(`${root}/${mode}-${command}.json`,result.stdout); const output=JSON.parse(result.stdout), n=output.normalized_result; const row={mode,command,exit:result.code,code:output.code,details:output.details, targets:n?.targets?.map(t=>({id:t.target_id,attached:t.attached})),target:n?.target?.target_id, commands:[...commands],sockets:[...sockets]}; results.push(row);console.log(JSON.stringify(row)); } } await writeFile(root+'/results.json',JSON.stringify(results,null,2)); }finally{ for(const client of ws.clients)client.terminate(); await new Promise(r=>ws.close(r));await new Promise(r=>server.close(r)); } JS ``` The invoked commands are `node scripts/rea.mjs list-electron-targets ENDPOINT --json` and `node scripts/rea.mjs inspect-electron-page ENDPOINT t1 --observation-ms 0 --json`. Each child receives `HOME=$WORK/home`. Both targets have valid same-port page sockets and point to the same existing regular HTML file. Only `t2.attached` changes between malformed and successful controls. The final non-page control changes the selected target's type to verify that an ineligible target is still refused. ### Expected behavior Preserve the eligible sibling in listing and allow inspection by its ID. Keep malformed metadata explicit as unknown or a target-scoped exclusion/diagnostic, without weakening file or socket authorization. The [Electron target boundary](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/electron-observation.md#L26) accepts local regular-file pages; the [workflow](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/electron-observation.md#L44) promises every eligible target inline and rediscovery of the selected ID. [Boundary guidance](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/AGENTS.md#L31) requires partial facts and meaningful failure reasons to be preserved. The [target result schema](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/javascript/electronObservation.ts#L47) already represents unknown attachment state as `null`; it does not explicitly prescribe coercing a string. ### Impact and scope P2: one malformed optional metadata field blocks inventory and inspection of usable siblings at the selected endpoint. Each command failed in both malformed-input iterations (four failing CLI calls). The null and omitted controls each restored both commands. A caller controlling the discovery producer can omit the field or return null; whether ordinary callers can change their producer's discovery output is unknown. The evidence covers a synthetic CDP endpoint through the public CLI. NOT_ENOUGH_INFO: no real Electron/Chromium producer emitting this string or production occurrence rate was established. No security boundary was crossed. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64. No native-analysis provider or installed Electron application was used. ### Target and artifact context A local HTTP/WebSocket CDP fixture advertises two page targets backed by one generated regular HTML file. The selected `t1` has `attached: false`; sibling `t2` has a string, null, or omitted attachment field. No external target service is contacted. Full CLI JSON and the reduced result matrix are written under `$WORK`. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Observed results, reduced to the discriminating fields: ```text t2.attached="unknown", twice: list-electron-targets -> exit 1, unreadable_output / invalid_endpoint_response inspect-electron-page t1 -> exit 1, unreadable_output / invalid_endpoint_response all four calls: sockets=[], commands=[] t2.attached=null: list -> exit 0, t1 attached=false and t2 attached=null inspect t1 -> exit 0, target=t1, only /devtools/page/t1 connected t2.attached omitted: list -> exit 0, t1 attached=false and t2 attached=null inspect t1 -> exit 0, target=t1, only /devtools/page/t1 connected valid metadata, t1.type="worker": list -> exit 0, only t2 retained inspect t1 -> exit 1, target_unavailable / target_not_allowed, no socket ``` The failing response gives generic `rea doctor` recovery. The successful inspection sends page/frame, runtime/debugger, resource, DOM, worker-inventory, final frame-check, and cleanup commands. The fixture waits for all CLI children and closes its sockets and servers. ### Typed outcome or recovery expected Return the valid sibling in inventory and successfully inspect it despite unrelated malformed attachment metadata. Preserve an explicit diagnostic for rejected metadata or rows. Keep the demonstrated `target_not_allowed` refusal for a selected non-page target. ### Analysis or suspected cause Source-derived: the [CDP endpoint target schema](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpEndpoint.ts#L29) restricts `attached` to boolean/null/omitted and wraps all rows in a single array schema. [Discovery parses that whole array](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpEndpoint.ts#L149) before any per-target processing. Both [listing](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpElectronProvider.ts#L140) and [inspection](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpElectronProvider.ts#L279) discover before selecting or authorizing targets. A [schema failure](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpEndpoint.ts#L370) becomes `invalid_endpoint_response`, which [projects to unreadable_output](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/analysisErrorProjection.ts#L188). ### Additional context Related: [#5437](https://github.com/morluto/rea/issues/5437) reports the same attachment trigger for V8 Inspector discovery, whose [separate row schema](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/inspector/V8InspectorEndpoint.ts#L20) is not used by these Electron commands. Fixing only that Inspector schema would leave the reproduced Electron failure. Closed [#2537](https://github.com/morluto/rea/issues/2537) addressed missing Inspector sockets, and [#3379](https://github.com/morluto/rea/issues/3379) addressed malformed post-discovery Electron collection replies; neither addresses this discovery-field validation. NOT_ENOUGH_INFO: the last known good version and real-world frequency are unknown.
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#3871
No description provided.