[GH-ISSUE #6436] [Bug] ASAR summaries omit known unavailable content-hash uncertainty #3850

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

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

Both inspect-artifact --detail summary and inspect-analysis-view with a summary view omit the unavailable-content-hash uncertainty for an ASAR with missing unpacked companion bytes. An occurrence view of the same producer Evidence retains the unavailable add-on and emits the existing hash-unknown message. Complete parent Evidence survives in the saved bundle.

Reproduction

From the built source checkout described under the version/environment field, with its locked dependencies and compiled runtime already available, run the following at the repository root using the pinned Node.js toolchain. Dependency preparation and the build were not rerun during verification. The script creates an inert ASAR and isolated HOME/XDG/TMPDIR directories, clears ambient client/provider overrides by constructing the child environment from PATH, executes only static inspection, and removes its fixture root in finally. It checks the currently observed omission; these assertions describe the failing baseline rather than repaired behavior.

node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createPackageWithOptions } from '@electron/asar';
const root = mkdtempSync(join(tmpdir(), 'asar-summary-'));
try {
  const contents = join(root, 'source');
  mkdirSync(join(contents, 'native'), { recursive: true });
  writeFileSync(join(contents, 'main.js'), 'console.log(1)\n');
  const bytes = 'inert addon bytes\n';
  writeFileSync(join(contents, 'native', 'addon.node'), bytes);
  const archive = join(root, 'app.asar');
  await createPackageWithOptions(contents, archive, { unpack: '*.node' });
  const env = { PATH: process.env.PATH };
  for (const key of ['HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','TMPDIR']) {
    env[key] = join(root, key.toLowerCase()); mkdirSync(env[key]);
  }
  const run = args => JSON.parse(execFileSync(process.execPath, ['scripts/rea.mjs', ...args, '--json'], { env, encoding: 'utf8', timeout: 30000, maxBuffer: 32*1024*1024 }));
  const unknown = value => value.unknowns.some(x => x.includes('Content hashes are unknown'));
  const companion = join(root, 'app.asar.unpacked', 'native', 'addon.node');
  const results = [];
  for (const phase of ['present','missing','restored']) {
    if (phase === 'missing') rmSync(companion);
    if (phase === 'restored') writeFileSync(companion, bytes);
    const parent = run(['inspect-artifact', archive]);
    const view = request => {
      const path = join(root, 'request.json');
      writeFileSync(path, JSON.stringify({source:{kind:'inline',evidence:parent},view:request}));
      return run(['inspect-analysis-view',path]).normalized_result;
    };
    const summary = view({kind:'summary'});
    const inventory = view({kind:'inventory',collection:'occurrences',limit:32});
    const bundle = join(root, phase+'-evidence.json');
    const directResult = run(['inspect-artifact',archive,'--detail','summary','--evidence_output',bundle]);
    const direct = directResult.summary.normalized_result;
    const retained = JSON.parse(readFileSync(bundle, 'utf8')).records.find(x=>x.operation==='inspect_artifact');
    const item = inventory.items.find(x=>x.logical_path==='native/addon.node');
    const unavailableCount = inventory.items.filter(x=>x.hash_status==='unavailable').length;
    const record = {phase, unavailable_count:unavailableCount, source_complete:summary.summary.source_complete, summary_unknowns:summary.unknowns, direct_unknowns:direct.unknowns, inventory_unknowns:inventory.unknowns, summary_hash_counts:summary.summary.available_hash_status_counts ?? null, addon_hash_status:item.hash_status, addon_artifact_id_null:item.artifact_id===null, addon_limitations:item.limitations, summary_limitations:summary.limitations, retained_operation:retained.operation, retained_has_unavailable:JSON.stringify(retained).includes('"hash_status":"unavailable"')};
    results.push(record);
    assert.equal(item.hash_status, phase==='missing'?'unavailable':'verified');
    assert.equal(unavailableCount, phase==='missing'?1:0);
    assert.equal(unknown(inventory), phase==='missing');
    assert.equal(unknown(summary), false);
    assert.equal(unknown(direct), false);
    assert.equal(summary.summary.source_complete, true);
    assert.equal(retained.operation, 'inspect_artifact');
  }
  console.log(JSON.stringify(results, null, 2));
} finally { rmSync(root, {recursive:true,force:true}); assert.equal(existsSync(root),false); }
JS

Expected behavior

Both summary routes should disclose that some recorded files have unavailable content hashes, preserving the known uncertainty already emitted by the inventory view of the same parent. An equivalent explicit caveat is sufficient; a new count field is not required. The available-companion controls should remain free of that caveat. source_complete: true is valid here: it describes complete graph checks and does not certify content-hash availability.

Impact and scope

P2: a bounded summary disclosure defect hides a known content-identity limitation from callers using the supported compact result. No downstream mistake, lost bytes, source mutation, or security boundary crossing was observed. A verified workaround is to query the same complete parent with an inventory occurrence view, or read the complete parent in the saved bundle; that retains the unavailable entry and its reason. Observed scope is the generated ASAR on one host. ZIP, other formats, MCP transport, and population frequency were not tested.

REA version

REA source main at 9eb829aca3; package version 6.7.0.

Latest stable GitHub release checked: rea-agents-6.7.0. This was a main-build runtime replay; the release tag was inspected as source only and is a different revision. Its older projection contains no dedicated content-hash unknown branch. No published-release runtime was replayed.

Runtime and platform

Node.js v24.18.0; macOS 27.0.1 arm64; prepared compiled runtime and locked dependencies. No deep-analysis engine or target application was run.

Target and artifact context

Generated ASAR containing embedded main.js and inert unpacked native/addon.node bytes. The same archive is inspected with the companion present, removed, and restored.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

In each successful three-phase replay, all 12 public CLI calls exited 0. The missing phase recorded exactly one unavailable occurrence, native/addon.node, with artifact_id: null and the limitation “ASAR unpacked companion bytes were unavailable; no content hash or child artifact was produced.” The inventory view added the content-hash unknown. Both the inline-parent summary and the direct summary omitted it, exposed no hash-status count, and contained only the generic static-inspection limitation. The saved bundle retained complete inspect_artifact Evidence containing the unavailable occurrence. Present/restored phases retained the add-on as verified and correctly had no hash unknown.

Smallest useful observed comparison from the script's output:

Phase Add-on hash status Inventory hash unknown Inline summary hash unknown Direct summary hash unknown
present verified false false false
missing unavailable true false false
restored verified false false false

Both summaries' only unknown was Inventory does not establish application runtime behavior or complete format-specific analysis. The missing-byte inventory additionally reported Content hashes are unknown for some recorded files; missing node IDs do not prove content absence. The true source_complete flag persisted through all phases and is not itself erroneous.

Contract breadcrumbs: AGENTS.md, Architecture and Cleanup, requires preserving uncertainty; docs/tool-design.md:49-56 requires relevant limitations inline; docs/javascript-artifact-reconstruction.md:176-181 supports missing companion bytes as unavailable; docs/mcp-contracts.md:393-407 defines summaries and source_complete.

Typed outcome or recovery expected

Return the supported summary with an explicit unavailable-content-hash caveat and retain the complete parent. A caller should not need another query solely to discover that known limitation. Existing inventory paging remains a demonstrated workaround.

Analysis or suspected cause

Source-derived: src/domain/analysisView/artifactInventoryView.ts:257-288 selects counts used by shared unknowns for inventory views and PCK/PyInstaller roots, skipping ASAR summaries. The summary branch at :305-332 computes its own counts after shared fields are formed and omits hash counts for this root format. This explains the observed contrast without requiring a specific repair.

Additional context

Three successful three-phase public CLI replays reproduced the missing-byte omission and the available-byte controls. The third replay ran the exact inline script above, including the unavailable-occurrence count assertion. The existing focused artifactInventoryView test file passed 10/10 with --maxWorkers=1; no test or source was changed. No last-known-good runtime was established.

Open and closed issue searches found no matching hash-summary report. Related #5983 concerns a JavaScript limitation counter, #3213 concerns missing entries in application analysis, #3926 concerns native-handoff digests, and #3171 concerns summary comparison admission. Those independently closable behaviors do not restore this summary disclosure.

Proposed restoration check: With the missing-companion fixture, both inspect-analysis-view summary and inspect-artifact --detail summary should explicitly report unavailable content-hash uncertainty. The same retained occurrence must remain unavailable with no child artifact; its inventory warning and complete saved parent must remain readable. With the companion present or restored, that occurrence must remain verified and the unavailable-hash caveat must remain absent. Keep source_complete tied to graph completeness; flipping that flag alone does not restore the disclosure contract.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6436 ## 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 Both `inspect-artifact --detail summary` and `inspect-analysis-view` with a summary view omit the unavailable-content-hash uncertainty for an ASAR with missing unpacked companion bytes. An occurrence view of the same producer Evidence retains the unavailable add-on and emits the existing hash-unknown message. Complete parent Evidence survives in the saved bundle. ## Reproduction From the built source checkout described under the version/environment field, with its locked dependencies and compiled runtime already available, run the following at the repository root using the pinned Node.js toolchain. Dependency preparation and the build were not rerun during verification. The script creates an inert ASAR and isolated HOME/XDG/TMPDIR directories, clears ambient client/provider overrides by constructing the child environment from PATH, executes only static inspection, and removes its fixture root in finally. It checks the currently observed omission; these assertions describe the failing baseline rather than repaired behavior. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createPackageWithOptions } from '@electron/asar'; const root = mkdtempSync(join(tmpdir(), 'asar-summary-')); try { const contents = join(root, 'source'); mkdirSync(join(contents, 'native'), { recursive: true }); writeFileSync(join(contents, 'main.js'), 'console.log(1)\n'); const bytes = 'inert addon bytes\n'; writeFileSync(join(contents, 'native', 'addon.node'), bytes); const archive = join(root, 'app.asar'); await createPackageWithOptions(contents, archive, { unpack: '*.node' }); const env = { PATH: process.env.PATH }; for (const key of ['HOME','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_STATE_HOME','TMPDIR']) { env[key] = join(root, key.toLowerCase()); mkdirSync(env[key]); } const run = args => JSON.parse(execFileSync(process.execPath, ['scripts/rea.mjs', ...args, '--json'], { env, encoding: 'utf8', timeout: 30000, maxBuffer: 32*1024*1024 })); const unknown = value => value.unknowns.some(x => x.includes('Content hashes are unknown')); const companion = join(root, 'app.asar.unpacked', 'native', 'addon.node'); const results = []; for (const phase of ['present','missing','restored']) { if (phase === 'missing') rmSync(companion); if (phase === 'restored') writeFileSync(companion, bytes); const parent = run(['inspect-artifact', archive]); const view = request => { const path = join(root, 'request.json'); writeFileSync(path, JSON.stringify({source:{kind:'inline',evidence:parent},view:request})); return run(['inspect-analysis-view',path]).normalized_result; }; const summary = view({kind:'summary'}); const inventory = view({kind:'inventory',collection:'occurrences',limit:32}); const bundle = join(root, phase+'-evidence.json'); const directResult = run(['inspect-artifact',archive,'--detail','summary','--evidence_output',bundle]); const direct = directResult.summary.normalized_result; const retained = JSON.parse(readFileSync(bundle, 'utf8')).records.find(x=>x.operation==='inspect_artifact'); const item = inventory.items.find(x=>x.logical_path==='native/addon.node'); const unavailableCount = inventory.items.filter(x=>x.hash_status==='unavailable').length; const record = {phase, unavailable_count:unavailableCount, source_complete:summary.summary.source_complete, summary_unknowns:summary.unknowns, direct_unknowns:direct.unknowns, inventory_unknowns:inventory.unknowns, summary_hash_counts:summary.summary.available_hash_status_counts ?? null, addon_hash_status:item.hash_status, addon_artifact_id_null:item.artifact_id===null, addon_limitations:item.limitations, summary_limitations:summary.limitations, retained_operation:retained.operation, retained_has_unavailable:JSON.stringify(retained).includes('"hash_status":"unavailable"')}; results.push(record); assert.equal(item.hash_status, phase==='missing'?'unavailable':'verified'); assert.equal(unavailableCount, phase==='missing'?1:0); assert.equal(unknown(inventory), phase==='missing'); assert.equal(unknown(summary), false); assert.equal(unknown(direct), false); assert.equal(summary.summary.source_complete, true); assert.equal(retained.operation, 'inspect_artifact'); } console.log(JSON.stringify(results, null, 2)); } finally { rmSync(root, {recursive:true,force:true}); assert.equal(existsSync(root),false); } JS ``` ## Expected behavior Both summary routes should disclose that some recorded files have unavailable content hashes, preserving the known uncertainty already emitted by the inventory view of the same parent. An equivalent explicit caveat is sufficient; a new count field is not required. The available-companion controls should remain free of that caveat. `source_complete: true` is valid here: it describes complete graph checks and does not certify content-hash availability. ## Impact and scope P2: a bounded summary disclosure defect hides a known content-identity limitation from callers using the supported compact result. No downstream mistake, lost bytes, source mutation, or security boundary crossing was observed. A verified workaround is to query the same complete parent with an inventory occurrence view, or read the complete parent in the saved bundle; that retains the unavailable entry and its reason. Observed scope is the generated ASAR on one host. ZIP, other formats, MCP transport, and population frequency were not tested. ## REA version REA source main at 9eb829aca34be36ff112826337996f6139f82eff; package version 6.7.0. Latest stable GitHub release checked: rea-agents-6.7.0. This was a main-build runtime replay; the release tag was inspected as source only and is a different revision. Its older projection contains no dedicated content-hash unknown branch. No published-release runtime was replayed. ## Runtime and platform Node.js v24.18.0; macOS 27.0.1 arm64; prepared compiled runtime and locked dependencies. No deep-analysis engine or target application was run. ## Target and artifact context Generated ASAR containing embedded main.js and inert unpacked native/addon.node bytes. The same archive is inspected with the companion present, removed, and restored. ## Interface CLI ## Earliest failure stage Transport or result projection ## Relevant output or evidence In each successful three-phase replay, all 12 public CLI calls exited 0. The missing phase recorded exactly one unavailable occurrence, `native/addon.node`, with `artifact_id: null` and the limitation “ASAR unpacked companion bytes were unavailable; no content hash or child artifact was produced.” The inventory view added the content-hash unknown. Both the inline-parent summary and the direct summary omitted it, exposed no hash-status count, and contained only the generic static-inspection limitation. The saved bundle retained complete `inspect_artifact` Evidence containing the unavailable occurrence. Present/restored phases retained the add-on as verified and correctly had no hash unknown. Smallest useful observed comparison from the script's output: | Phase | Add-on hash status | Inventory hash unknown | Inline summary hash unknown | Direct summary hash unknown | |---|---|---|---|---| | present | verified | false | false | false | | missing | unavailable | true | false | false | | restored | verified | false | false | false | Both summaries' only unknown was `Inventory does not establish application runtime behavior or complete format-specific analysis.` The missing-byte inventory additionally reported `Content hashes are unknown for some recorded files; missing node IDs do not prove content absence.` The true source_complete flag persisted through all phases and is not itself erroneous. Contract breadcrumbs: `AGENTS.md`, Architecture and Cleanup, requires preserving uncertainty; `docs/tool-design.md:49-56` requires relevant limitations inline; `docs/javascript-artifact-reconstruction.md:176-181` supports missing companion bytes as unavailable; `docs/mcp-contracts.md:393-407` defines summaries and source_complete. ## Typed outcome or recovery expected Return the supported summary with an explicit unavailable-content-hash caveat and retain the complete parent. A caller should not need another query solely to discover that known limitation. Existing inventory paging remains a demonstrated workaround. ## Analysis or suspected cause Source-derived: `src/domain/analysisView/artifactInventoryView.ts:257-288` selects counts used by shared unknowns for inventory views and PCK/PyInstaller roots, skipping ASAR summaries. The summary branch at `:305-332` computes its own counts after shared fields are formed and omits hash counts for this root format. This explains the observed contrast without requiring a specific repair. ## Additional context Three successful three-phase public CLI replays reproduced the missing-byte omission and the available-byte controls. The third replay ran the exact inline script above, including the unavailable-occurrence count assertion. The existing focused artifactInventoryView test file passed 10/10 with --maxWorkers=1; no test or source was changed. No last-known-good runtime was established. Open and closed issue searches found no matching hash-summary report. Related [#5983](https://github.com/morluto/rea/issues/5983) concerns a JavaScript limitation counter, [#3213](https://github.com/morluto/rea/issues/3213) concerns missing entries in application analysis, [#3926](https://github.com/morluto/rea/issues/3926) concerns native-handoff digests, and [#3171](https://github.com/morluto/rea/issues/3171) concerns summary comparison admission. Those independently closable behaviors do not restore this summary disclosure. Proposed restoration check: With the missing-companion fixture, both `inspect-analysis-view` summary and `inspect-artifact --detail summary` should explicitly report unavailable content-hash uncertainty. The same retained occurrence must remain unavailable with no child artifact; its inventory warning and complete saved parent must remain readable. With the companion present or restored, that occurrence must remain verified and the unavailable-hash caveat must remain absent. Keep source_complete tied to graph completeness; flipping that flag alone does not restore the disclosure contract.
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#3850
No description provided.