[GH-ISSUE #5075] [Bug] Inline literal object spreads erase static WebMCP declaration metadata #3114

Closed
opened 2026-10-31 00:36:47 +00:00 by bsakaguchi · 4 comments
Owner

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

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

The production analyzeWebBundle application service returns name: null, description: null, and schema_property_names: [] for declarations that obtain their metadata from an inline object-literal spread, or place an unrelated inline object spread after explicit metadata. The equivalent direct declaration returns "catalog", "Browse catalog", and ["q"].

Observed on current main through CdpBrowserProvider using the repository fake CDP browser over loopback HTTP/WebSocket. This is mocked-browser boundary verification, not real Chrome or CLI/MCP transport verification. The supplied JavaScript is analyzed without execution.

Reproduction

From a checkout at the source revision below with locked dependencies installed, save this probe as $WORK/probe.mts, where $WORK is a disposable directory outside the checkout:

import assert from "node:assert/strict";
import { pathToFileURL } from "node:url";
const load = (p: string) => import(pathToFileURL(`${process.cwd()}/${p}`).href);
const { startFakeCdpBrowser } = await load("tests/fixtures/fakeCdpBrowser.ts");
const { CdpBrowserProvider } = await load("src/browser/CdpBrowserProvider.ts");
const { analyzeWebBundleInputSchema } = await load("src/domain/webBundleAnalysis.ts");
const { analyzeWebBundle } = await load("src/application/BrowserObservationService.ts");
const cases = [
 ["direct", 'navigator.modelContext.registerTool({name:"catalog",description:"Browse catalog",inputSchema:{properties:{q:{}}}});', "catalog", "Browse catalog", ["q"]],
 ["literal-spread", 'navigator.modelContext.registerTool({...{name:"catalog",description:"Browse catalog",inputSchema:{properties:{q:{}}}}});', null, null, []],
 ["unrelated-literal-spread", 'navigator.modelContext.registerTool({name:"catalog",description:"Browse catalog",inputSchema:{properties:{q:{}}},...{timeout:5}});', null, null, []],
 ["dynamic-spread-control", 'navigator.modelContext.registerTool({name:"catalog",...dynamic});', null, null, []],
] as const;
let source = "";
const browser = await startFakeCdpBrowser({commandResult: command => command.method === "Debugger.getScriptSource" ? {scriptSource:source} : undefined});
try {
 const provider = new CdpBrowserProvider();
 for (const [label,text,name,description,properties] of cases) {
  source=text;
  const input=analyzeWebBundleInputSchema.parse({cdp_endpoint:browser.endpoint,target_id:"allowed-page",observation_ms:0,include_script_sources:true});
  const result=await analyzeWebBundle(provider,input);
  assert(result.ok, result.ok ? "" : JSON.stringify(result.error));
  const output=result.value.normalized_result as any;
  const declarations=output.observations.webmcp_declarations;
  assert(declarations.length>0);
  for(const d of declarations) assert.deepEqual([d.name,d.description,d.schema_property_names],[name,description,properties]);
  console.log(JSON.stringify({label,declarations:declarations.map(({name,description,schema_property_names,trust}:any)=>({name,description,schema_property_names,trust}))}));
 }
} finally {await browser.close();}

Run from the checkout root with isolated configuration and temporary directories:

mkdir -p "$WORK/home" "$WORK/tmp"
HOME="$WORK/home" TMPDIR="$WORK/tmp" \
XDG_CONFIG_HOME="$WORK/home/config" XDG_CACHE_HOME="$WORK/home/cache" \
node --import ./node_modules/tsx/dist/loader.mjs "$WORK/probe.mts"

One executed round completed all four cases; assertions record the faulty baseline outputs rather than asserting the desired correction. No build or real browser is required.

Expected behavior

Both inline literal-spread cases should retain the same name, description and input property names as the direct case. These bounded literals contain no unresolved keys or getters. The dynamic-spread control should remain unknown.

The browser guide advertises static WebMCP declarations. The source metadata contract requires preserving metadata using its owning interpretation. The shared static-property resolver already resolves inline object literals while preserving unresolved overrides. The executed direct declaration supplies a differential control for the exact expected metadata.

Impact and scope

A bounded source syntax case loses meaningful tool identity, description and input metadata in returned analysis Evidence. All three affected metadata fields were omitted in both tested literal-spread cases; the direct control retained them. This can make the static declaration inventory incomplete. Real-application frequency and downstream incidents are NOT_ENOUGH_INFO; they were not measured. The fixture source remains manually readable, but no automatic equivalent-output workaround was verified. No target execution or security-boundary crossing occurred.

REA version

Source main 3537e58c1ab29309f06eabf50bc52adba9817a0c; package metadata 6.7.0. The published stable artifact was not executed.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1, Darwin 27.0.0, arm64. Repository Node pin is 24.18.0; this probe used the package-supported >=26 runtime. Locked existing dependencies; no native analysis provider involved.

Target and artifact context

Four inert synthetic JavaScript sources supplied by the repository's fake CDP browser. The production capture, static analyzer, output-schema validation and application Evidence wrapper were exercised. No page tool was registered or invoked.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced observed metadata, with every entry retaining trust: "page-declared-untrusted":

{"label":"direct","name":"catalog","description":"Browse catalog","schema_property_names":["q"]}
{"label":"literal-spread","name":null,"description":null,"schema_property_names":[]}
{"label":"unrelated-literal-spread","name":null,"description":null,"schema_property_names":[]}
{"label":"dynamic-spread-control","name":null,"description":null,"schema_property_names":[]}

The production provider captures source and invokes the analyzer. The application service returns its result as Evidence. These are observed boundary results; public CLI/MCP transport, real Chrome and published-release runtime coverage remain NOT_ENOUGH_INFO.

Typed outcome or recovery expected

Keep name: "catalog", description: "Browse catalog" and schema_property_names: ["q"] in both static literal-spread cases, with the existing untrusted designation. Preserve unknown metadata for genuinely unresolved spreads.

Analysis or suspected cause

Source-derived explanation: objectValue clears its current value on every spread without inspecting its literal argument. addWebMcpDeclaration uses that helper for declaration and schema properties, then projects missing values to null and empty arrays.

Additional context

The relevant analyzer, helper, provider and application-service files have no source differences between tag rea-agents-6.7.0 and the tested main revision. This establishes source equality only; no stable runtime reproduction is claimed.

Related #3987 concerns an unrelated computed empty-template key clearing a name. Its trigger and restoration check differ from resolving inline literal spreads. #3331 concerns schema keywords falsely reported as input fields. Neither covers this omission.

Originally created by @coygeek on GitHub (Oct 18, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5075 ## 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 Evidence, artifacts, or reconstruction ## Observed behavior The production `analyzeWebBundle` application service returns `name: null`, `description: null`, and `schema_property_names: []` for declarations that obtain their metadata from an inline object-literal spread, or place an unrelated inline object spread after explicit metadata. The equivalent direct declaration returns `"catalog"`, `"Browse catalog"`, and `["q"]`. Observed on current main through `CdpBrowserProvider` using the repository fake CDP browser over loopback HTTP/WebSocket. This is mocked-browser boundary verification, not real Chrome or CLI/MCP transport verification. The supplied JavaScript is analyzed without execution. ## Reproduction From a checkout at the source revision below with locked dependencies installed, save this probe as `$WORK/probe.mts`, where `$WORK` is a disposable directory outside the checkout: ```ts import assert from "node:assert/strict"; import { pathToFileURL } from "node:url"; const load = (p: string) => import(pathToFileURL(`${process.cwd()}/${p}`).href); const { startFakeCdpBrowser } = await load("tests/fixtures/fakeCdpBrowser.ts"); const { CdpBrowserProvider } = await load("src/browser/CdpBrowserProvider.ts"); const { analyzeWebBundleInputSchema } = await load("src/domain/webBundleAnalysis.ts"); const { analyzeWebBundle } = await load("src/application/BrowserObservationService.ts"); const cases = [ ["direct", 'navigator.modelContext.registerTool({name:"catalog",description:"Browse catalog",inputSchema:{properties:{q:{}}}});', "catalog", "Browse catalog", ["q"]], ["literal-spread", 'navigator.modelContext.registerTool({...{name:"catalog",description:"Browse catalog",inputSchema:{properties:{q:{}}}}});', null, null, []], ["unrelated-literal-spread", 'navigator.modelContext.registerTool({name:"catalog",description:"Browse catalog",inputSchema:{properties:{q:{}}},...{timeout:5}});', null, null, []], ["dynamic-spread-control", 'navigator.modelContext.registerTool({name:"catalog",...dynamic});', null, null, []], ] as const; let source = ""; const browser = await startFakeCdpBrowser({commandResult: command => command.method === "Debugger.getScriptSource" ? {scriptSource:source} : undefined}); try { const provider = new CdpBrowserProvider(); for (const [label,text,name,description,properties] of cases) { source=text; const input=analyzeWebBundleInputSchema.parse({cdp_endpoint:browser.endpoint,target_id:"allowed-page",observation_ms:0,include_script_sources:true}); const result=await analyzeWebBundle(provider,input); assert(result.ok, result.ok ? "" : JSON.stringify(result.error)); const output=result.value.normalized_result as any; const declarations=output.observations.webmcp_declarations; assert(declarations.length>0); for(const d of declarations) assert.deepEqual([d.name,d.description,d.schema_property_names],[name,description,properties]); console.log(JSON.stringify({label,declarations:declarations.map(({name,description,schema_property_names,trust}:any)=>({name,description,schema_property_names,trust}))})); } } finally {await browser.close();} ``` Run from the checkout root with isolated configuration and temporary directories: ```sh mkdir -p "$WORK/home" "$WORK/tmp" HOME="$WORK/home" TMPDIR="$WORK/tmp" \ XDG_CONFIG_HOME="$WORK/home/config" XDG_CACHE_HOME="$WORK/home/cache" \ node --import ./node_modules/tsx/dist/loader.mjs "$WORK/probe.mts" ``` One executed round completed all four cases; assertions record the faulty baseline outputs rather than asserting the desired correction. No build or real browser is required. ## Expected behavior Both inline literal-spread cases should retain the same name, description and input property names as the direct case. These bounded literals contain no unresolved keys or getters. The dynamic-spread control should remain unknown. The [browser guide](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/docs/browser-observation.md#L19) advertises static WebMCP declarations. The [source metadata contract](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/AGENTS.md#L13-L19) requires preserving metadata using its owning interpretation. The [shared static-property resolver](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/domain/javascript/javascriptAstValues.ts#L134-L183) already resolves inline object literals while preserving unresolved overrides. The executed direct declaration supplies a differential control for the exact expected metadata. ## Impact and scope A bounded source syntax case loses meaningful tool identity, description and input metadata in returned analysis Evidence. All three affected metadata fields were omitted in both tested literal-spread cases; the direct control retained them. This can make the static declaration inventory incomplete. Real-application frequency and downstream incidents are NOT_ENOUGH_INFO; they were not measured. The fixture source remains manually readable, but no automatic equivalent-output workaround was verified. No target execution or security-boundary crossing occurred. ## REA version Source main `3537e58c1ab29309f06eabf50bc52adba9817a0c`; package metadata 6.7.0. The published stable artifact was not executed. ## Runtime and platform Node.js v26.12.0; macOS 27.0.1, Darwin 27.0.0, arm64. Repository Node pin is 24.18.0; this probe used the package-supported >=26 runtime. Locked existing dependencies; no native analysis provider involved. ## Target and artifact context Four inert synthetic JavaScript sources supplied by the repository's fake CDP browser. The production capture, static analyzer, output-schema validation and application Evidence wrapper were exercised. No page tool was registered or invoked. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Reduced observed metadata, with every entry retaining `trust: "page-declared-untrusted"`: ```json {"label":"direct","name":"catalog","description":"Browse catalog","schema_property_names":["q"]} {"label":"literal-spread","name":null,"description":null,"schema_property_names":[]} {"label":"unrelated-literal-spread","name":null,"description":null,"schema_property_names":[]} {"label":"dynamic-spread-control","name":null,"description":null,"schema_property_names":[]} ``` The [production provider](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/browser/CdpBrowserProvider.ts#L164-L190) captures source and invokes the analyzer. The [application service](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/BrowserObservationService.ts#L80-L105) returns its result as Evidence. These are observed boundary results; public CLI/MCP transport, real Chrome and published-release runtime coverage remain NOT_ENOUGH_INFO. ## Typed outcome or recovery expected Keep `name: "catalog"`, `description: "Browse catalog"` and `schema_property_names: ["q"]` in both static literal-spread cases, with the existing untrusted designation. Preserve unknown metadata for genuinely unresolved spreads. ## Analysis or suspected cause Source-derived explanation: [objectValue](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/domain/webBundleAnalyzerAst.ts#L11-L38) clears its current value on every spread without inspecting its literal argument. [addWebMcpDeclaration](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/domain/webBundleAnalyzerInspection.ts#L356-L394) uses that helper for declaration and schema properties, then projects missing values to null and empty arrays. ## Additional context The relevant analyzer, helper, provider and application-service files have no source differences between tag `rea-agents-6.7.0` and the tested main revision. This establishes source equality only; no stable runtime reproduction is claimed. Related [#3987](https://github.com/morluto/rea/issues/3987) concerns an unrelated computed empty-template key clearing a name. Its trigger and restoration check differ from resolving inline literal spreads. [#3331](https://github.com/morluto/rea/issues/3331) concerns schema keywords falsely reported as input fields. Neither covers this omission.
Author
Owner

@SII-YDD commented on GitHub (Oct 19, 2026):

Executed a local shared-owner correction on main 9af0b611 (Node24.18.0/npm11.16.0, isolated Linux arm64). webBundleAnalyzerAst.objectValue currently repeats property interpretation and clears certainty at every spread. Reusing staticObjectProperty(object, name, isNullishJavaScriptSyntax) preserves literal spreads and rightmost overrides while retaining dynamic/getter uncertainty:

const result = staticObjectProperty(object, name, isNullishJavaScriptSyntax);
return result.status === "explicit" ? result.property.value : undefined;

Original producer + final regressions: 6 failed/6 passed,2 files. Corrected producer + adjacent browser/domain workflows: 74 passed,5 files. Controls include nested/unrelated literal spreads, later overrides, dynamic overrides versus later explicit keys, unknown computed keys, null spreads, and accessors. The shared owner's existing depth/uncertainty rules remain in force.

The public regression executes the compiled CLI and real SDK stdio MCP against the existing fake CDP HTTP/WebSocket fixture. It checks name/description/schema names, untrusted provenance, advertised schemas through connectLocalToolsMcp, missing-target failure -> same-connection ping -> successful analysis, closed CDP connections, and unopened binary session. No page evaluation or tool invocation occurs. Browser evidence is simulated CDP, not real Chrome qualification.

check:fast, check, check:ci, docs:check (5 consistency cases), discovery1,616 and diff checks pass. No schema/provider protocol changes or measured prevalence/performance claim. The patch is local, AI-assisted; no new PR, maintainer approval or adoption is claimed yet.

<!-- gh-comment-id:6261085803 --> @SII-YDD commented on GitHub (Oct 19, 2026): Executed a local shared-owner correction on main `9af0b611` (Node24.18.0/npm11.16.0, isolated Linux arm64). `webBundleAnalyzerAst.objectValue` currently repeats property interpretation and clears certainty at every spread. Reusing `staticObjectProperty(object, name, isNullishJavaScriptSyntax)` preserves literal spreads and rightmost overrides while retaining dynamic/getter uncertainty: ```ts const result = staticObjectProperty(object, name, isNullishJavaScriptSyntax); return result.status === "explicit" ? result.property.value : undefined; ``` Original producer + final regressions: **6 failed/6 passed,2 files**. Corrected producer + adjacent browser/domain workflows: **74 passed,5 files**. Controls include nested/unrelated literal spreads, later overrides, dynamic overrides versus later explicit keys, unknown computed keys, null spreads, and accessors. The shared owner's existing depth/uncertainty rules remain in force. The public regression executes the compiled CLI and real SDK stdio MCP against the existing fake CDP HTTP/WebSocket fixture. It checks name/description/schema names, untrusted provenance, advertised schemas through `connectLocalToolsMcp`, missing-target failure -> same-connection ping -> successful analysis, closed CDP connections, and unopened binary session. No page evaluation or tool invocation occurs. Browser evidence is simulated CDP, not real Chrome qualification. `check:fast`, `check`, `check:ci`, `docs:check` (5 consistency cases), discovery1,616 and diff checks pass. No schema/provider protocol changes or measured prevalence/performance claim. The patch is local, AI-assisted; no new PR, maintainer approval or adoption is claimed yet.
Author
Owner

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

Claiming this. Repro'd via the AST path in webBundleAnalyzerAst.ts (objectValue bails at any spread element and lets a later spread erase an already-resolved key); will follow up with a fix branch + regression tests shortly.

<!-- gh-comment-id:6277766795 --> @akushonkamen commented on GitHub (Oct 19, 2026): Claiming this. Repro'd via the AST path in `webBundleAnalyzerAst.ts` (`objectValue` bails at any spread element and lets a later spread erase an already-resolved key); will follow up with a fix branch + regression tests shortly.
Author
Owner

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

Progress update: fix branch pushed at github.com/akushonkamen/rea@0017364db (branch fix/5075-objectvalue-spread-metadata).

Root cause: objectValue in src/domain/webBundleAnalyzerAst.ts reset its accumulated value at every SpreadElement without inspecting the spread argument, so inline literal spreads lost their metadata and a later unrelated spread erased already-resolved keys.

Fix: objectValue now delegates to the shared staticObjectProperty resolver (src/domain/javascript/javascriptAstValues.ts), which recursively resolves bounded inline object-literal spreads, preserves rightmost explicit overrides, keeps getter-carrying spreads and unresolved/dynamic spreads unknown (fail-closed), and enforces the existing expression-depth limit. No changes to the fail-closed semantics for genuinely unresolvable input.

Tests: a new domain-level unit case in src/domain/webBundleAnalyzerAst.test.ts and a new boundary regression (tests/boundary/browser/webBundleWebMcpSpreadMetadata.test.ts) running the production CdpBrowserProvider.analyzeBundle against the repository fake CDP browser with all four issue-probe sources (direct, literal-spread, unrelated-literal-spread, dynamic-spread control). Both failed before the fix and pass after. npx vitest run --project domain (229 files / 4420 passed) and npx vitest run --project boundary are green. A PR will follow once creating one is unblocked; the branch is ready for review.

<!-- gh-comment-id:6278159445 --> @akushonkamen commented on GitHub (Oct 19, 2026): Progress update: fix branch pushed at https://github.com/akushonkamen/rea/commit/0017364db (branch `fix/5075-objectvalue-spread-metadata`). Root cause: `objectValue` in `src/domain/webBundleAnalyzerAst.ts` reset its accumulated value at every `SpreadElement` without inspecting the spread argument, so inline literal spreads lost their metadata and a later unrelated spread erased already-resolved keys. Fix: `objectValue` now delegates to the shared `staticObjectProperty` resolver (`src/domain/javascript/javascriptAstValues.ts`), which recursively resolves bounded inline object-literal spreads, preserves rightmost explicit overrides, keeps getter-carrying spreads and unresolved/dynamic spreads unknown (fail-closed), and enforces the existing expression-depth limit. No changes to the fail-closed semantics for genuinely unresolvable input. Tests: a new domain-level unit case in `src/domain/webBundleAnalyzerAst.test.ts` and a new boundary regression (`tests/boundary/browser/webBundleWebMcpSpreadMetadata.test.ts`) running the production `CdpBrowserProvider.analyzeBundle` against the repository fake CDP browser with all four issue-probe sources (direct, literal-spread, unrelated-literal-spread, dynamic-spread control). Both failed before the fix and pass after. `npx vitest run --project domain` (229 files / 4420 passed) and `npx vitest run --project boundary` are green. A PR will follow once creating one is unblocked; the branch is ready for review.
Author
Owner

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

Closing as fixed on main by github.com/morluto/rea@d23be5c089, retained at 67a6477f01.

Static WebMCP declaration analysis now reuses the shared bounded property resolver for inline literal spreads and keeps dynamic overrides unknown. Domain and CLI/MCP acceptance regressions cover the two reported literal cases and the dynamic control.

This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.

<!-- gh-comment-id:6324268666 --> @morluto commented on GitHub (Oct 21, 2026): Closing as fixed on main by https://github.com/morluto/rea/commit/d23be5c0898c39c9f0c54e2e0b43ee746d92d316, retained at 67a6477f011ae358fc333e5a6e26c86d932654ed. Static WebMCP declaration analysis now reuses the shared bounded property resolver for inline literal spreads and keeps dynamic overrides unknown. Domain and CLI/MCP acceptance regressions cover the two reported literal cases and the dynamic control. This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.
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#3114
No description provided.