[GH-ISSUE #6475] [Bug] inspect-web-page classifies workers with unknown ownership as out of target scope #3883

Closed
opened 2026-10-31 00:44:55 +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/6475

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

node scripts/rea.mjs inspect-web-page ENDPOINT page --observation-ms 0 --json drops a worker and records workers: out_of_target_scope when CDP omits both optional ownership relationships. The result does not distinguish unknown ownership from a worker explicitly linked to another page.

A controlled loopback CDP fixture reproduced this through the public CLI. With an allowed-origin worker URL and openerId: "page", one worker is returned. Removing only openerId returns workers: [] and an out_of_target_scope exclusion. unavailable_sections remains empty and no limitation explains missing worker ownership. An empty worker URL with missing relationships receives the same exclusion.

Reproduction

Prepare a fresh checkout at the source revision below:

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

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

Run this inline fixture from the repository root. It supplies valid discovery and frame replies and changes only worker metadata between cases. It invokes the real CLI, redirects its home directory, and closes its server and sockets afterward. No browser or remote service is needed.

node --input-type=module <<'JS'
import {createServer} from 'node:http';
import {createRequire} from 'node:module';
import {resolve,join} from 'node:path';
import {mkdir,mkdtemp,rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {execFile} from 'node:child_process';
import {promisify} from 'node:util';
const root=await mkdtemp(join(tmpdir(),'rea-worker-scope-'));
await mkdir(join(root,'home'));
const {WebSocketServer}=createRequire(resolve('package.json'))('ws');
let endpoint,mode;
const pageUrl=()=>`${endpoint}/index.html`;
const server=createServer((req,res)=>{
 res.setHeader('Content-Type','application/json');
 res.end(JSON.stringify(req.url==='/json/list'?
  [{id:'page',type:'page',title:'Fixture',url:pageUrl(),attached:false}]:
  {Browser:'CDPFixture/1.0','Protocol-Version':'1.3','User-Agent':'Fixture',
   'V8-Version':'1','WebKit-Version':'1',
   webSocketDebuggerUrl:endpoint.replace('http:','ws:')+'/devtools/browser/fixture'}));
});
const sockets=new WebSocketServer({server});
sockets.on('connection',socket=>socket.on('message',raw=>{
 const c=JSON.parse(raw);let result={};
 switch(c.method){
  case 'Target.attachToTarget':result={sessionId:'page-session'};break;
  case 'Page.getFrameTree':result={frameTree:{frame:{id:'main',loaderId:'loader',url:pageUrl()}}};break;
  case 'Page.getResourceTree':result={frameTree:{frame:{id:'main',url:pageUrl()},resources:[]}};break;
  case 'DOMSnapshot.captureSnapshot':result={strings:[pageUrl(),'#document','HTML'],documents:[{documentURL:0,baseURL:0,nodes:{nodeType:[9,1],nodeName:[1,2],nodeValue:[-1,-1],parentIndex:[-1,0],attributes:[[],[]]}}]};break;
  case 'Accessibility.getFullAXTree':result={nodes:[]};break;
  case 'Storage.getUsageAndQuota':result={usage:0,quota:1024,usageBreakdown:[]};break;
  case 'Target.getTargets':result={targetInfos:[{
   targetId:'worker',type:'worker',attached:true,
   url:mode==='missing-url'?'':`${endpoint}/worker.js`,
   ...(mode==='related'?{openerId:'page'}:mode==='foreign'?{openerId:'other-page'}:{})
  }]};break;
 }
 socket.send(JSON.stringify({id:c.id,...(c.sessionId?{sessionId:c.sessionId}:{}),result}));
}));
await new Promise(r=>server.listen(0,'127.0.0.1',r));
endpoint=`http://127.0.0.1:${server.address().port}`;
try {
 for(mode of ['related','missing-relationship','missing-url','foreign']){
  const {stdout}=await promisify(execFile)(process.execPath,
   ['scripts/rea.mjs','inspect-web-page',endpoint,'page','--observation-ms','0','--json'],
   {env:{...process.env,HOME:join(root,'home')},timeout:25000,maxBuffer:16*1024*1024});
  const n=JSON.parse(stdout).normalized_result;
  console.log(JSON.stringify({mode,workers:n.workers,
   excluded:n.completeness.excluded.filter(x=>x.section==='workers'),
   unavailable:n.completeness.unavailable_sections}));
 }
}finally{
 for(const socket of sockets.clients)socket.terminate();
 await new Promise(r=>sockets.close(r));
 await new Promise(r=>server.close(r));
 await rm(root,{recursive:true,force:true});
}
JS

Each controlled case reproduced in both runs (2/2), including a replay of the inline fixture above. The worker inventory has valid mandatory fields; neither relationship field is present in the two unknown cases. This is protocol-boundary evidence, not a claim that a real browser supplied the synthetic known-URL target.

Expected behavior

Keep unknown ownership distinct from proven foreign ownership. If REA cannot associate a worker with the selected page, it may withhold the target details, but should explain that limitation and preserve the uncertainty in coverage. Same-origin URLs alone must not establish page ownership.

The identity contract requires unresolved identity/applicability to remain unknown. The worker contract treats opener/parent relationships as supplied metadata and preserves the origin boundary. The completeness contract requires explicit coverage for omissions.

Impact and scope

P2: callers receive a definite target-policy exclusion where the producer supplied insufficient ownership evidence. Worker inventory and its completeness can therefore mislead runtime investigations. Other page observations still succeed; no security boundary crossing was observed.

Separately, four successful 200 ms CLI captures across two local browser runs returned the same worker exclusion for a page that creates a same-origin dedicated worker. In HeadlessChrome/145.0.7632.6, independent Target.getTargets reads returned that worker with an empty URL, attached: false, and neither relationship field. This establishes a real unknown-origin/ownership case. The known-URL case is established by the controlled fixture above. Blob workers, other browser versions, and disallowed-origin controls were not tested.

No REA option restoring this classification was established. Independent CDP inspection can reveal raw metadata, but does not repair the returned REA coverage.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64. The repository's Node.js 24.18.0 development pin was not exercised. The separate real-browser observations used existing HeadlessChrome/145.0.7632.6, matching the locked playwright-core 1.58.2 browser revision.

Target and artifact context

Loopback HTTP/WebSocket CDP fixture, one authorized selected HTTP page, and one valid worker target record. Controlled worker metadata varies between a selected-page opener, missing relationships with a same-origin URL, missing relationships with an empty URL, and a different-page opener. No remote target or user data is involved.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

All four controlled CLI calls exited successfully. Selected observed output:

related:
  workers: [{type: "worker", opener_target_id: "page", parent_frame_id: null, ...}]
  worker exclusions: []
missing-relationship:
  workers: []
  worker exclusions: [{section: "workers", reason: "out_of_target_scope", count: 1}]
missing-url:
  workers: []
  worker exclusions: [{section: "workers", reason: "out_of_target_scope", count: 1}]
foreign:
  workers: []
  worker exclusions: [{section: "workers", reason: "out_of_target_scope", count: 1}]
All cases: unavailable_sections: []

The unknown and foreign cases become indistinguishable at the worker exclusion boundary.

Typed outcome or recovery expected

Retain the known-related control and the exclusion of known-foreign targets. Report unknown ownership explicitly for missing relationships without inferring page ownership or exposing unapproved target details. An empty URL should also preserve unknown origin rather than being hidden behind a definite target exclusion.

Analysis or suspected cause

Source-derived explanation: captureWorkers accepts absent relationships, then computes relatedToPage as false for them and immediately records out_of_target_scope. This precedes the URL/unknown-origin classification. The public capture calls this collector after frame authorization. The completeness ledger treats that reason as policy filtering.

Additional context

Related #4425 concerns missing worker network events while a worker with a known parent is retained; this report concerns ownership classification of a live returned target. Completed #3378 addresses malformed inventories/mandatory entries, whereas this fixture has valid mandatory fields and absent optional relationships. #883 requests broader worker source and lifecycle attribution; this report concerns uncertainty in the existing inventory contract. No known-good revision was established.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6475 ### 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 `node scripts/rea.mjs inspect-web-page ENDPOINT page --observation-ms 0 --json` drops a worker and records `workers: out_of_target_scope` when CDP omits both optional ownership relationships. The result does not distinguish unknown ownership from a worker explicitly linked to another page. A controlled loopback CDP fixture reproduced this through the public CLI. With an allowed-origin worker URL and `openerId: "page"`, one worker is returned. Removing only `openerId` returns `workers: []` and an `out_of_target_scope` exclusion. `unavailable_sections` remains empty and no limitation explains missing worker ownership. An empty worker URL with missing relationships receives the same exclusion. ### Reproduction Prepare a fresh checkout at the source revision below: ```sh git checkout 9eb829aca34be36ff112826337996f6139f82eff npm ci && npm run build:cached ``` The observed runs used dependencies and a built runtime already present at this revision; the installation/build commands above are preparation instructions. Run this inline fixture from the repository root. It supplies valid discovery and frame replies and changes only worker metadata between cases. It invokes the real CLI, redirects its home directory, and closes its server and sockets afterward. No browser or remote service is needed. ```sh node --input-type=module <<'JS' import {createServer} from 'node:http'; import {createRequire} from 'node:module'; import {resolve,join} from 'node:path'; import {mkdir,mkdtemp,rm} from 'node:fs/promises'; import {tmpdir} from 'node:os'; import {execFile} from 'node:child_process'; import {promisify} from 'node:util'; const root=await mkdtemp(join(tmpdir(),'rea-worker-scope-')); await mkdir(join(root,'home')); const {WebSocketServer}=createRequire(resolve('package.json'))('ws'); let endpoint,mode; const pageUrl=()=>`${endpoint}/index.html`; const server=createServer((req,res)=>{ res.setHeader('Content-Type','application/json'); res.end(JSON.stringify(req.url==='/json/list'? [{id:'page',type:'page',title:'Fixture',url:pageUrl(),attached:false}]: {Browser:'CDPFixture/1.0','Protocol-Version':'1.3','User-Agent':'Fixture', 'V8-Version':'1','WebKit-Version':'1', webSocketDebuggerUrl:endpoint.replace('http:','ws:')+'/devtools/browser/fixture'})); }); const sockets=new WebSocketServer({server}); sockets.on('connection',socket=>socket.on('message',raw=>{ const c=JSON.parse(raw);let result={}; switch(c.method){ case 'Target.attachToTarget':result={sessionId:'page-session'};break; case 'Page.getFrameTree':result={frameTree:{frame:{id:'main',loaderId:'loader',url:pageUrl()}}};break; case 'Page.getResourceTree':result={frameTree:{frame:{id:'main',url:pageUrl()},resources:[]}};break; case 'DOMSnapshot.captureSnapshot':result={strings:[pageUrl(),'#document','HTML'],documents:[{documentURL:0,baseURL:0,nodes:{nodeType:[9,1],nodeName:[1,2],nodeValue:[-1,-1],parentIndex:[-1,0],attributes:[[],[]]}}]};break; case 'Accessibility.getFullAXTree':result={nodes:[]};break; case 'Storage.getUsageAndQuota':result={usage:0,quota:1024,usageBreakdown:[]};break; case 'Target.getTargets':result={targetInfos:[{ targetId:'worker',type:'worker',attached:true, url:mode==='missing-url'?'':`${endpoint}/worker.js`, ...(mode==='related'?{openerId:'page'}:mode==='foreign'?{openerId:'other-page'}:{}) }]};break; } socket.send(JSON.stringify({id:c.id,...(c.sessionId?{sessionId:c.sessionId}:{}),result})); })); await new Promise(r=>server.listen(0,'127.0.0.1',r)); endpoint=`http://127.0.0.1:${server.address().port}`; try { for(mode of ['related','missing-relationship','missing-url','foreign']){ const {stdout}=await promisify(execFile)(process.execPath, ['scripts/rea.mjs','inspect-web-page',endpoint,'page','--observation-ms','0','--json'], {env:{...process.env,HOME:join(root,'home')},timeout:25000,maxBuffer:16*1024*1024}); const n=JSON.parse(stdout).normalized_result; console.log(JSON.stringify({mode,workers:n.workers, excluded:n.completeness.excluded.filter(x=>x.section==='workers'), unavailable:n.completeness.unavailable_sections})); } }finally{ for(const socket of sockets.clients)socket.terminate(); await new Promise(r=>sockets.close(r)); await new Promise(r=>server.close(r)); await rm(root,{recursive:true,force:true}); } JS ``` Each controlled case reproduced in both runs (2/2), including a replay of the inline fixture above. The worker inventory has valid mandatory fields; neither relationship field is present in the two unknown cases. This is protocol-boundary evidence, not a claim that a real browser supplied the synthetic known-URL target. ### Expected behavior Keep unknown ownership distinct from proven foreign ownership. If REA cannot associate a worker with the selected page, it may withhold the target details, but should explain that limitation and preserve the uncertainty in coverage. Same-origin URLs alone must not establish page ownership. The [identity contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/AGENTS.md#L19-L21) requires unresolved identity/applicability to remain unknown. The [worker contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/browser-observation.md#L376-L379) treats opener/parent relationships as supplied metadata and preserves the origin boundary. The [completeness contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/browser-observation.md#L383-L392) requires explicit coverage for omissions. ### Impact and scope P2: callers receive a definite target-policy exclusion where the producer supplied insufficient ownership evidence. Worker inventory and its completeness can therefore mislead runtime investigations. Other page observations still succeed; no security boundary crossing was observed. Separately, four successful 200 ms CLI captures across two local browser runs returned the same worker exclusion for a page that creates a same-origin dedicated worker. In HeadlessChrome/145.0.7632.6, independent `Target.getTargets` reads returned that worker with an empty URL, `attached: false`, and neither relationship field. This establishes a real unknown-origin/ownership case. The known-URL case is established by the controlled fixture above. Blob workers, other browser versions, and disallowed-origin controls were not tested. No REA option restoring this classification was established. Independent CDP inspection can reveal raw metadata, but does not repair the returned REA coverage. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64. The repository's Node.js 24.18.0 development pin was not exercised. The separate real-browser observations used existing HeadlessChrome/145.0.7632.6, matching the locked playwright-core 1.58.2 browser revision. ### Target and artifact context Loopback HTTP/WebSocket CDP fixture, one authorized selected HTTP page, and one valid worker target record. Controlled worker metadata varies between a selected-page opener, missing relationships with a same-origin URL, missing relationships with an empty URL, and a different-page opener. No remote target or user data is involved. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence All four controlled CLI calls exited successfully. Selected observed output: ```text related: workers: [{type: "worker", opener_target_id: "page", parent_frame_id: null, ...}] worker exclusions: [] missing-relationship: workers: [] worker exclusions: [{section: "workers", reason: "out_of_target_scope", count: 1}] missing-url: workers: [] worker exclusions: [{section: "workers", reason: "out_of_target_scope", count: 1}] foreign: workers: [] worker exclusions: [{section: "workers", reason: "out_of_target_scope", count: 1}] All cases: unavailable_sections: [] ``` The unknown and foreign cases become indistinguishable at the worker exclusion boundary. ### Typed outcome or recovery expected Retain the known-related control and the exclusion of known-foreign targets. Report unknown ownership explicitly for missing relationships without inferring page ownership or exposing unapproved target details. An empty URL should also preserve unknown origin rather than being hidden behind a definite target exclusion. ### Analysis or suspected cause Source-derived explanation: [captureWorkers](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpPageCaptureWorkers.ts#L62-L95) accepts absent relationships, then computes `relatedToPage` as false for them and immediately records `out_of_target_scope`. This precedes the URL/unknown-origin classification. The public capture [calls this collector](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpPageCapture.ts#L150-L153) after frame authorization. The [completeness ledger](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/browser/CdpCaptureCompleteness.ts#L23-L29) treats that reason as policy filtering. ### Additional context Related [#4425](https://github.com/morluto/rea/issues/4425) concerns missing worker network events while a worker with a known parent is retained; this report concerns ownership classification of a live returned target. Completed [#3378](https://github.com/morluto/rea/issues/3378) addresses malformed inventories/mandatory entries, whereas this fixture has valid mandatory fields and absent optional relationships. [#883](https://github.com/morluto/rea/issues/883) requests broader worker source and lifecycle attribution; this report concerns uncertainty in the existing inventory contract. No known-good revision was established.
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#3883
No description provided.