[GH-ISSUE #6817] fix(browser): keep opaque cookie partitions unknown in storage comparisons #4163

Closed
opened 2026-10-31 00:47:19 +00:00 by bsakaguchi · 1 comment
Owner

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

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 CDP storage adapter treats a cookie with partitionKeyOpaque: true and no serializable partitionKey as an ordinary unpartitioned cookie. It emits the same identity and value fingerprints for both inputs and marks the opaque observation fingerprints_complete: true. Passing these actual adapter outputs into production compareWebCaptures returns storage unchanged.

This is an offline production-boundary reproduction with generated CDP replies, not a live-browser capture. A control with an explicit partition key produces different identity fingerprints and storage changed.

Reproduction

Use a source checkout at the revision below with locked dependencies and dist/ built. Save this self-contained script as opaque-cookie.mjs outside the checkout, then run it from the checkout root with Node 24.18.0:

node /path/to/opaque-cookie.mjs

The endpoint and URLs are inert fixture identities. The replacement connection returns generated replies; no browser or network target is contacted. This code was executed once, including all three cases and assertions, before this draft was written.

import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';
const load = (p) => import(pathToFileURL(resolve(p)).href);
const {captureStorage} = await load('dist/browser/CdpCaptureStorage.js');
const {inspectWebPageInputSchema} = await load('dist/domain/browserObservation.js');
const {webPageInspectionSchema} = await load('dist/domain/browserObservationSchemas.js');
const {compareWebCapturesInputSchema} = await load('dist/domain/webCaptureDiffSchemas.js');
const {compareWebCaptures} = await load('dist/domain/webCaptureDiff.js');
const origin = 'https://example.test';
const cookie = {name:'fixture',value:'same',domain:'example.test',path:'/',expires:-1,size:11,httpOnly:false,secure:false,session:true,sameSite:'Lax',priority:'Medium',sameParty:false,sourceScheme:'Secure',sourcePort:443};
async function storage(extra) {
  const replies = {
    'Storage.getUsageAndQuota': {usage:0,quota:1000},
    'DOMStorage.getDOMStorageItems': {entries:[]},
    'IndexedDB.requestDatabaseNames': {databaseNames:[]},
    'CacheStorage.requestCacheNames': {caches:[]},
    'Network.getCookies': {cookies:[{...cookie,...extra}]}
  };
  return (await captureStorage({
    connection:{async send(method){assert.ok(method in replies); return replies[method];}},
    sessionId:undefined,
    input:inspectWebPageInputSchema.parse({cdp_endpoint:'http://127.0.0.1:9222',target_id:'fixture',include_storage_keys:true,include_storage_fingerprints:true})
  },origin+'/',[])).value;
}
const plain = await storage({partitionKeyOpaque:false});
const opaque = await storage({partitionKeyOpaque:true});
const known = await storage({partitionKey:{topLevelSite:'https://top.example.test',hasCrossSiteAncestor:true},partitionKeyOpaque:false});
const at = new Date().toISOString();
function inspection(storage) {
  return webPageInspectionSchema.parse({
    browser:{product:'fixture',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'},
    target:{target_id:'fixture',type:'page',title:'Fixture',url:origin+'/',origin,attached:false},
    capture_window:{started_at:at,ended_at:at,observation_ms:0},
    completeness:{status:'complete_within_window',conditions:['complete_within_window'],policy_filtered_sections:[],attach_limited_sections:[],truncated_sections:[],unavailable_sections:[],excluded:[],dropped_events:Object.fromEntries(['scripts','network_requests','console_events','websocket_connections','websocket_frames','webmcp_tools','timeline_events','total'].map(k=>[k,0]))},
    frames:[],dom:{total_nodes:0,nodes:[]},accessibility:{total_nodes:0,nodes:[],text_capture:{status:'not_approved',retained_bytes:0,excluded_fields:0}},scripts:{total:0,items:[]},resources:[],workers:[],
    network:{requests:[],websocket_connections:[],coverage_started_at:at,prior_activity_available:false},console:{events:[],coverage_started_at:at,prior_activity_available:false},
    metadata:{responses:[],dom_urls:[],agent_hints:[],excluded_dom_urls:0,headers_allowlisted:true},storage,limitations:[]
  });
}
const compare = (a,b) => compareWebCaptures(compareWebCapturesInputSchema.parse({before:{inspection:inspection(a)},after:{inspection:inspection(b)}})).dimensions.storage;
console.log(JSON.stringify({case:'opaque-vs-unpartitioned',opaque_complete:opaque.fingerprints_complete,same_identity:plain.content_fingerprints[0].identity_sha256===opaque.content_fingerprints[0].identity_sha256,comparison:compare(plain,opaque)}));
console.log(JSON.stringify({case:'opaque-vs-opaque',comparison:compare(opaque,opaque)}));
console.log(JSON.stringify({case:'known-partition-control',same_identity:plain.content_fingerprints[0].identity_sha256===known.content_fingerprints[0].identity_sha256,comparison:compare(plain,known)}));
assert.equal(opaque.fingerprints_complete,true);
assert.deepEqual(plain.content_fingerprints,opaque.content_fingerprints);
assert.equal(compare(plain,opaque).status,'unchanged');
assert.equal(compare(plain,known).status,'changed');

Expected behavior

An opaque cookie partition must remain an unresolved identity. The adapter should preserve that uncertainty and prevent a full storage unchanged claim, including when two opaque observations have otherwise identical fields. The known-partition control should remain comparable.

The bundled Playwright protocol declares optional Cookie.partitionKeyOpaque as the indicator of an opaque partition key. The official protocol definition also distinguishes it from optional partitionKey. The REA storage comparison guide says fingerprints support comparison only when storage was captured completely. Repository invariants require unresolved identity to remain unknown rather than aligned.

Impact and scope

P2: supported CDP evidence can produce a false storage-equality result because an explicitly opaque partition identity is collapsed into the unpartitioned identity. The runtime evidence establishes the adapter and domain-comparison boundary for one generated cookie. It does not establish how frequently real Chromium returns these cookies, nor does it establish a live browser, CLI or MCP reproduction. No cookie store was read or changed.

A practical workaround preserving partition identity has not been demonstrated. Callers with the raw CDP response can recognize the opaque flag, but REA's projected fingerprints discard it.

REA version

rea-agents 6.7.0, published tag rea-agents-6.7.0; source revision 27dc42d10249f0410904a75c0997842eb03b931c.

Runtime and platform

Node.js v24.18.0; Linux 7.2.8-9-omarchy; x86_64. Locked npm dependencies and the compiled runtime from this revision were used. No browser runtime was started.

Target and artifact context

Generated valid-shape Network.getCookies replies for a single benign cookie, empty DOM storage, IndexedDB and Cache Storage inventories, and complete fixed usage/quota responses. Each result is embedded into a schema-validated passive inspection before comparison.

Interface

Other

Earliest failure stage

Capture or observation

Relevant output or evidence

The executed assertions passed. Output, shortened to the decision fields:

{"case":"opaque-vs-unpartitioned","opaque_complete":true,"same_identity":true,"comparison":{"total_changes":0,"changes":[],"status":"unchanged","reason":null}}
{"case":"opaque-vs-opaque","comparison":{"total_changes":0,"changes":[],"status":"unchanged","reason":null}}
{"case":"known-partition-control","same_identity":false,"comparison":{"status":"changed","total_changes":2}}

The source boundary is addCookies: its identity uses cookie.partitionKey ?? null, never inspects partitionKeyOpaque, and unconditionally passes complete: true. The comparison consumes complete fingerprint identities.

Typed outcome or recovery expected

Retain available cookie value evidence while marking partition identity and aggregate fingerprint coverage incomplete. The storage comparison should return unknown when no independently observed difference establishes a change.

Analysis or suspected cause

Source-derived: optional partitionKey has two meanings here: an unpartitioned cookie, or an opaque partition that cannot be represented by that field. Reading only partitionKey erases the distinction. The control shows that the comparator can distinguish serializable partition keys; the information loss occurs earlier in the producer projection.

Alternative explanation considered: equal cookie names and values might justify equality despite an unknown partition. That cannot establish equality of partition-bound storage identities, and the explicit-key control already treats partition differences as distinct entries. The existing storage tests cover unavailable/malformed inventories and incomplete IndexedDB objects; they do not cover opaque cookie partitions.

Additional context

Inspected current main 567b55947486516b9d35d30b7202ac69c6f40f6d before drafting. Its cookie identity and completeness logic is unchanged; differences in this module concern base64 decoding for Cache Storage. Main was inspected, not executed.

No previous good revision is established. The initial open-issue snapshot did not contain a matching opaque-cookie/partition report.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed production adapter and comparator turn explicitly opaque partition identity into complete fingerprints and an unchanged storage result; the serializable-partition control returns changed. Real-browser frequency and public transport behavior remain untested.
  • Workaround status: unverified
  • Workaround basis: Reading the raw CDP opaque flag could tell an analyst to reject the equality conclusion, but no REA workflow retaining that identity uncertainty was demonstrated.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6817 ## 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 CDP storage adapter treats a cookie with `partitionKeyOpaque: true` and no serializable `partitionKey` as an ordinary unpartitioned cookie. It emits the same identity and value fingerprints for both inputs and marks the opaque observation `fingerprints_complete: true`. Passing these actual adapter outputs into production `compareWebCaptures` returns storage `unchanged`. This is an offline production-boundary reproduction with generated CDP replies, not a live-browser capture. A control with an explicit partition key produces different identity fingerprints and storage `changed`. ## Reproduction Use a source checkout at the revision below with locked dependencies and `dist/` built. Save this self-contained script as `opaque-cookie.mjs` outside the checkout, then run it from the checkout root with Node 24.18.0: ```sh node /path/to/opaque-cookie.mjs ``` The endpoint and URLs are inert fixture identities. The replacement connection returns generated replies; no browser or network target is contacted. This code was executed once, including all three cases and assertions, before this draft was written. ```js import assert from 'node:assert/strict'; import { pathToFileURL } from 'node:url'; import { resolve } from 'node:path'; const load = (p) => import(pathToFileURL(resolve(p)).href); const {captureStorage} = await load('dist/browser/CdpCaptureStorage.js'); const {inspectWebPageInputSchema} = await load('dist/domain/browserObservation.js'); const {webPageInspectionSchema} = await load('dist/domain/browserObservationSchemas.js'); const {compareWebCapturesInputSchema} = await load('dist/domain/webCaptureDiffSchemas.js'); const {compareWebCaptures} = await load('dist/domain/webCaptureDiff.js'); const origin = 'https://example.test'; const cookie = {name:'fixture',value:'same',domain:'example.test',path:'/',expires:-1,size:11,httpOnly:false,secure:false,session:true,sameSite:'Lax',priority:'Medium',sameParty:false,sourceScheme:'Secure',sourcePort:443}; async function storage(extra) { const replies = { 'Storage.getUsageAndQuota': {usage:0,quota:1000}, 'DOMStorage.getDOMStorageItems': {entries:[]}, 'IndexedDB.requestDatabaseNames': {databaseNames:[]}, 'CacheStorage.requestCacheNames': {caches:[]}, 'Network.getCookies': {cookies:[{...cookie,...extra}]} }; return (await captureStorage({ connection:{async send(method){assert.ok(method in replies); return replies[method];}}, sessionId:undefined, input:inspectWebPageInputSchema.parse({cdp_endpoint:'http://127.0.0.1:9222',target_id:'fixture',include_storage_keys:true,include_storage_fingerprints:true}) },origin+'/',[])).value; } const plain = await storage({partitionKeyOpaque:false}); const opaque = await storage({partitionKeyOpaque:true}); const known = await storage({partitionKey:{topLevelSite:'https://top.example.test',hasCrossSiteAncestor:true},partitionKeyOpaque:false}); const at = new Date().toISOString(); function inspection(storage) { return webPageInspectionSchema.parse({ browser:{product:'fixture',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'}, target:{target_id:'fixture',type:'page',title:'Fixture',url:origin+'/',origin,attached:false}, capture_window:{started_at:at,ended_at:at,observation_ms:0}, completeness:{status:'complete_within_window',conditions:['complete_within_window'],policy_filtered_sections:[],attach_limited_sections:[],truncated_sections:[],unavailable_sections:[],excluded:[],dropped_events:Object.fromEntries(['scripts','network_requests','console_events','websocket_connections','websocket_frames','webmcp_tools','timeline_events','total'].map(k=>[k,0]))}, frames:[],dom:{total_nodes:0,nodes:[]},accessibility:{total_nodes:0,nodes:[],text_capture:{status:'not_approved',retained_bytes:0,excluded_fields:0}},scripts:{total:0,items:[]},resources:[],workers:[], network:{requests:[],websocket_connections:[],coverage_started_at:at,prior_activity_available:false},console:{events:[],coverage_started_at:at,prior_activity_available:false}, metadata:{responses:[],dom_urls:[],agent_hints:[],excluded_dom_urls:0,headers_allowlisted:true},storage,limitations:[] }); } const compare = (a,b) => compareWebCaptures(compareWebCapturesInputSchema.parse({before:{inspection:inspection(a)},after:{inspection:inspection(b)}})).dimensions.storage; console.log(JSON.stringify({case:'opaque-vs-unpartitioned',opaque_complete:opaque.fingerprints_complete,same_identity:plain.content_fingerprints[0].identity_sha256===opaque.content_fingerprints[0].identity_sha256,comparison:compare(plain,opaque)})); console.log(JSON.stringify({case:'opaque-vs-opaque',comparison:compare(opaque,opaque)})); console.log(JSON.stringify({case:'known-partition-control',same_identity:plain.content_fingerprints[0].identity_sha256===known.content_fingerprints[0].identity_sha256,comparison:compare(plain,known)})); assert.equal(opaque.fingerprints_complete,true); assert.deepEqual(plain.content_fingerprints,opaque.content_fingerprints); assert.equal(compare(plain,opaque).status,'unchanged'); assert.equal(compare(plain,known).status,'changed'); ``` ## Expected behavior An opaque cookie partition must remain an unresolved identity. The adapter should preserve that uncertainty and prevent a full storage `unchanged` claim, including when two opaque observations have otherwise identical fields. The known-partition control should remain comparable. The bundled Playwright protocol declares optional `Cookie.partitionKeyOpaque` as the indicator of an opaque partition key. The [official protocol definition](https://github.com/ChromeDevTools/devtools-protocol/blob/master/json/browser_protocol.json) also distinguishes it from optional `partitionKey`. The REA [storage comparison guide](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/docs/browser-observation.md#L331) says fingerprints support comparison only when storage was captured completely. [Repository invariants](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/AGENTS.md) require unresolved identity to remain unknown rather than aligned. ## Impact and scope P2: supported CDP evidence can produce a false storage-equality result because an explicitly opaque partition identity is collapsed into the unpartitioned identity. The runtime evidence establishes the adapter and domain-comparison boundary for one generated cookie. It does not establish how frequently real Chromium returns these cookies, nor does it establish a live browser, CLI or MCP reproduction. No cookie store was read or changed. A practical workaround preserving partition identity has not been demonstrated. Callers with the raw CDP response can recognize the opaque flag, but REA's projected fingerprints discard it. ## REA version rea-agents 6.7.0, published tag `rea-agents-6.7.0`; source revision `27dc42d10249f0410904a75c0997842eb03b931c`. ## Runtime and platform Node.js v24.18.0; Linux 7.2.8-9-omarchy; x86_64. Locked npm dependencies and the compiled runtime from this revision were used. No browser runtime was started. ## Target and artifact context Generated valid-shape `Network.getCookies` replies for a single benign cookie, empty DOM storage, IndexedDB and Cache Storage inventories, and complete fixed usage/quota responses. Each result is embedded into a schema-validated passive inspection before comparison. ## Interface Other ## Earliest failure stage Capture or observation ## Relevant output or evidence The executed assertions passed. Output, shortened to the decision fields: ```json {"case":"opaque-vs-unpartitioned","opaque_complete":true,"same_identity":true,"comparison":{"total_changes":0,"changes":[],"status":"unchanged","reason":null}} {"case":"opaque-vs-opaque","comparison":{"total_changes":0,"changes":[],"status":"unchanged","reason":null}} {"case":"known-partition-control","same_identity":false,"comparison":{"status":"changed","total_changes":2}} ``` The source boundary is [`addCookies`](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/src/browser/CdpCaptureStorageFingerprints.ts#L134-L160): its identity uses `cookie.partitionKey ?? null`, never inspects `partitionKeyOpaque`, and unconditionally passes `complete: true`. The [comparison consumes complete fingerprint identities](https://github.com/morluto/rea/blob/27dc42d10249f0410904a75c0997842eb03b931c/src/domain/webCaptureDiff.ts#L957-L981). ## Typed outcome or recovery expected Retain available cookie value evidence while marking partition identity and aggregate fingerprint coverage incomplete. The storage comparison should return `unknown` when no independently observed difference establishes a change. ## Analysis or suspected cause Source-derived: optional `partitionKey` has two meanings here: an unpartitioned cookie, or an opaque partition that cannot be represented by that field. Reading only `partitionKey` erases the distinction. The control shows that the comparator can distinguish serializable partition keys; the information loss occurs earlier in the producer projection. Alternative explanation considered: equal cookie names and values might justify equality despite an unknown partition. That cannot establish equality of partition-bound storage identities, and the explicit-key control already treats partition differences as distinct entries. The existing storage tests cover unavailable/malformed inventories and incomplete IndexedDB objects; they do not cover opaque cookie partitions. ## Additional context Inspected current main `567b55947486516b9d35d30b7202ac69c6f40f6d` before drafting. Its cookie identity and completeness logic is unchanged; differences in this module concern base64 decoding for Cache Storage. Main was inspected, not executed. No previous good revision is established. The initial open-issue snapshot did not contain a matching opaque-cookie/partition report. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed production adapter and comparator turn explicitly opaque partition identity into complete fingerprints and an unchanged storage result; the serializable-partition control returns changed. Real-browser frequency and public transport behavior remain untested. - Workaround status: unverified - Workaround basis: Reading the raw CDP opaque flag could tell an analyst to reject the equality conclusion, but no REA workflow retaining that identity uncertainty was demonstrated.
Author
Owner

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

Reopening for the remaining identity question: main now marks an opaque partition fingerprint incomplete, which prevents the reported definite unchanged comparison, but the producer still hashes an absent partition key as null, the same identity input used for an unpartitioned cookie. #7203 proposes distinguishing that opaque case. This is a source comparison; I have not rerun the reported reproduction.

<!-- gh-comment-id:6428194168 --> @morluto commented on GitHub (Oct 26, 2026): Reopening for the remaining identity question: main now marks an opaque partition fingerprint incomplete, which prevents the reported definite unchanged comparison, but the producer still hashes an absent partition key as `null`, the same identity input used for an unpartitioned cookie. #7203 proposes distinguishing that opaque case. This is a source comparison; I have not rerun the reported reproduction.
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#4163
No description provided.