[GH-ISSUE #4295] [Bug] Keyed archive objects holding data, dates or non-finite reals are labeled dictionary #1699

Closed
opened 2026-10-16 21:09:13 +00:00 by bsakaguchi · 1 comment
Owner

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

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

inspect_keyed_archive labels $objects entries that are plist data, plist dates or non-finite reals as kind: "dictionary". Strings, booleans, finite numbers and large integers in the same archive are labeled kind: "scalar". The labels describe REA's JSON wrapper ({"$plist_type":"data",…}) rather than the archived value. As a result, a real data object and a real dictionary with the same keys produce identical object records, and the result cannot tell them apart. The behavior is the same for XML and binary archives.

Observed on the release build. Both commands exited 0. The binary and XML archives produce the same object records:

{"id":1,"kind":"dictionary","status":"observed","value":{"$plist_type":"data","base64":"3q2+7w=="}}
{"id":2,"kind":"dictionary","status":"observed","value":{"$plist_type":"date","iso8601":"2001-01-01T00:00:00.000Z"}}
{"id":3,"kind":"scalar","status":"observed","value":{"$plist_type":"integer","decimal":"9007199254740993"}}
{"id":4,"kind":"scalar","status":"observed","value":"hello"}
{"id":5,"kind":"scalar","status":"observed","value":42}
{"id":6,"kind":"scalar","status":"observed","value":1.5}
{"id":7,"kind":"dictionary","status":"observed","value":{"$plist_type":"real","value":null}}
{"id":8,"kind":"dictionary","status":"observed","value":{"$plist_type":"data","base64":"3q2+7w=="}}

(class_id and class_name are null in every record and are omitted above.) Objects 1 and 8 are identical in every field, and no limitation explains the overlap. One limitation describes the {"$plist_type":"real","value":null} representation of object 7, but it does not mention its kind. The same labels appeared in separate runs with single-purpose fixtures (data, date and large integer; infinity and boolean; data and lookalike dictionary).

Reproduction

Use the release build stated under Runtime and platform. The fixture script needs Python 3.13 or later for plistlib's aware_datetime argument:

mkdir -p /tmp/rea-bug04 && cd /tmp/rea-bug04
python3 -I -c "
import plistlib, datetime
top = {k: plistlib.UID(i) for i, k in enumerate(['data','date','big','text','small','real','inf','lookalike'], 1)}
objs = ['\$null', bytes.fromhex('deadbeef'), datetime.datetime(2001,1,1,tzinfo=datetime.timezone.utc), 9007199254740993, 'hello', 42, 1.5, float('inf'), {'\$plist_type':'data','base64':'3q2+7w=='}]
open('archive.bin','wb').write(plistlib.dumps({'\$archiver':'NSKeyedArchiver','\$version':100000,'\$top':top,'\$objects':objs}, fmt=plistlib.FMT_BINARY, aware_datetime=True))"
plutil -convert xml1 -o archive.xml archive.bin
plutil -p archive.bin
npx -y rea-agents@6.5.0 inspect-keyed-archive ./archive.bin --json
npx -y rea-agents@6.5.0 inspect-keyed-archive ./archive.xml --json

Object 1 is plist data, object 2 a plist date, object 7 a positive-infinity real, and object 8 a real dictionary whose keys imitate REA's data wrapper. plutil -p shows object 1 as {length = 4, bytes = 0xdeadbeef} and object 8 as a two-key dictionary.

Expected behavior

Apple's CFPropertyList reference lists the property list object types as distinct types: "CFData, CFString, CFArray, CFDictionary, CFDate, CFBoolean, and CFNumber". Data, dates and numbers are primitive values, not dictionaries. REA's inspect_keyed_archive result schema offers kind values nil, scalar, array, dictionary, class-descriptor and archived-object, and REA already labels strings, booleans, finite numbers and exact large integers ({"$plist_type":"integer",…}) as scalar. Under the same rule, objects 1, 2 and 7 should be scalar, and only object 8 should be dictionary. If REA intends dictionary to describe its JSON representation, the result should say so, because otherwise objects 1 and 8 are indistinguishable.

Impact and scope

Workaround: Treating a $plist_type key in an object's value as a type tag, rather than trusting kind, identifies data, dates and non-finite reals in ordinary archives. Where a lookalike dictionary is possible, plutil -p on the archive shows each $objects entry's real type, for example {length = 4, bytes = 0xdeadbeef} for data.

REA version

6.5.0 (tag rea-agents-6.5.0; classification unchanged on main)

Runtime and platform

  • REA: rea-agents 6.5.0 from npm (--version prints 6.5.0), release tag rea-agents-6.5.0 at commit 3b79af28e80785afce9f5998d8f03289402a1847. Main at d52a488 leaves the classification unchanged. Its keyedArchive.ts diff touches only reference scanning, and the dictionary branch moves to line 218.
  • Host: macOS 27.0.1 (26A434) arm64, system plutil, Python 3.14 standard library, Node.js v26.10.0.

Target and artifact context

self-built NSKeyedArchiver plist, binary and XML forms, whose $objects hold data, a date, a large integer, a string, numbers, an infinite real and a lookalike dictionary

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: Apple's CFPropertyList reference (cited above) for the distinct primitive types, plus REA's own scalar labeling of strings, numbers, booleans and exact large integers in the same result. The kind enum is defined at keyedArchive.ts L45-L52. REA's docs do not define kind further.
  • Failure source: rea inspect-keyed-archive ./archive.bin --json and ./archive.xml --json on the release build, objects 1, 2, 7 and 8 above.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

This investigator ran the fixture above on the release build. Suspected cause, from source: projectPlistValue first wraps data, dates and non-finite reals into $plist_type objects (plistValue.ts L31-L41). projectKeyedArchive then classifies every non-array object without $classname or $class as dictionary (keyedArchive.ts L194-L204). Large integers are wrapped only later by the number projection (KeyedArchiveReader.ts L78-L82), so they keep scalar.

Typed outcome or recovery expected

With the fixture above, objects 1, 2 and 7 must no longer be labeled dictionary. They should be scalar, consistent with objects 3 to 6. Object 8 must stay dictionary, so that objects 1 and 8 become distinguishable from the result alone. Both the binary and the XML archive must give the same labels. Discriminating control: an archived object with $class stays archived-object, and its class descriptor stays class-descriptor.

Analysis or suspected cause

No response

Additional context

  • Foundation's NSKeyedArchiver stores Data values directly in $objects as plist data. An archive written by Foundation that includes Data showed the same dictionary label, so the issue is not limited to hand-built fixtures.
  • Related but different: merged PR #870 fixed dictionaries keyed UID being read as references. Open #3373, #4042 and #3876 concern numeric values, not object labels.
Originally created by @coygeek on GitHub (Oct 16, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4295 ### 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 `inspect_keyed_archive` labels `$objects` entries that are plist data, plist dates or non-finite reals as `kind: "dictionary"`. Strings, booleans, finite numbers and large integers in the same archive are labeled `kind: "scalar"`. The labels describe REA's JSON wrapper (`{"$plist_type":"data",…}`) rather than the archived value. As a result, a real data object and a real dictionary with the same keys produce identical object records, and the result cannot tell them apart. The behavior is the same for XML and binary archives. Observed on the release build. Both commands exited 0. The binary and XML archives produce the same object records: ```json {"id":1,"kind":"dictionary","status":"observed","value":{"$plist_type":"data","base64":"3q2+7w=="}} {"id":2,"kind":"dictionary","status":"observed","value":{"$plist_type":"date","iso8601":"2001-01-01T00:00:00.000Z"}} {"id":3,"kind":"scalar","status":"observed","value":{"$plist_type":"integer","decimal":"9007199254740993"}} {"id":4,"kind":"scalar","status":"observed","value":"hello"} {"id":5,"kind":"scalar","status":"observed","value":42} {"id":6,"kind":"scalar","status":"observed","value":1.5} {"id":7,"kind":"dictionary","status":"observed","value":{"$plist_type":"real","value":null}} {"id":8,"kind":"dictionary","status":"observed","value":{"$plist_type":"data","base64":"3q2+7w=="}} ``` (`class_id` and `class_name` are `null` in every record and are omitted above.) Objects 1 and 8 are identical in every field, and no limitation explains the overlap. One limitation describes the `{"$plist_type":"real","value":null}` representation of object 7, but it does not mention its `kind`. The same labels appeared in separate runs with single-purpose fixtures (data, date and large integer; infinity and boolean; data and lookalike dictionary). ### Reproduction Use the release build stated under Runtime and platform. The fixture script needs Python 3.13 or later for `plistlib`'s `aware_datetime` argument: ```sh mkdir -p /tmp/rea-bug04 && cd /tmp/rea-bug04 python3 -I -c " import plistlib, datetime top = {k: plistlib.UID(i) for i, k in enumerate(['data','date','big','text','small','real','inf','lookalike'], 1)} objs = ['\$null', bytes.fromhex('deadbeef'), datetime.datetime(2001,1,1,tzinfo=datetime.timezone.utc), 9007199254740993, 'hello', 42, 1.5, float('inf'), {'\$plist_type':'data','base64':'3q2+7w=='}] open('archive.bin','wb').write(plistlib.dumps({'\$archiver':'NSKeyedArchiver','\$version':100000,'\$top':top,'\$objects':objs}, fmt=plistlib.FMT_BINARY, aware_datetime=True))" plutil -convert xml1 -o archive.xml archive.bin plutil -p archive.bin npx -y rea-agents@6.5.0 inspect-keyed-archive ./archive.bin --json npx -y rea-agents@6.5.0 inspect-keyed-archive ./archive.xml --json ``` Object 1 is plist data, object 2 a plist date, object 7 a positive-infinity real, and object 8 a real dictionary whose keys imitate REA's data wrapper. `plutil -p` shows object 1 as `{length = 4, bytes = 0xdeadbeef}` and object 8 as a two-key dictionary. ### Expected behavior Apple's [CFPropertyList reference](https://developer.apple.com/documentation/corefoundation/cfpropertylist) lists the property list object types as distinct types: "CFData, CFString, CFArray, CFDictionary, CFDate, CFBoolean, and CFNumber". Data, dates and numbers are primitive values, not dictionaries. REA's `inspect_keyed_archive` result schema offers `kind` values `nil`, `scalar`, `array`, `dictionary`, `class-descriptor` and `archived-object`, and REA already labels strings, booleans, finite numbers and exact large integers (`{"$plist_type":"integer",…}`) as `scalar`. Under the same rule, objects 1, 2 and 7 should be `scalar`, and only object 8 should be `dictionary`. If REA intends `dictionary` to describe its JSON representation, the result should say so, because otherwise objects 1 and 8 are indistinguishable. ### Impact and scope Workaround: Treating a `$plist_type` key in an object's `value` as a type tag, rather than trusting `kind`, identifies data, dates and non-finite reals in ordinary archives. Where a lookalike dictionary is possible, `plutil -p` on the archive shows each `$objects` entry's real type, for example `{length = 4, bytes = 0xdeadbeef}` for data. ### REA version 6.5.0 (tag `rea-agents-6.5.0`; classification unchanged on main) ### Runtime and platform - REA: `rea-agents` 6.5.0 from npm (`--version` prints `6.5.0`), release tag `rea-agents-6.5.0` at commit `3b79af28e80785afce9f5998d8f03289402a1847`. Main at `d52a488` leaves the classification unchanged. Its `keyedArchive.ts` diff touches only reference scanning, and the `dictionary` branch moves to line 218. - Host: macOS 27.0.1 (26A434) arm64, system `plutil`, Python 3.14 standard library, Node.js v26.10.0. ### Target and artifact context self-built `NSKeyedArchiver` plist, binary and XML forms, whose `$objects` hold data, a date, a large integer, a string, numbers, an infinite real and a lookalike dictionary ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: Apple's CFPropertyList reference (cited above) for the distinct primitive types, plus REA's own `scalar` labeling of strings, numbers, booleans and exact large integers in the same result. The `kind` enum is defined at [keyedArchive.ts L45-L52](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/domain/apple/keyedArchive.ts#L45-L52). REA's docs do not define `kind` further. - Failure source: `rea inspect-keyed-archive ./archive.bin --json` and `./archive.xml --json` on the release build, objects 1, 2, 7 and 8 above. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete This investigator ran the fixture above on the release build. Suspected cause, from source: `projectPlistValue` first wraps data, dates and non-finite reals into `$plist_type` objects ([plistValue.ts L31-L41](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/domain/apple/plistValue.ts#L31-L41)). `projectKeyedArchive` then classifies every non-array object without `$classname` or `$class` as `dictionary` ([keyedArchive.ts L194-L204](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/domain/apple/keyedArchive.ts#L194-L204)). Large integers are wrapped only later by the number projection ([KeyedArchiveReader.ts L78-L82](https://github.com/morluto/rea/blob/3b79af28e80785afce9f5998d8f03289402a1847/src/artifacts/apple/KeyedArchiveReader.ts#L78-L82)), so they keep `scalar`. ### Typed outcome or recovery expected With the fixture above, objects 1, 2 and 7 must no longer be labeled `dictionary`. They should be `scalar`, consistent with objects 3 to 6. Object 8 must stay `dictionary`, so that objects 1 and 8 become distinguishable from the result alone. Both the binary and the XML archive must give the same labels. Discriminating control: an archived object with `$class` stays `archived-object`, and its class descriptor stays `class-descriptor`. ### Analysis or suspected cause _No response_ ### Additional context - Foundation's `NSKeyedArchiver` stores `Data` values directly in `$objects` as plist data. An archive written by Foundation that includes `Data` showed the same `dictionary` label, so the issue is not limited to hand-built fixtures. - Related but different: merged PR #870 fixed dictionaries keyed `UID` being read as references. Open #3373, #4042 and #3876 concern numeric values, not object labels.
Author
Owner

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

Fixed on main in 2cf9c28f2.

The graph owner now projects decoded plist values and retains generated scalar identity through all dictionary/UID decisions. Literal wrapper-shaped dictionaries remain dictionaries; scalar $top values are rejected as malformed. The intermediate JSON clone is removed. Numeric projection, unknown-value reporting, source bytes and binary prototype-key partial evidence remain covered.

Adapted the relevant scalar proposal from @Jax367 in #4317, with additional scalar-root and prototype controls; unrelated PR changes were excluded. The captured Foundation data-node expectation and its manifest digest were corrected after reviewing the unchanged producer bytes/source; this was not a new Foundation capture.

Validation: 175 distinct focused checks across 23 files passed, one skipped; build, check:fast, docs:check and commit/push hooks passed. Both XML and binary cases were exercised through the built CLI and actual stdio MCP. Baseline controls reproduced the incorrect kinds and scalar root acceptance. 24 tiny completed CLI cost comparisons retained ordinary full replies and Evidence IDs byte for byte; no statistical speed claim. No heavy profiling, investigated-target execution or real deep-provider verification.

<!-- gh-comment-id:6233819525 --> @morluto commented on GitHub (Oct 17, 2026): Fixed on main in 2cf9c28f2. The graph owner now projects decoded plist values and retains generated scalar identity through all dictionary/UID decisions. Literal wrapper-shaped dictionaries remain dictionaries; scalar `$top` values are rejected as malformed. The intermediate JSON clone is removed. Numeric projection, unknown-value reporting, source bytes and binary prototype-key partial evidence remain covered. Adapted the relevant scalar proposal from @Jax367 in #4317, with additional scalar-root and prototype controls; unrelated PR changes were excluded. The captured Foundation data-node expectation and its manifest digest were corrected after reviewing the unchanged producer bytes/source; this was not a new Foundation capture. Validation: 175 distinct focused checks across 23 files passed, one skipped; build, check:fast, docs:check and commit/push hooks passed. Both XML and binary cases were exercised through the built CLI and actual stdio MCP. Baseline controls reproduced the incorrect kinds and scalar root acceptance. 24 tiny completed CLI cost comparisons retained ordinary full replies and Evidence IDs byte for byte; no statistical speed claim. No heavy profiling, investigated-target execution or real deep-provider verification.
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#1699
No description provided.