[GH-ISSUE #5836] [Bug] Artifact capacity refusals lose their typed boundary and recommend narrowing an unsupported path #3532

Closed
opened 2026-10-31 00:41:34 +00:00 by bsakaguchi · 2 comments
Owner

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

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

An executed public CLI reproduction with a 7,247-byte synthetic PyInstaller executable containing 129 outer-compressed PYZ images returns code: truncated and the same generic sentence for both message and remediation.action:

Artifact is too large to process safely. Narrow the requested path or use a smaller artifact.

The underlying refusal is the reader's 128-file private-storage capacity allowance, independent of input byte size. Its precise explanation survives in details.detail, but the reader's typed capacity boundary is absent from the public diagnostic. The advertised inspect-artifact command has no internal-member selector with which to follow the path-narrowing advice. A 207-byte one-image control succeeds. The repeated 129-image refusal has identical exit status, stdout and stderr.

Reproduction

Run from the source checkout below with its locked dependencies and existing build. Save the following as reproduce.py in an external scratch directory, set VERIFY_WORK_ROOT to that directory, and execute the script with Python 3. This is the executed fixture and CLI procedure. It uses the repository's own CArchive/PYZ fixture builder through the installed tsx dependency, isolates home/configuration/state/cache/temporary paths, and runs no target code or external analysis engine.

import json, os, pathlib, subprocess
root = pathlib.Path.cwd()
work = pathlib.Path(os.environ['VERIFY_WORK_ROOT']).resolve()
work.mkdir(exist_ok=True)
env = {k: v for k, v in os.environ.items()
       if not k.startswith(('REA_', 'HOPPER_', 'GHIDRA_')) and k != 'NODE_OPTIONS'}
for key, name in [('HOME', 'home'), ('XDG_CONFIG_HOME', 'config'),
                  ('XDG_DATA_HOME', 'data'), ('XDG_CACHE_HOME', 'cache'),
                  ('XDG_STATE_HOME', 'state'), ('TMPDIR', 'temp')]:
    (work / name).mkdir(exist_ok=True)
    env[key] = str(work / name)
fixture = (root / 'src/artifacts/pyinstaller/CArchive.fixture.ts').as_uri()
script = "import {writeFileSync} from 'node:fs';\n"
script += 'import {cArchiveExecutable,pyzArchive,PYTHON_39_MAGIC} from ' + json.dumps(fixture) + ';\n'
script += '''
const payload = pyzArchive({magic: PYTHON_39_MAGIC, modules: []});
for (const count of [1, 129]) {
  const bytes = cArchiveExecutable({layout: '2.1', pythonVersion: 309,
    members: Array.from({length: count}, (_, i) => ({name: `PYZ${i}.pyz`,
      typeCode: 'z', payload, compress: true}))});
  writeFileSync(new URL(`pyinstaller-${count}`, import.meta.url), bytes);
  console.log(`images=${count}, bytes=${bytes.length}`);
}
'''
(work / 'generate.mjs').write_text(script)
subprocess.run(['node', '--import', 'tsx', str(work / 'generate.mjs')],
               env=env, check=True)
for count in [1, 129]:
    arguments = ['node', 'scripts/rea.mjs', 'inspect-artifact',
                 str(work / f'pyinstaller-{count}'), '--json']
    result = subprocess.run(arguments, env=env, text=True, capture_output=True)
    parsed = json.loads(result.stdout)
    print(f'images={count}, exit={result.returncode}, stderr_bytes={len(result.stderr)}')
    if count == 129:
        print(json.dumps(parsed, indent=2))
        repeat = subprocess.run(arguments, env=env, text=True, capture_output=True)
        print('repeat_identical=' + str((result.returncode, result.stdout, result.stderr)
                                       == (repeat.returncode, repeat.stdout, repeat.stderr)))

The failing case exits 1 with empty stderr. The one-image control exits 0 with empty stderr. The script invokes node scripts/rea.mjs inspect-artifact <fixture> --json; each generated member has an empty valid PYZ payload wrapped in outer zlib compression.

Expected behavior

Keep the intentional safe refusal, identify the actual capacity allowance in the user-facing diagnosis and recovery, and preserve the producer's resource distinction in structured error data. Recovery should explain the fixed 128-file allowance and the operation's available choices. It should not imply that this command can select a narrower internal path.

This expectation follows the repository's requirement to preserve meaningful failure reasons at typed boundaries in AGENTS.md. The producer contract explicitly defines a limit boundary so callers need not infer it in ArtifactReader.ts. The capacity guard documents that its allowance is independent of byte budgets in PyInstallerPyzStore.ts, and emits the exact constraint with boundary: "capacity" at the refusal. The CLI contract exposes no member-path filter.

This finding does not establish that truncated must become resource_constraint, prescribe a new schema key, or request a larger allowance.

Impact and scope

P2: a bounded diagnostic and recovery defect. An analyst sees size-oriented guidance for a filesystem-capacity refusal, and a structured caller loses the typed resource distinction unless it infers it from prose. Reading details.detail is an available manual diagnostic workaround. It does not restore a structured boundary or make internal-path narrowing available.

The observed scope is inspect-artifact on the generated PyInstaller fixture on the platform below. The intentional refusal still occurs; no incorrect successful inventory, data loss or isolation failure was observed. A separate executed 117-byte ZIP with a central-directory declaration of 300,000,000 uncompressed bytes also returned the generic message/action while retaining its actual 268,435,456-byte decompression-budget explanation in details.detail. That ZIP probes declared expansion admission, not successful decompression.

Other artifact operations use the same translation and presentation functions by source inspection, but MCP, extraction, Godot metadata refusals and other platforms were not executed for this report.

REA version

rea-agents 6.7.0; source revision a5f982de8d, using the checkout's existing compiled CLI.

Runtime and platform

Node v26.12.0; macOS 27.0.1; arm64; Python 3 and repository-installed tsx for fixture generation; no Hopper, Ghidra or IDA involved.

Target and artifact context

Synthetic ELF with appended PyInstaller 2.1 CArchive, Python version field 309, and 129 empty valid PYZ images stored as outer-compressed type-z members; one-image control.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Trimmed stdout from the executed replay script; no field in the failing JSON was omitted:

images=1, bytes=207
images=129, bytes=7247
images=1, exit=0, stderr_bytes=0
images=129, exit=1, stderr_bytes=0
{
  "error": "Analysis failed",
  "code": "truncated",
  "category": "truncated",
  "message": "Artifact is too large to process safely. Narrow the requested path or use a smaller artifact.",
  "retryable": false,
  "remediation": {
    "action": "Artifact is too large to process safely. Narrow the requested path or use a smaller artifact."
  },
  "details": {
    "operation": "inspect_artifact",
    "reason": "limit",
    "truncated": true,
    "detail": "PyInstaller compressed PYZ images exceed the 128-file private storage budget"
  }
}
repeat_identical=True

Source inspection identifies the reachable guard in PyInstallerArtifactReader.ts: it counts outer-compressed type-z entries and admits the complete private-file footprint before spooling. The observed error confirms that this guard, rather than a byte-expansion or metadata guard, rejected the fixture.

Typed outcome or recovery expected

Retain the precise constraint already available in details.detail, expose its capacity nature without requiring prose parsing, and give recovery that applies to this operation. Preserve the refusal and its non-retryable status. No restoration or fix was tested.

Analysis or suspected cause

Source-derived: translateArtifactFailure copies the reason and message into the domain error, but does not copy the reader boundary into its public fields. It retains the original cause internally. The details projection emits no boundary, and artifactMessage uses a fixed size sentence for limit, ignoring detail. Remediation falls back to that same message.

Additional context

No last-known-good revision was established. Read-only searches of open and closed issues and PRs found related integrity, overwrite and other fixed-budget reports, but no same capacity-boundary failure. Related: #5808 proposes a broader audit of residual fixed-allowance recovery without reporting this capacity-boundary failure.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5836 ### 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 An executed public CLI reproduction with a 7,247-byte synthetic PyInstaller executable containing 129 outer-compressed PYZ images returns `code: truncated` and the same generic sentence for both `message` and `remediation.action`: > Artifact is too large to process safely. Narrow the requested path or use a smaller artifact. The underlying refusal is the reader's 128-file private-storage capacity allowance, independent of input byte size. Its precise explanation survives in `details.detail`, but the reader's typed `capacity` boundary is absent from the public diagnostic. The advertised `inspect-artifact` command has no internal-member selector with which to follow the path-narrowing advice. A 207-byte one-image control succeeds. The repeated 129-image refusal has identical exit status, stdout and stderr. ### Reproduction Run from the source checkout below with its locked dependencies and existing build. Save the following as `reproduce.py` in an external scratch directory, set `VERIFY_WORK_ROOT` to that directory, and execute the script with Python 3. This is the executed fixture and CLI procedure. It uses the repository's own CArchive/PYZ fixture builder through the installed `tsx` dependency, isolates home/configuration/state/cache/temporary paths, and runs no target code or external analysis engine. ```python import json, os, pathlib, subprocess root = pathlib.Path.cwd() work = pathlib.Path(os.environ['VERIFY_WORK_ROOT']).resolve() work.mkdir(exist_ok=True) env = {k: v for k, v in os.environ.items() if not k.startswith(('REA_', 'HOPPER_', 'GHIDRA_')) and k != 'NODE_OPTIONS'} for key, name in [('HOME', 'home'), ('XDG_CONFIG_HOME', 'config'), ('XDG_DATA_HOME', 'data'), ('XDG_CACHE_HOME', 'cache'), ('XDG_STATE_HOME', 'state'), ('TMPDIR', 'temp')]: (work / name).mkdir(exist_ok=True) env[key] = str(work / name) fixture = (root / 'src/artifacts/pyinstaller/CArchive.fixture.ts').as_uri() script = "import {writeFileSync} from 'node:fs';\n" script += 'import {cArchiveExecutable,pyzArchive,PYTHON_39_MAGIC} from ' + json.dumps(fixture) + ';\n' script += ''' const payload = pyzArchive({magic: PYTHON_39_MAGIC, modules: []}); for (const count of [1, 129]) { const bytes = cArchiveExecutable({layout: '2.1', pythonVersion: 309, members: Array.from({length: count}, (_, i) => ({name: `PYZ${i}.pyz`, typeCode: 'z', payload, compress: true}))}); writeFileSync(new URL(`pyinstaller-${count}`, import.meta.url), bytes); console.log(`images=${count}, bytes=${bytes.length}`); } ''' (work / 'generate.mjs').write_text(script) subprocess.run(['node', '--import', 'tsx', str(work / 'generate.mjs')], env=env, check=True) for count in [1, 129]: arguments = ['node', 'scripts/rea.mjs', 'inspect-artifact', str(work / f'pyinstaller-{count}'), '--json'] result = subprocess.run(arguments, env=env, text=True, capture_output=True) parsed = json.loads(result.stdout) print(f'images={count}, exit={result.returncode}, stderr_bytes={len(result.stderr)}') if count == 129: print(json.dumps(parsed, indent=2)) repeat = subprocess.run(arguments, env=env, text=True, capture_output=True) print('repeat_identical=' + str((result.returncode, result.stdout, result.stderr) == (repeat.returncode, repeat.stdout, repeat.stderr))) ``` The failing case exits 1 with empty stderr. The one-image control exits 0 with empty stderr. The script invokes `node scripts/rea.mjs inspect-artifact <fixture> --json`; each generated member has an empty valid PYZ payload wrapped in outer zlib compression. ### Expected behavior Keep the intentional safe refusal, identify the actual capacity allowance in the user-facing diagnosis and recovery, and preserve the producer's resource distinction in structured error data. Recovery should explain the fixed 128-file allowance and the operation's available choices. It should not imply that this command can select a narrower internal path. This expectation follows the repository's requirement to preserve meaningful failure reasons at typed boundaries in [AGENTS.md](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/AGENTS.md#L31-L31). The producer contract explicitly defines a limit boundary so callers need not infer it in [ArtifactReader.ts](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/artifacts/ArtifactReader.ts#L20-L30). The capacity guard documents that its allowance is independent of byte budgets in [PyInstallerPyzStore.ts](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/artifacts/pyinstaller/PyInstallerPyzStore.ts#L25-L27), and emits the exact constraint with `boundary: "capacity"` at [the refusal](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/artifacts/pyinstaller/PyInstallerPyzStore.ts#L50-L56). The [CLI contract](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/cli/artifactCommands.ts#L232-L265) exposes no member-path filter. This finding does not establish that `truncated` must become `resource_constraint`, prescribe a new schema key, or request a larger allowance. ### Impact and scope P2: a bounded diagnostic and recovery defect. An analyst sees size-oriented guidance for a filesystem-capacity refusal, and a structured caller loses the typed resource distinction unless it infers it from prose. Reading `details.detail` is an available manual diagnostic workaround. It does not restore a structured boundary or make internal-path narrowing available. The observed scope is `inspect-artifact` on the generated PyInstaller fixture on the platform below. The intentional refusal still occurs; no incorrect successful inventory, data loss or isolation failure was observed. A separate executed 117-byte ZIP with a central-directory declaration of 300,000,000 uncompressed bytes also returned the generic message/action while retaining its actual 268,435,456-byte decompression-budget explanation in `details.detail`. That ZIP probes declared expansion admission, not successful decompression. Other artifact operations use the same translation and presentation functions by source inspection, but MCP, extraction, Godot metadata refusals and other platforms were not executed for this report. ### REA version rea-agents 6.7.0; source revision a5f982de8dbe4b092cc20ab89d492b7e7028b758, using the checkout's existing compiled CLI. ### Runtime and platform Node v26.12.0; macOS 27.0.1; arm64; Python 3 and repository-installed tsx for fixture generation; no Hopper, Ghidra or IDA involved. ### Target and artifact context Synthetic ELF with appended PyInstaller 2.1 CArchive, Python version field 309, and 129 empty valid PYZ images stored as outer-compressed type-z members; one-image control. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Trimmed stdout from the executed replay script; no field in the failing JSON was omitted: ```text images=1, bytes=207 images=129, bytes=7247 images=1, exit=0, stderr_bytes=0 images=129, exit=1, stderr_bytes=0 { "error": "Analysis failed", "code": "truncated", "category": "truncated", "message": "Artifact is too large to process safely. Narrow the requested path or use a smaller artifact.", "retryable": false, "remediation": { "action": "Artifact is too large to process safely. Narrow the requested path or use a smaller artifact." }, "details": { "operation": "inspect_artifact", "reason": "limit", "truncated": true, "detail": "PyInstaller compressed PYZ images exceed the 128-file private storage budget" } } repeat_identical=True ``` Source inspection identifies the reachable guard in [PyInstallerArtifactReader.ts](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/artifacts/PyInstallerArtifactReader.ts#L423-L438): it counts outer-compressed type-z entries and admits the complete private-file footprint before spooling. The observed error confirms that this guard, rather than a byte-expansion or metadata guard, rejected the fixture. ### Typed outcome or recovery expected Retain the precise constraint already available in `details.detail`, expose its capacity nature without requiring prose parsing, and give recovery that applies to this operation. Preserve the refusal and its non-retryable status. No restoration or fix was tested. ### Analysis or suspected cause Source-derived: [translateArtifactFailure](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/artifacts/ArtifactOperationFailure.ts#L20-L38) copies the reason and message into the domain error, but does not copy the reader boundary into its public fields. It retains the original cause internally. The [details projection](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorProjection.ts#L356-L367) emits no boundary, and [artifactMessage](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorPresentation.ts#L432-L441) uses a fixed size sentence for `limit`, ignoring `detail`. [Remediation falls back to that same message](https://github.com/morluto/rea/blob/a5f982de8dbe4b092cc20ab89d492b7e7028b758/src/domain/analysisErrorPresentation.ts#L133-L142). ### Additional context No last-known-good revision was established. Read-only searches of open and closed issues and PRs found related integrity, overwrite and other fixed-budget reports, but no same capacity-boundary failure. Related: [#5808](https://github.com/morluto/rea/issues/5808) proposes a broader audit of residual fixed-allowance recovery without reporting this capacity-boundary failure.
Author
Owner

@XinwangTao commented on GitHub (Oct 21, 2026):

Confirmed — the generic "too large / narrow the path" message misrepresents a fixed 128-file capacity refusal and recommends an action the inspect-artifact CLI cannot perform (no member-path filter exists). The typed boundary: "capacity" and the precise explanation are already present at the producer, so propagating the boundary into the public diagnostic and aligning remediation with actually available choices (not implying narrower-path selection) would fix the mismatch without changing the refusal itself. The ZIP budget case confirms this is a general translation issue, not PyInstaller-specific.

<!-- gh-comment-id:6312046616 --> @XinwangTao commented on GitHub (Oct 21, 2026): Confirmed — the generic "too large / narrow the path" message misrepresents a fixed 128-file capacity refusal and recommends an action the inspect-artifact CLI cannot perform (no member-path filter exists). The typed boundary: "capacity" and the precise explanation are already present at the producer, so propagating the boundary into the public diagnostic and aligning remediation with actually available choices (not implying narrower-path selection) would fix the mismatch without changing the refusal itself. The ZIP budget case confirms this is a general translation issue, not PyInstaller-specific.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Artifact reader boundary now survives translation and public projection, limit messages use the precise producer detail, and capacity-specific recovery advises respecting reported capacity rather than narrowing an unsupported member path. The original PyInstaller 128-file capacity guard remains. The translation regression asserts boundary/detail/message/nonretryability for capacity/file-size/memory.

Implementation: ArtifactOperationFailure.ts, analysisErrorPresentation.ts.

The relevant regression checks in ArtifactOperationFailure.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380735817 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Artifact reader boundary now survives translation and public projection, limit messages use the precise producer detail, and capacity-specific recovery advises respecting reported capacity rather than narrowing an unsupported member path. The original PyInstaller 128-file capacity guard remains. The translation regression asserts boundary/detail/message/nonretryability for capacity/file-size/memory. Implementation: [ArtifactOperationFailure.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/ArtifactOperationFailure.ts#L32), [analysisErrorPresentation.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/analysisErrorPresentation.ts#L111). The relevant regression checks in [ArtifactOperationFailure.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/ArtifactOperationFailure.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3532
No description provided.