[GH-ISSUE #6738] [Bug] Model views accept a conflicting parent path after caching declarations #4101

Closed
opened 2026-10-31 00:46:49 +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/6738

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 shared inspectAnalysisView application service changes its result for the same authenticated parent Evidence depending on whether its immutable model report was previously projected. With an empty synthetic GGUF inspected through the real model service, a parent whose subject path disagrees with normalized_result.artifact.path returns AnalysisInputError before caching. After a valid parent sharing the same report is projected, the conflicting parent succeeds. Its returned Evidence subject and model artifact name different paths.

This is an observed application-boundary reproduction with deliberately inconsistent synthetic parent subjects. Compiled CLI/MCP transport and a naturally occurring producer of conflicting parents were not tested. A conflicting digest remains rejected by a downstream application check; this report does not claim the application accepts it.

Reproduction

Use the source revision below with locked dependencies installed. Save the following script as /tmp/rea-model-cache/repro.mts, then run it from the checkout root with the repository's pinned Node 24.22.1:

node --import ./node_modules/tsx/dist/loader.mjs \
  /tmp/rea-model-cache/repro.mts "$PWD" /tmp/rea-model-cache/input

The following script was actually executed with these two paths supplied as arguments. It creates only a benign 32-byte GGUF file, calls the real model provider/service, creates authenticated Evidence through createEvidence, and calls the shared view application service. No parser result or view result is mocked.

import assert from 'node:assert/strict';
import { mkdir, writeFile } from 'node:fs/promises';
import { resolve, join } from 'node:path';
const [checkoutArg, rootArg] = process.argv.slice(2);
const checkout = resolve(checkoutArg);
const root = resolve(rootArg);
await mkdir(root, {recursive:true});
const { ModelArtifactProvider } = await import(join(checkout, 'src/model/ModelArtifactProvider.ts'));
const { ModelArtifactService } = await import(join(checkout, 'src/application/model/ModelArtifactService.ts'));
const { inspectAnalysisView } = await import(join(checkout, 'src/application/analysisView/AnalysisViewService.ts'));
const { createEvidence, parseEvidence } = await import(join(checkout, 'src/domain/evidence.ts'));
const { freezeJsonSnapshot } = await import(join(checkout, 'src/domain/immutableJson.ts'));
const path = join(root, 'empty.gguf');
const bytes = Buffer.alloc(32);
bytes.write('GGUF'); bytes.writeUInt32LE(3, 4);
await writeFile(path, bytes);
const service = new ModelArtifactService(new ModelArtifactProvider());
try {
  const inspected = await service.inspect({path,hash:'full',detail:'complete'});
  assert(inspected.ok);
  const original = inspected.value;
  assert(original.subject !== null);
  const report = freezeJsonSnapshot(original.normalized_result);
  const make = (name, targetPath, digest, result=report) => createEvidence(
    {path:targetPath,sha256:digest,format:'file'}, original.provider,
    {operation:original.operation, parameters:{path,hash:'full',fixture:name},
     result, confidence:'observed', limitations:original.limitations});
  const digest = original.subject.digest.sha256;
  const records = {
    valid:make('valid',path,digest),
    wrong_path:make('wrong_path',join(root,'other.gguf'),digest),
    wrong_digest:make('wrong_digest',path,'c'.repeat(64)),
  };
  for (const record of Object.values(records)) {
    assert.equal(parseEvidence(record),record);
    assert.equal(record.normalized_result,report);
  }
  const call = async record => await inspectAnalysisView(
    {source:{kind:'retained-evidence',evidence_id:record.evidence_id},view:{kind:'summary'}},
    id => id === record.evidence_id ? record : undefined);
  const compact = (result, parent) => result.ok ? {
    ok:true,
    source_matches_parent_path:result.value.normalized_result.artifact.path===parent.subject.local_path,
    source_matches_parent_digest:result.value.normalized_result.artifact.sha256===parent.subject.digest.sha256,
    returned_subject_matches_parent:result.value.subject.digest.sha256===parent.subject.digest.sha256,
  } : {ok:false,error:result.error._tag};
  const cold = {};
  for (const name of ['wrong_path','wrong_digest']) {
    const result = await call(records[name]);
    assert.equal(result.ok,false);
    assert.equal(result.error._tag,'AnalysisInputError');
    cold[name]=compact(result,records[name]);
  }
  const initial = await call(records.valid);
  assert.equal(initial.ok,true);
  const warm = {};
  for (const name of ['wrong_path','wrong_digest']) {
    const result = await call(records[name]);
    assert.equal(result.ok,name==='wrong_path');
    if (!result.ok) assert.equal(result.error._tag,'EvidenceIntegrityError');
    warm[name]=compact(result,records[name]);
  }
  const separateReport = freezeJsonSnapshot(structuredClone(report));
  const independent = make('fresh_wrong_digest',path,'c'.repeat(64),separateReport);
  const independentResult = await call(independent);
  assert.equal(independentResult.ok,false);
  console.log(JSON.stringify({node:process.version,platform:process.platform,arch:process.arch,
    source_revision:'76b56957e94d804667f7890b244639ecc4725b32',
    fixture:{format:'gguf',version:3,file_size:32,metadata:0,tensors:0,full_hash:true},
    authenticated_records:true,shared_immutable_report:true,cold,valid_projection:initial.ok,warm,
    fresh_report_control:compact(independentResult,independent)},null,2));
} finally {await service.close();}

The final script exited 0. The cold and warm path checks reproduced in both the diagnostic application run and the final assertion-bearing run. These calls characterize a deterministic cache transition within a process; they do not measure production frequency.

Expected behavior

Apply the same parent-subject consistency check on every projection, including cache hits. A cached report establishes parsed model facts; it does not establish a different parent envelope's path or digest. Both cold and warm calls should return AnalysisInputError for the demonstrated conflicting subject path.

The model projector itself defines this path/digest contract. The analogous APK signature view regression explicitly requires subject checks again when facts are already cached.

Impact and scope

The demonstrated effect is cache-dependent acceptance of inconsistent model Evidence, yielding two different artifact paths in one successful result. It affects parents that share one sealed model-result object and disagree about the subject path. The real application service and authenticated Evidence were exercised; the conflicting envelopes were constructed synthetically.

NOT_ENOUGH_INFO: whether an ordinary CLI/MCP operation or built-in producer currently creates this sharing/conflict combination; prevalence in real workloads; other platforms; a user-facing recovery command. No model execution, input mutation, security impact, or cross-session effect is claimed.

REA version

Package metadata 6.7.0; source 76b56957e94d804667f7890b244639ecc4725b32. This is a source-checkout reproduction, not a released-package or installed-CLI reproduction.

Runtime and platform

Node.js v24.22.1; Linux x86_64; repository locked dependencies with its tsx source loader. No external analysis engine or model runtime.

Target and artifact context

Synthetic 32-byte GGUF v3 containing zero metadata entries, zero tensors, and alignment padding. The real provider completed full-file hashing. Authenticated synthetic Evidence records share its sealed normalized report and vary only their parent subjects and fixture labels.

Interface

Other — shared source application service (inspectAnalysisView) using retained-Evidence lookup.

Earliest failure stage

Transport or result projection

Relevant output or evidence

Selected output from the executed script:

{
  "cold": {
    "wrong_path": {
      "ok": false,
      "error": "AnalysisInputError"
    },
    "wrong_digest": {
      "ok": false,
      "error": "AnalysisInputError"
    }
  },
  "valid_projection": true,
  "warm": {
    "wrong_path": {
      "ok": true,
      "source_matches_parent_path": false,
      "source_matches_parent_digest": true,
      "returned_subject_matches_parent": true
    },
    "wrong_digest": {
      "ok": false,
      "error": "EvidenceIntegrityError"
    }
  },
  "fresh_report_control": {
    "ok": false,
    "error": "AnalysisInputError"
  }
}

warm.wrong_path.ok: true with source_matches_parent_path: false is the observed defect. The distinct freshly sealed report control returns AnalysisInputError. For the digest control, the application returns EvidenceIntegrityError after warming rather than accepting the conflicting digest.

  • Evidence provenance: observed shared application/provider execution with a synthetic local fixture and synthetic parent envelopes.
  • Local verification: reproduced before writing this draft.
  • Reproduction completeness: complete for this source application boundary; ordinary producer reach and public adapters remain unverified.

Typed outcome or recovery expected

Return the existing AnalysisInputError at source.subject for every conflicting parent path, independent of prior queries. Preserve reuse of the validated report without reusing a prior envelope's validation result.

Analysis or suspected cause

Source-derived: the model cache hit returns immediately before the per-parent path/digest check. The application supplies the current parent subject separately, while Evidence creation can preserve sealed result sharing. The cache key covers the result object, not the parent subject. Moving or repeating the parent check after cache lookup would follow the existing APK view pattern; no fix was implemented.

Additional context

Open and closed issue searches included model in:title, "cache" "subject" in:title, "cached" "identity" in:title, "subject" "cache" in:title, "modelArtifactView.ts" "subject", "modelArtifactView", and "model" "identity" in:title; no matching report was found. Related model summary/cancellation reports concern different behaviors. The parent audit will perform its own fresh duplicate check before any submission.

No source changes, installs, builds, full suites, commits, PRs, or issue mutations were performed. No known-good revision was established.

Triage assessment

  • Impact level: P2
  • Assessment status: provisional
  • Impact basis: The shared application demonstrably accepts conflicting model paths after a cache hit, producing inconsistent Evidence. The affected source boundary is observed, but a normal built-in producer or CLI/MCP workflow reaching the shared-report/conflicting-subject condition remains unverified.
  • Workaround status: unverified
  • Workaround basis: A fresh report object restores rejection in the executed control, but no practical user-facing command or recovery path was established for affected consumers.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6738 ## 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 shared `inspectAnalysisView` application service changes its result for the same authenticated parent Evidence depending on whether its immutable model report was previously projected. With an empty synthetic GGUF inspected through the real model service, a parent whose subject path disagrees with `normalized_result.artifact.path` returns `AnalysisInputError` before caching. After a valid parent sharing the same report is projected, the conflicting parent succeeds. Its returned Evidence subject and model artifact name different paths. This is an observed application-boundary reproduction with deliberately inconsistent synthetic parent subjects. Compiled CLI/MCP transport and a naturally occurring producer of conflicting parents were not tested. A conflicting digest remains rejected by a downstream application check; this report does not claim the application accepts it. ## Reproduction Use the source revision below with locked dependencies installed. Save the following script as `/tmp/rea-model-cache/repro.mts`, then run it from the checkout root with the repository's pinned Node 24.22.1: ```sh node --import ./node_modules/tsx/dist/loader.mjs \ /tmp/rea-model-cache/repro.mts "$PWD" /tmp/rea-model-cache/input ``` The following script was actually executed with these two paths supplied as arguments. It creates only a benign 32-byte GGUF file, calls the real model provider/service, creates authenticated Evidence through `createEvidence`, and calls the shared view application service. No parser result or view result is mocked. ```ts import assert from 'node:assert/strict'; import { mkdir, writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; const [checkoutArg, rootArg] = process.argv.slice(2); const checkout = resolve(checkoutArg); const root = resolve(rootArg); await mkdir(root, {recursive:true}); const { ModelArtifactProvider } = await import(join(checkout, 'src/model/ModelArtifactProvider.ts')); const { ModelArtifactService } = await import(join(checkout, 'src/application/model/ModelArtifactService.ts')); const { inspectAnalysisView } = await import(join(checkout, 'src/application/analysisView/AnalysisViewService.ts')); const { createEvidence, parseEvidence } = await import(join(checkout, 'src/domain/evidence.ts')); const { freezeJsonSnapshot } = await import(join(checkout, 'src/domain/immutableJson.ts')); const path = join(root, 'empty.gguf'); const bytes = Buffer.alloc(32); bytes.write('GGUF'); bytes.writeUInt32LE(3, 4); await writeFile(path, bytes); const service = new ModelArtifactService(new ModelArtifactProvider()); try { const inspected = await service.inspect({path,hash:'full',detail:'complete'}); assert(inspected.ok); const original = inspected.value; assert(original.subject !== null); const report = freezeJsonSnapshot(original.normalized_result); const make = (name, targetPath, digest, result=report) => createEvidence( {path:targetPath,sha256:digest,format:'file'}, original.provider, {operation:original.operation, parameters:{path,hash:'full',fixture:name}, result, confidence:'observed', limitations:original.limitations}); const digest = original.subject.digest.sha256; const records = { valid:make('valid',path,digest), wrong_path:make('wrong_path',join(root,'other.gguf'),digest), wrong_digest:make('wrong_digest',path,'c'.repeat(64)), }; for (const record of Object.values(records)) { assert.equal(parseEvidence(record),record); assert.equal(record.normalized_result,report); } const call = async record => await inspectAnalysisView( {source:{kind:'retained-evidence',evidence_id:record.evidence_id},view:{kind:'summary'}}, id => id === record.evidence_id ? record : undefined); const compact = (result, parent) => result.ok ? { ok:true, source_matches_parent_path:result.value.normalized_result.artifact.path===parent.subject.local_path, source_matches_parent_digest:result.value.normalized_result.artifact.sha256===parent.subject.digest.sha256, returned_subject_matches_parent:result.value.subject.digest.sha256===parent.subject.digest.sha256, } : {ok:false,error:result.error._tag}; const cold = {}; for (const name of ['wrong_path','wrong_digest']) { const result = await call(records[name]); assert.equal(result.ok,false); assert.equal(result.error._tag,'AnalysisInputError'); cold[name]=compact(result,records[name]); } const initial = await call(records.valid); assert.equal(initial.ok,true); const warm = {}; for (const name of ['wrong_path','wrong_digest']) { const result = await call(records[name]); assert.equal(result.ok,name==='wrong_path'); if (!result.ok) assert.equal(result.error._tag,'EvidenceIntegrityError'); warm[name]=compact(result,records[name]); } const separateReport = freezeJsonSnapshot(structuredClone(report)); const independent = make('fresh_wrong_digest',path,'c'.repeat(64),separateReport); const independentResult = await call(independent); assert.equal(independentResult.ok,false); console.log(JSON.stringify({node:process.version,platform:process.platform,arch:process.arch, source_revision:'76b56957e94d804667f7890b244639ecc4725b32', fixture:{format:'gguf',version:3,file_size:32,metadata:0,tensors:0,full_hash:true}, authenticated_records:true,shared_immutable_report:true,cold,valid_projection:initial.ok,warm, fresh_report_control:compact(independentResult,independent)},null,2)); } finally {await service.close();} ``` The final script exited 0. The cold and warm path checks reproduced in both the diagnostic application run and the final assertion-bearing run. These calls characterize a deterministic cache transition within a process; they do not measure production frequency. ## Expected behavior Apply the same parent-subject consistency check on every projection, including cache hits. A cached report establishes parsed model facts; it does not establish a different parent envelope's path or digest. Both cold and warm calls should return `AnalysisInputError` for the demonstrated conflicting subject path. The model projector itself defines this [path/digest contract](https://github.com/morluto/rea/blob/76b56957e94d804667f7890b244639ecc4725b32/src/domain/analysisView/modelArtifactView.ts#L150-L164). The analogous [APK signature view regression](https://github.com/morluto/rea/blob/76b56957e94d804667f7890b244639ecc4725b32/tests/boundary/filesystem/apkSignatureViewIdentity.test.ts#L12) explicitly requires subject checks again when facts are already cached. ## Impact and scope The demonstrated effect is cache-dependent acceptance of inconsistent model Evidence, yielding two different artifact paths in one successful result. It affects parents that share one sealed model-result object and disagree about the subject path. The real application service and authenticated Evidence were exercised; the conflicting envelopes were constructed synthetically. NOT_ENOUGH_INFO: whether an ordinary CLI/MCP operation or built-in producer currently creates this sharing/conflict combination; prevalence in real workloads; other platforms; a user-facing recovery command. No model execution, input mutation, security impact, or cross-session effect is claimed. ## REA version Package metadata 6.7.0; source `76b56957e94d804667f7890b244639ecc4725b32`. This is a source-checkout reproduction, not a released-package or installed-CLI reproduction. ## Runtime and platform Node.js v24.22.1; Linux x86_64; repository locked dependencies with its `tsx` source loader. No external analysis engine or model runtime. ## Target and artifact context Synthetic 32-byte GGUF v3 containing zero metadata entries, zero tensors, and alignment padding. The real provider completed full-file hashing. Authenticated synthetic Evidence records share its sealed normalized report and vary only their parent subjects and fixture labels. ## Interface Other — shared source application service (`inspectAnalysisView`) using retained-Evidence lookup. ## Earliest failure stage Transport or result projection ## Relevant output or evidence Selected output from the executed script: ```json { "cold": { "wrong_path": { "ok": false, "error": "AnalysisInputError" }, "wrong_digest": { "ok": false, "error": "AnalysisInputError" } }, "valid_projection": true, "warm": { "wrong_path": { "ok": true, "source_matches_parent_path": false, "source_matches_parent_digest": true, "returned_subject_matches_parent": true }, "wrong_digest": { "ok": false, "error": "EvidenceIntegrityError" } }, "fresh_report_control": { "ok": false, "error": "AnalysisInputError" } } ``` `warm.wrong_path.ok: true` with `source_matches_parent_path: false` is the observed defect. The distinct freshly sealed report control returns `AnalysisInputError`. For the digest control, the application returns `EvidenceIntegrityError` after warming rather than accepting the conflicting digest. - Evidence provenance: observed shared application/provider execution with a synthetic local fixture and synthetic parent envelopes. - Local verification: reproduced before writing this draft. - Reproduction completeness: complete for this source application boundary; ordinary producer reach and public adapters remain unverified. ## Typed outcome or recovery expected Return the existing `AnalysisInputError` at `source.subject` for every conflicting parent path, independent of prior queries. Preserve reuse of the validated report without reusing a prior envelope's validation result. ## Analysis or suspected cause Source-derived: the [model cache hit returns immediately](https://github.com/morluto/rea/blob/76b56957e94d804667f7890b244639ecc4725b32/src/domain/analysisView/modelArtifactView.ts#L137-L164) before the per-parent path/digest check. The [application supplies the current parent subject separately](https://github.com/morluto/rea/blob/76b56957e94d804667f7890b244639ecc4725b32/src/application/analysisView/AnalysisViewService.ts#L204-L228), while [Evidence creation can preserve sealed result sharing](https://github.com/morluto/rea/blob/76b56957e94d804667f7890b244639ecc4725b32/src/domain/evidence.ts#L458-L473). The cache key covers the result object, not the parent subject. Moving or repeating the parent check after cache lookup would follow the existing APK view pattern; no fix was implemented. ## Additional context Open and closed issue searches included `model in:title`, `"cache" "subject" in:title`, `"cached" "identity" in:title`, `"subject" "cache" in:title`, `"modelArtifactView.ts" "subject"`, `"modelArtifactView"`, and `"model" "identity" in:title`; no matching report was found. Related model summary/cancellation reports concern different behaviors. The parent audit will perform its own fresh duplicate check before any submission. No source changes, installs, builds, full suites, commits, PRs, or issue mutations were performed. No known-good revision was established. ## Triage assessment - Impact level: P2 - Assessment status: provisional - Impact basis: The shared application demonstrably accepts conflicting model paths after a cache hit, producing inconsistent Evidence. The affected source boundary is observed, but a normal built-in producer or CLI/MCP workflow reaching the shared-report/conflicting-subject condition remains unverified. - Workaround status: unverified - Workaround basis: A fresh report object restores rejection in the executed control, but no practical user-facing command or recovery path was established for affected consumers.
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#4101
No description provided.