[GH-ISSUE #5177] [Bug] Apktool reports an existing non-executable launcher as missing #3177

Closed
opened 2026-10-31 00:37:31 +00:00 by bsakaguchi · 3 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Provider or capability discovery

Observed behavior

An existing regular file with mode 0644 produced native spawn error EACCES. REA availability reported executable_missing; the source CLI returned capability_unavailable and a message containing was not found, with advice to set an absolute launcher path that was already supplied. Neither projected result exposed EACCES. The nonexistent control received the same diagnosis. After changing only the existing fixture to mode 0755, client inspection succeeded with reported version 3.0.3.

Reproduction

From the repository root with its pinned Node and locked dependencies available, run this isolated source-CLI probe. It creates only a local launcher fixture and never decodes an APK.

WORK=$(mktemp -d)
mkdir -p "$WORK/home" "$WORK/config" "$WORK/cache" "$WORK/temp"
cat > "$WORK/reproduce.mts" <<'PROBE'
import { writeFile, chmod, stat } from 'node:fs/promises';
import { spawn } from 'node:child_process';
import { resolve, join } from 'node:path';
import { pathToFileURL } from 'node:url';
const root = process.env.PROBE_ROOT;
const launcher = join(root, 'apktool');
await writeFile(launcher, '#!/bin/sh\nprintf "3.0.3\\n"\n');
await chmod(launcher, 0o644);
const rawSpawnError = await new Promise(resolve => {
  spawn(launcher, ['--version']).once('error', error => resolve(error.code));
});
const { createCli } = await import(pathToFileURL(resolve('src/cli.ts')).href);
const { ApktoolProvider } = await import(pathToFileURL(resolve('src/apktool/ApktoolProvider.ts')).href);
for (const [label, command] of [['denied', launcher], ['missing', join(root, 'absent')]]) {
  const environment = { ...process.env, REA_APKTOOL_COMMAND: command };
  const provider = new ApktoolProvider({ environment });
  let availability;
  try { availability = await provider.inspectAvailability(); }
  finally { await provider.close(); }
  let stdout = '';
  await createCli(environment).serve(['inspect-apktool-client', '--json'], {
    stdout: chunk => { stdout += chunk; }, exit: () => {}
  });
  const result = JSON.parse(stdout);
  console.log(JSON.stringify({ label, fileExists: label === 'denied' && (await stat(command)).isFile(),
    ...(label === 'denied' ? { rawSpawnError } : {}), availabilityCode: availability.code,
    cliCode: result.code, assertsNotFound: result.message.includes('was not found'),
    exposesEACCES: stdout.includes('EACCES') }));
}
await chmod(launcher, 0o755);
let stdout = '';
await createCli({ ...process.env, REA_APKTOOL_COMMAND: launcher }).serve(['inspect-apktool-client', '--json'], {
  stdout: chunk => { stdout += chunk; }, exit: () => {}
});
console.log('runnable-control', stdout.trim());
process.exitCode = 0;
PROBE
env -i PATH="$PATH" HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \
  XDG_CACHE_HOME="$WORK/cache" TMPDIR="$WORK/temp" PROBE_ROOT="$WORK" \
  node --import tsx "$WORK/reproduce.mts"

The same probe was executed locally. The denied and missing cases each reproduced twice across two focused runs; the executable control succeeded once. The source CLI registry and production provider are real; the launcher is an inert fixture, not a real Apktool engine. No compiled executable launcher or published-package runtime was run.

Expected behavior

The readiness diagnostic should preserve the meaningful permission failure for the selected launcher instead of asserting that it does not exist. Refusal should remain unavailable. A nonexistent launcher should retain its missing-launcher diagnosis, and the executable fixture should still report its version.

Impact and scope

The incorrect diagnostic reproduced with explicit absolute launcher selection on macOS. Correct refusal still prevents execution; the demonstrated defect is the misleading reason and recovery advice. Correcting the fixture's executable permission restored readiness. User frequency and downstream consumers' behavior are unknown; other platforms, PATH selection, decoding and MCP transport were not tested.

REA version

Executed source main 9af0b6113c81803f88ec7d242e8fabaaa5d7f1f3, package metadata 6.7.0. Latest published release: rea-agents-6.7.0 at 27dc42d10249f0410904a75c0997842eb03b931c. Relevant probe/launcher code is identical in the release and main; release runtime was not separately executed.

Runtime and platform

Node.js v24.18.0; macOS 27.0.1 arm64; locked source dependencies. The fixture is an inert shell script, with no real Apktool/JVM.

Target and artifact context

No APK or analysis target. A local launcher fixture selected by an absolute REA_APKTOOL_COMMAND path; its only intended output is a version string. Home, configuration, cache and temporary directories are isolated.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

{"label":"denied","fileExists":true,"rawSpawnError":"EACCES","availabilityCode":"executable_missing","cliCode":"capability_unavailable","assertsNotFound":true,"exposesEACCES":false}
{"label":"missing","fileExists":false,"availabilityCode":"executable_missing","cliCode":"capability_unavailable","assertsNotFound":true,"exposesEACCES":false}

The runnable control returned successful inspection Evidence with normalized_result.client.apktool_version: "3.0.3". The script prints its entire successful result.

Expected source: the repository contract requires preserving meaningful failure reasons. Failure source: the executed source CLI probe above; the failure projection explains why a native permission failure receives the missing-launcher message. The internal cause retains the errno, but the observed projected results do not.

Typed outcome or recovery expected

Keep the unavailable outcome and expose a truthful permission-related reason so the caller can repair the selected launcher's access. Preserve the absent-path and executable controls.

Additional context

Related issue #1945 concerns generic target advice and relative selection. This fixture supplies an absolute existing path and reproduces after that correction; it concerns loss of a native permission failure.

Triage assessment

  • Impact level: P3
  • Assessment status: supported
  • Impact basis: The observed defect is an incorrect diagnosis and recovery message for a refused launcher. Refusal remains correct, and no changed downstream execution behavior or data loss was established.
  • Workaround status: available
  • Workaround basis: Checking and correcting the executable permission on the selected fixture restored successful inspection in the same probe. Real launcher permissions should be corrected only when execution is intended and authorized.
Originally created by @coygeek on GitHub (Oct 19, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5177 ## 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 Provider or capability discovery ## Observed behavior An existing regular file with mode `0644` produced native spawn error `EACCES`. REA availability reported `executable_missing`; the source CLI returned `capability_unavailable` and a message containing `was not found`, with advice to set an absolute launcher path that was already supplied. Neither projected result exposed `EACCES`. The nonexistent control received the same diagnosis. After changing only the existing fixture to mode `0755`, client inspection succeeded with reported version `3.0.3`. ## Reproduction From the repository root with its pinned Node and locked dependencies available, run this isolated source-CLI probe. It creates only a local launcher fixture and never decodes an APK. ```sh WORK=$(mktemp -d) mkdir -p "$WORK/home" "$WORK/config" "$WORK/cache" "$WORK/temp" cat > "$WORK/reproduce.mts" <<'PROBE' import { writeFile, chmod, stat } from 'node:fs/promises'; import { spawn } from 'node:child_process'; import { resolve, join } from 'node:path'; import { pathToFileURL } from 'node:url'; const root = process.env.PROBE_ROOT; const launcher = join(root, 'apktool'); await writeFile(launcher, '#!/bin/sh\nprintf "3.0.3\\n"\n'); await chmod(launcher, 0o644); const rawSpawnError = await new Promise(resolve => { spawn(launcher, ['--version']).once('error', error => resolve(error.code)); }); const { createCli } = await import(pathToFileURL(resolve('src/cli.ts')).href); const { ApktoolProvider } = await import(pathToFileURL(resolve('src/apktool/ApktoolProvider.ts')).href); for (const [label, command] of [['denied', launcher], ['missing', join(root, 'absent')]]) { const environment = { ...process.env, REA_APKTOOL_COMMAND: command }; const provider = new ApktoolProvider({ environment }); let availability; try { availability = await provider.inspectAvailability(); } finally { await provider.close(); } let stdout = ''; await createCli(environment).serve(['inspect-apktool-client', '--json'], { stdout: chunk => { stdout += chunk; }, exit: () => {} }); const result = JSON.parse(stdout); console.log(JSON.stringify({ label, fileExists: label === 'denied' && (await stat(command)).isFile(), ...(label === 'denied' ? { rawSpawnError } : {}), availabilityCode: availability.code, cliCode: result.code, assertsNotFound: result.message.includes('was not found'), exposesEACCES: stdout.includes('EACCES') })); } await chmod(launcher, 0o755); let stdout = ''; await createCli({ ...process.env, REA_APKTOOL_COMMAND: launcher }).serve(['inspect-apktool-client', '--json'], { stdout: chunk => { stdout += chunk; }, exit: () => {} }); console.log('runnable-control', stdout.trim()); process.exitCode = 0; PROBE env -i PATH="$PATH" HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \ XDG_CACHE_HOME="$WORK/cache" TMPDIR="$WORK/temp" PROBE_ROOT="$WORK" \ node --import tsx "$WORK/reproduce.mts" ``` The same probe was executed locally. The denied and missing cases each reproduced twice across two focused runs; the executable control succeeded once. The source CLI registry and production provider are real; the launcher is an inert fixture, not a real Apktool engine. No compiled executable launcher or published-package runtime was run. ## Expected behavior The readiness diagnostic should preserve the meaningful permission failure for the selected launcher instead of asserting that it does not exist. Refusal should remain unavailable. A nonexistent launcher should retain its missing-launcher diagnosis, and the executable fixture should still report its version. ## Impact and scope The incorrect diagnostic reproduced with explicit absolute launcher selection on macOS. Correct refusal still prevents execution; the demonstrated defect is the misleading reason and recovery advice. Correcting the fixture's executable permission restored readiness. User frequency and downstream consumers' behavior are unknown; other platforms, PATH selection, decoding and MCP transport were not tested. ## REA version Executed source main `9af0b6113c81803f88ec7d242e8fabaaa5d7f1f3`, package metadata 6.7.0. Latest published release: `rea-agents-6.7.0` at `27dc42d10249f0410904a75c0997842eb03b931c`. Relevant probe/launcher code is identical in the release and main; release runtime was not separately executed. ## Runtime and platform Node.js v24.18.0; macOS 27.0.1 arm64; locked source dependencies. The fixture is an inert shell script, with no real Apktool/JVM. ## Target and artifact context No APK or analysis target. A local launcher fixture selected by an absolute `REA_APKTOOL_COMMAND` path; its only intended output is a version string. Home, configuration, cache and temporary directories are isolated. ## Interface CLI ## Earliest failure stage Setup or provider admission ## Relevant output or evidence ```json {"label":"denied","fileExists":true,"rawSpawnError":"EACCES","availabilityCode":"executable_missing","cliCode":"capability_unavailable","assertsNotFound":true,"exposesEACCES":false} {"label":"missing","fileExists":false,"availabilityCode":"executable_missing","cliCode":"capability_unavailable","assertsNotFound":true,"exposesEACCES":false} ``` The runnable control returned successful inspection Evidence with `normalized_result.client.apktool_version: "3.0.3"`. The script prints its entire successful result. Expected source: the [repository contract](https://github.com/morluto/rea/blob/9af0b6113c81803f88ec7d242e8fabaaa5d7f1f3/AGENTS.md#L31) requires preserving meaningful failure reasons. Failure source: the executed source CLI probe above; the [failure projection](https://github.com/morluto/rea/blob/9af0b6113c81803f88ec7d242e8fabaaa5d7f1f3/src/apktool/ApktoolConfiguration.ts#L506-L520) explains why a native permission failure receives the missing-launcher message. The internal cause retains the errno, but the observed projected results do not. ## Typed outcome or recovery expected Keep the unavailable outcome and expose a truthful permission-related reason so the caller can repair the selected launcher's access. Preserve the absent-path and executable controls. ## Additional context Related [issue #1945](https://github.com/morluto/rea/issues/1945) concerns generic target advice and relative selection. This fixture supplies an absolute existing path and reproduces after that correction; it concerns loss of a native permission failure. ## Triage assessment - Impact level: P3 - Assessment status: supported - Impact basis: The observed defect is an incorrect diagnosis and recovery message for a refused launcher. Refusal remains correct, and no changed downstream execution behavior or data loss was established. - Workaround status: available - Workaround basis: Checking and correcting the executable permission on the selected fixture restored successful inspection in the same probe. Real launcher permissions should be corrected only when execution is intended and authorized.
Author
Owner

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

Agree — the diagnostic is misleading: a permission failure (EACCES) gets collapsed into the missing-launcher message even though the file exists and the absolute path is already correct. The fix should differentiate the spawn errno at the failure-projection layer: EACCES should surface as a permission-related reason (e.g. "launcher exists but is not executable") while ENOENT keeps the missing-launcher diagnosis. That preserves the unavailable outcome but gives the caller an actionable reason instead of telling them to re-supply the path they already provided. Nice repro isolating the denied and missing cases with the same fixture.

<!-- gh-comment-id:6259945588 --> @XinwangTao commented on GitHub (Oct 19, 2026): Agree — the diagnostic is misleading: a permission failure (EACCES) gets collapsed into the missing-launcher message even though the file exists and the absolute path is already correct. The fix should differentiate the spawn errno at the failure-projection layer: EACCES should surface as a permission-related reason (e.g. "launcher exists but is not executable") while ENOENT keeps the missing-launcher diagnosis. That preserves the unavailable outcome but gives the caller an actionable reason instead of telling them to re-supply the path they already provided. Nice repro isolating the denied and missing cases with the same fixture.
Author
Owner

@Yi-111-a commented on GitHub (Oct 19, 2026):

Draft PR: https://github.com/morluto/rea/pull/5180

Splits ENOENT from EACCES/EPERM in the apktool launcher version-probe failure, so a launcher that exists but is not executable reports a permission reason with a filesystem_error diagnostic and execute-access remediation instead of executable_missing / "was not found". ProviderRejectionCode is unchanged; refusal behavior is unchanged. Kept as draft pending maintainer review.

<!-- gh-comment-id:6259952083 --> @Yi-111-a commented on GitHub (Oct 19, 2026): Draft PR: https://github.com/morluto/rea/pull/5180 Splits `ENOENT` from `EACCES`/`EPERM` in the apktool launcher version-probe failure, so a launcher that exists but is not executable reports a permission reason with a `filesystem_error` diagnostic and execute-access remediation instead of `executable_missing` / "was not found". `ProviderRejectionCode` is unchanged; refusal behavior is unchanged. Kept as draft pending maintainer review.
Author
Owner

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

Checked resolved on main at d87e73f2db.

I replayed the source provider and compiled public CLI using an inert launcher matrix. A directory and an existing 0644 file both retain EACCES, the selected command and execute-permission/file-type recovery, without claiming the launcher was not found. The absent path retains distinct ENOENT/missing-launcher advice. Changing only the file permission to 0755 succeeds with the expected version; successful unrecognized output remains a separate version failure. This verifies the original EACCES report; no native EPERM, real Apktool/JVM or device run is claimed.

Relevant implementation: ApktoolConfiguration.ts, ApktoolConfiguration.ts.

Closing as completed on main.

<!-- gh-comment-id:6380754044 --> @coygeek commented on GitHub (Oct 24, 2026): Checked resolved on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. I replayed the source provider and compiled public CLI using an inert launcher matrix. A directory and an existing 0644 file both retain `EACCES`, the selected command and execute-permission/file-type recovery, without claiming the launcher was not found. The absent path retains distinct ENOENT/missing-launcher advice. Changing only the file permission to 0755 succeeds with the expected version; successful unrecognized output remains a separate version failure. This verifies the original EACCES report; no native EPERM, real Apktool/JVM or device run is claimed. Relevant implementation: [ApktoolConfiguration.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/apktool/ApktoolConfiguration.ts#L523), [ApktoolConfiguration.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/apktool/ApktoolConfiguration.ts#L449). 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#3177
No description provided.