[GH-ISSUE #6776] [Bug] Authored className fields overwrite and disappear from keyed-nib class evidence #4132

Closed
opened 2026-10-31 00:47:03 +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/6776

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 analyzeInterfaceBuilderBundle reader reports an ordinary authored className: "StoredLabel" field as the object's archive class, replacing the $class descriptor's ArchiveProbeView. Reordering only those dictionary entries restores the class name but still loses the authored field. All coverage facets report complete with zero omissions, and graph.truncated is false.

The generic decodeKeyedArchiveBytes reader retains class_name: "ArchiveProbeView" and the authored field for the same bytes. Renaming the field to modelClassName also preserves both facts in the Interface Builder graph. These were source-boundary executions; CLI/MCP admission and transport were not run.

Reproduction

From the source checkout with locked dependencies installed, save the script below as /tmp/rea-classname-probe.mts, then run:

node --import ./node_modules/tsx/dist/loader.mjs /tmp/rea-classname-probe.mts "$PWD"

The executed script used the same code and arguments, with a different scratch script location. It ran each variant once in one process, exited 0, and removed its generated bundles. It generates inert XML keyed archives and invokes the production bundle analyzer and generic keyed-archive decoder; no application class is instantiated.

import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';

const repo = resolve(process.argv[2]!);
const { analyzeInterfaceBuilderBundle } = await import(pathToFileURL(join(repo, 'src/artifacts/apple/InterfaceBuilderAnalysis.ts')).href);
const { decodeKeyedArchiveBytes } = await import(pathToFileURL(join(repo, 'src/artifacts/apple/KeyedArchiveReader.ts')).href);
const uid = (n: number) => ({ 'CF$UID': n });
function xml(value: unknown): string {
  if (typeof value === 'number') return '<integer>' + value + '</integer>';
  if (typeof value === 'string') return '<string>' + value + '</string>';
  if (Array.isArray(value)) return '<array>' + value.map(xml).join('') + '</array>';
  return '<dict>' + Object.entries(value as object).map(([k,v]) => '<key>' + k + '</key>' + xml(v)).join('') + '</dict>';
}
const temporary = await mkdtemp('/tmp/rea-classname-');
try {
  for (const variant of ['control', 'class-first', 'class-last']) {
    const field = variant === 'control' ? 'modelClassName' : 'className';
    const data = { [field]: 'StoredLabel', title: 'Fixture' };
    const object = variant === 'class-last'
      ? { ...data, $class: uid(2) }
      : { $class: uid(2), ...data };
    const archive = {
      $archiver: 'NSKeyedArchiver', $version: 100000,
      $objects: ['$null', object, { $classname: 'ArchiveProbeView', $classes: ['ArchiveProbeView', 'NSView', 'NSObject'] }],
      $top: { root: uid(1) },
    };
    const bytes = Buffer.from('<?xml version="1.0"?><plist version="1.0">' + xml(archive) + '</plist>');
    const bundlePath = join(temporary, variant + '.app');
    await mkdir(join(bundlePath, 'Contents/Resources'), { recursive: true });
    await writeFile(join(bundlePath, 'Contents/Resources/Main.nib'), bytes);
    const result = await analyzeInterfaceBuilderBundle({ bundlePath, targetSha256: 'a'.repeat(64) });
    const keyed = decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 20 });
    console.log(JSON.stringify({ variant,
      object: result.graph.nodes.find((n: any) => n.attributes.interface_builder_object_id === '1'),
      document: result.documents[0], coverage: result.graph.coverage, truncated: result.graph.truncated,
      keyedObject: keyed.objects.find((n: any) => n.id === 1),
    }));
  }
} finally { await rm(temporary, { recursive: true, force: true }); }

Expected behavior

Report ArchiveProbeView as the class resolved from $class in both dictionary orders, and preserve className: "StoredLabel" as ordinary serialized data. Preserve modelClassName in the control. Dictionary ordering must not change class identity.

docs/native-investigation.md:459-461 promises controller/class names from compiled nib resources, and src/contracts/artifactToolContracts.ts:92-96 explicitly includes keyed property lists. AGENTS.md:19 requires preserved identity and source evidence. The fixture has a valid, in-bounds class reference, and the generic decoder resolves it correctly. The source declares no reservation of an application's className field as metadata.

Impact and scope

Observed non-security evidence corruption: a known class becomes an unrelated stored label, and an authored field disappears without an omission signal. The three small synthetic XML fixtures remain below every default limit. No source artifact changed. Real-app incidence, binary-plist input, native Apple output and earlier releases were not tested.

The demonstrated manual workaround is the generic keyed-archive reader, which preserved the correct class descriptor and raw field on the exact failing bytes. Reordering entries is not a full workaround because the authored field still disappears.

REA version

rea-agents 6.7.0; source main at 99d2d7ad22bcbd780919760008aba6a88e7d1077. The parent supplied 6.7.0 as the latest npm version. This probe exercised the source checkout with locked dependencies, not the published runtime.

Runtime and platform

Node.js v24.22.1; Linux x64; repository tsx loader. No build, full suite, Apple tools or target process ran.

Target and artifact context

Three generated XML NSKeyedArchiver archives at Contents/Resources/Main.nib, each with one ArchiveProbeView, its class descriptor declaring NSView ancestry, and a root reference to object 1. They differ only in the ordinary field name or the order of $class and the authored fields. The analyzer's target digest is synthetic source-boundary scaffolding, not an observed executable identity.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced from the executed JSON output:

variant       Interface Builder class_name  authored field in node attributes
control       ArchiveProbeView              modelClassName = StoredLabel
class-first   StoredLabel                   className absent
class-last    ArchiveProbeView              className absent

Every case returned one object, hierarchy_complete: true, graph.truncated: false, and complete objects/connections/hierarchy/classes/archive_decode coverage with zero omissions. For the failing class-first bytes, the generic production keyed-archive decoder returned:

{
  "id": 1,
  "kind": "archived-object",
  "class_id": 2,
  "class_name": "ArchiveProbeView",
  "status": "unknown-class",
  "value": {
    "$class": {"CF$UID": 2},
    "className": "StoredLabel",
    "title": "Fixture"
  }
}

Evidence provenance: observed production bundle-analysis and generic keyed-archive boundaries. Local verification: reproduced. Reproduction completeness: complete for the supplied XML variants. No native compiler/exporter acceptance or public adapter execution is claimed.

Typed outcome or recovery expected

Restoration check, proposed and not executed: both dictionary orders must retain the correct $class-derived name and the authored className value; the renamed-field control and generic keyed-archive results must remain correct. A change that only makes the descriptor win still fails field preservation. No repair or restored behavior is claimed.

Analysis or suspected cause

Source-derived: src/domain/apple/interfaceBuilderKeyedArchive.ts:276-280 places generated $class metadata and authored fields in the same className property, so assignment order decides its value. Lines 399-407 then consume that property as the class and remove it from serialized attributes. This explains both the order-sensitive wrong name and the field loss in either order.

Source-derived user-facing reachability: src/cli/artifactCommands.ts:180-188 dispatches decode_interface_builder; src/artifacts/ArtifactProvider.ts:224-232 invokes the same analyzer. The separate bundle-resource provider also invokes it at lines 125-133. Those adapters were not executed here.

Additional context

Open and closed searches covered plist, keyed archive, Interface Builder, and tighter className/authored-field combinations. Related #6662 concerns an authored archiveUID changing object identity; this fixture contains no archiveUID, retains object ID 1, and fails class identity and raw-field preservation after that report's identity correction. Related #5721 concerns native placeholder/class-swapper records; this fixture has neither and already has a directly resolvable class descriptor.

NOT_ENOUGH_INFO: real-exporter prevalence and last known good version. No failure reproducer was rerun after this draft was written.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A small supported keyed-archive input returns a false class name and silently loses ordinary serialized data while reporting complete coverage; the field-name and dictionary-order controls isolate the bounded evidence defect.
  • Workaround status: available
  • Workaround basis: The executed generic keyed-archive decoder retained both the correct class descriptor and the raw authored field for the same failing bytes, enabling manual inspection without modifying the archive; it does not repair the Interface Builder graph.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6776 ### 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 `analyzeInterfaceBuilderBundle` reader reports an ordinary authored `className: "StoredLabel"` field as the object's archive class, replacing the `$class` descriptor's `ArchiveProbeView`. Reordering only those dictionary entries restores the class name but still loses the authored field. All coverage facets report complete with zero omissions, and `graph.truncated` is false. The generic `decodeKeyedArchiveBytes` reader retains `class_name: "ArchiveProbeView"` and the authored field for the same bytes. Renaming the field to `modelClassName` also preserves both facts in the Interface Builder graph. These were source-boundary executions; CLI/MCP admission and transport were not run. ### Reproduction From the source checkout with locked dependencies installed, save the script below as `/tmp/rea-classname-probe.mts`, then run: ```sh node --import ./node_modules/tsx/dist/loader.mjs /tmp/rea-classname-probe.mts "$PWD" ``` The executed script used the same code and arguments, with a different scratch script location. It ran each variant once in one process, exited 0, and removed its generated bundles. It generates inert XML keyed archives and invokes the production bundle analyzer and generic keyed-archive decoder; no application class is instantiated. ```ts import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const repo = resolve(process.argv[2]!); const { analyzeInterfaceBuilderBundle } = await import(pathToFileURL(join(repo, 'src/artifacts/apple/InterfaceBuilderAnalysis.ts')).href); const { decodeKeyedArchiveBytes } = await import(pathToFileURL(join(repo, 'src/artifacts/apple/KeyedArchiveReader.ts')).href); const uid = (n: number) => ({ 'CF$UID': n }); function xml(value: unknown): string { if (typeof value === 'number') return '<integer>' + value + '</integer>'; if (typeof value === 'string') return '<string>' + value + '</string>'; if (Array.isArray(value)) return '<array>' + value.map(xml).join('') + '</array>'; return '<dict>' + Object.entries(value as object).map(([k,v]) => '<key>' + k + '</key>' + xml(v)).join('') + '</dict>'; } const temporary = await mkdtemp('/tmp/rea-classname-'); try { for (const variant of ['control', 'class-first', 'class-last']) { const field = variant === 'control' ? 'modelClassName' : 'className'; const data = { [field]: 'StoredLabel', title: 'Fixture' }; const object = variant === 'class-last' ? { ...data, $class: uid(2) } : { $class: uid(2), ...data }; const archive = { $archiver: 'NSKeyedArchiver', $version: 100000, $objects: ['$null', object, { $classname: 'ArchiveProbeView', $classes: ['ArchiveProbeView', 'NSView', 'NSObject'] }], $top: { root: uid(1) }, }; const bytes = Buffer.from('<?xml version="1.0"?><plist version="1.0">' + xml(archive) + '</plist>'); const bundlePath = join(temporary, variant + '.app'); await mkdir(join(bundlePath, 'Contents/Resources'), { recursive: true }); await writeFile(join(bundlePath, 'Contents/Resources/Main.nib'), bytes); const result = await analyzeInterfaceBuilderBundle({ bundlePath, targetSha256: 'a'.repeat(64) }); const keyed = decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 20 }); console.log(JSON.stringify({ variant, object: result.graph.nodes.find((n: any) => n.attributes.interface_builder_object_id === '1'), document: result.documents[0], coverage: result.graph.coverage, truncated: result.graph.truncated, keyedObject: keyed.objects.find((n: any) => n.id === 1), })); } } finally { await rm(temporary, { recursive: true, force: true }); } ``` ### Expected behavior Report `ArchiveProbeView` as the class resolved from `$class` in both dictionary orders, and preserve `className: "StoredLabel"` as ordinary serialized data. Preserve `modelClassName` in the control. Dictionary ordering must not change class identity. `docs/native-investigation.md:459-461` promises controller/class names from compiled nib resources, and `src/contracts/artifactToolContracts.ts:92-96` explicitly includes keyed property lists. `AGENTS.md:19` requires preserved identity and source evidence. The fixture has a valid, in-bounds class reference, and the generic decoder resolves it correctly. The source declares no reservation of an application's `className` field as metadata. ### Impact and scope Observed non-security evidence corruption: a known class becomes an unrelated stored label, and an authored field disappears without an omission signal. The three small synthetic XML fixtures remain below every default limit. No source artifact changed. Real-app incidence, binary-plist input, native Apple output and earlier releases were not tested. The demonstrated manual workaround is the generic keyed-archive reader, which preserved the correct class descriptor and raw field on the exact failing bytes. Reordering entries is not a full workaround because the authored field still disappears. ### REA version `rea-agents` 6.7.0; source `main` at `99d2d7ad22bcbd780919760008aba6a88e7d1077`. The parent supplied 6.7.0 as the latest npm version. This probe exercised the source checkout with locked dependencies, not the published runtime. ### Runtime and platform Node.js v24.22.1; Linux x64; repository `tsx` loader. No build, full suite, Apple tools or target process ran. ### Target and artifact context Three generated XML NSKeyedArchiver archives at `Contents/Resources/Main.nib`, each with one `ArchiveProbeView`, its class descriptor declaring `NSView` ancestry, and a root reference to object 1. They differ only in the ordinary field name or the order of `$class` and the authored fields. The analyzer's target digest is synthetic source-boundary scaffolding, not an observed executable identity. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reduced from the executed JSON output: ```text variant Interface Builder class_name authored field in node attributes control ArchiveProbeView modelClassName = StoredLabel class-first StoredLabel className absent class-last ArchiveProbeView className absent ``` Every case returned one object, `hierarchy_complete: true`, `graph.truncated: false`, and complete objects/connections/hierarchy/classes/archive_decode coverage with zero omissions. For the failing `class-first` bytes, the generic production keyed-archive decoder returned: ```json { "id": 1, "kind": "archived-object", "class_id": 2, "class_name": "ArchiveProbeView", "status": "unknown-class", "value": { "$class": {"CF$UID": 2}, "className": "StoredLabel", "title": "Fixture" } } ``` Evidence provenance: observed production bundle-analysis and generic keyed-archive boundaries. Local verification: reproduced. Reproduction completeness: complete for the supplied XML variants. No native compiler/exporter acceptance or public adapter execution is claimed. ### Typed outcome or recovery expected Restoration check, proposed and not executed: both dictionary orders must retain the correct `$class`-derived name and the authored `className` value; the renamed-field control and generic keyed-archive results must remain correct. A change that only makes the descriptor win still fails field preservation. No repair or restored behavior is claimed. ### Analysis or suspected cause Source-derived: `src/domain/apple/interfaceBuilderKeyedArchive.ts:276-280` places generated `$class` metadata and authored fields in the same `className` property, so assignment order decides its value. Lines 399-407 then consume that property as the class and remove it from serialized attributes. This explains both the order-sensitive wrong name and the field loss in either order. Source-derived user-facing reachability: `src/cli/artifactCommands.ts:180-188` dispatches `decode_interface_builder`; `src/artifacts/ArtifactProvider.ts:224-232` invokes the same analyzer. The separate bundle-resource provider also invokes it at lines 125-133. Those adapters were not executed here. ### Additional context Open and closed searches covered `plist`, `keyed archive`, `Interface Builder`, and tighter `className`/authored-field combinations. Related #6662 concerns an authored `archiveUID` changing object identity; this fixture contains no `archiveUID`, retains object ID 1, and fails class identity and raw-field preservation after that report's identity correction. Related #5721 concerns native placeholder/class-swapper records; this fixture has neither and already has a directly resolvable class descriptor. NOT_ENOUGH_INFO: real-exporter prevalence and last known good version. No failure reproducer was rerun after this draft was written. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A small supported keyed-archive input returns a false class name and silently loses ordinary serialized data while reporting complete coverage; the field-name and dictionary-order controls isolate the bounded evidence defect. - Workaround status: available - Workaround basis: The executed generic keyed-archive decoder retained both the correct class descriptor and the raw authored field for the same failing bytes, enabling manual inspection without modifying the archive; it does not repair the Interface Builder graph.
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#4132
No description provided.