fix: refuse component reads when no app is connected - #60
Open
boorad wants to merge 1 commit into
Open
Conversation
`devtools errors` printed "No components with errors or warnings" with
nothing attached. The tree is empty because no app is connected, not
because nothing matched, so a check that was never performed rendered
identically to a check that passed. For an agent reading command output as
evidence that is worse than an error: a vacuous pass propagates.
Refuse instead. `get tree`, `get component`, `find`, `count` and `errors`
now return `{ ok: false, code: 'NO_APP_CONNECTED' }` when no app is
attached. The check reads connection health in the same synchronous turn
that reads the tree, so nothing can attach or detach in between, and the
code is machine-readable so callers need not match on message text.
`get tree` used to attach a "disconnected Ns ago" hint to an empty success.
That context now qualifies the refusal, where it says something actionable
rather than decorating an answer that looked fine.
Profiling commands are untouched: they read captured session data, which
legitimately outlives the app that produced it.
Two auto-restart tests used `get tree` purely to route a command through
ensureDaemon; they now assert the refusal alongside the restart they are
actually about, since `status` bypasses that path.
boorad
added a commit
to boorad/agent-device
that referenced
this pull request
Sep 10, 2026
The probe cannot guarantee the read that follows it observed an app, and its unparseable-status branch let the vacuous result through — fail-open by default in the one place a false clean is the bug. Attachment is checked in the operation that reads the component tree instead, upstream in callstackincubator/agent-react-devtools#60, which this repo picks up through the existing 1:1 passthrough once the pin bumps.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
devtools errorsprintsNo components with errors or warningswith nothing attached. The tree is empty because no app is connected, not because nothing matched, so a check that was never performed renders identically to a check that passed. For an agent reading command output as evidence that is worse than an error, because a vacuous pass propagates into whatever it reports.get tree,get component,find,countanderrorsnow return{ ok: false, code: 'NO_APP_CONNECTED' }when nothing is attached. Connection health is read in the same synchronous turn as the tree, so nothing can attach or detach between the check and the answer, andcodeis machine-readable so callers need not match on message text.cli.tsneeded no change: every read already exits non-zero onok: false.Profiling commands are deliberately untouched, since they read captured session data that legitimately outlives the app that produced it.
Two behavior changes worth calling out:
get treeused to attach adisconnected Ns ago, waiting for reconnect...hint to an empty success. That context now qualifies the refusal instead, where it is actionable rather than decorating an answer that looked fine.connection-health.test.tsis updated to match.daemon-auto-restarttests usedget treeonly to route a command throughensureDaemon. They now assert the refusal alongside the restart they are actually about;statuscannot substitute, because it bypassesensureDaemonentirely.Context
Found while using this package through
agent-device, which passes these commands through 1:1 — reported as callstack/agent-device#2430. I first gated it in that wrapper by probingstatusbefore each read; @thymikee correctly rejected that: a separate probe cannot guarantee the later read observed an app, since the app can disconnect in between, and an unparseable status lets the original vacuous result through. Checking inside the operation that reads the tree is his suggestion, and it is the right one. Reported independently by a second user on RN 0.87.1, where nothing ever attaches, so everyerrorsresult was vacuous.Validation
bun run test125 pass,bun run test:e2e42 pass,typecheckandlintclean,buildsucceeds.New
component-read-attachment.test.tscovers each read refusing with no app ever connected, a read issued after the app disconnected refusing rather than answering from the stale tree, all reads still answering while attached, andstatusstaying answerable throughout. Each new assertion was verified to fail with the daemon guard reverted (7 of them go red), so none of them pass vacuously.Also exercised against the built CLI:
errorsandfindagainst a live daemon with nothing attached both print the refusal and exit 1.