fix(daemon): carry the typed reason on the apps and logs refusals

Merged
TH
2d ago
+87-23

What

Two daemon routes refuse an operation the device's runtime owner did not admit, and neither tells the
caller which operation fact refused.

resolveAppsRuntime (the apps route) and handleLogsCommand (the logs route) each built their
own UNSUPPORTED_OPERATION from the unavailable operation fact. Both kept the message and the hint
and dropped fact.reason, leaving hint text as the only way to tell one refusal from another — and
AGENTS.md says not to match text.

The provider-runtime counterpart of this shipped in #3940 for network dump. This moves the two
remaining routes onto the same shared shape. After this, the only route that still builds this refusal
itself is record, and it already carries details.reason (it also adds a fallback hint of its own,
which is why it keeps its own builder).

How

Both return unsupportedOperationResponse(command, fact) from
packages/daemon/src/core/runtime-admission.ts, the one shape every other runtime-admission refusal
already uses, and the local response literals are deleted.

The wording does not move. The helper builds ${command} is not supported on this device from the
same command word each literal hardcoded, so apps is not supported on this device and
logs is not supported on this device are unchanged. The hint is carried exactly as before.

One wire detail worth stating: logs previously set hint: inspectFact.hint unconditionally, which
put an hint: undefined key on the in-memory object when the fact had no hint. The shared helper
omits the key instead. Over the wire both serialize to no hint, so the payload a client receives
does not change.

No docs change. These two routes now report reasons their sibling routes already report
(owner-capability-missing, unsupported-provider-mode, unsupported-platform-leaf), so no new
reason value enters the surface and no caller-visible wording changes. Neither apps nor logs has a
doc line that documents a refusal reason today, and there is no case where a reader needs one named —
#3940 documented its reason because that one told the reader where traffic evidence lives.

Tests

Both assertions go through the session handler, not the helper.

  • session-inventory-apps-runtime.test.ts: apps refuses an unavailable listApps fact and reports
    details.reason: 'owner-capability-missing' with the hint, and the runtime is never bound. This
    route had no unsupported-path test at all before; runtimeFacts() gained an operations override so
    the case can refuse one cell.
  • session-logs.test.ts: the existing unsupported-precedence case now asserts
    details: { reason: 'owner-capability-missing' } (it was toMatchObject on message and hint), and a
    new case refuses an unavailable app-log cell with unsupported-provider-mode and asserts that reason
    on the wire.

Both were mutation-checked rather than assumed: reverting either route to its hand-built literal makes
the new assertions fail on the missing details.reason (apps: 1 failure; logs: 2), and restoring the
shared call turns them green.

Verification

  • pnpm check:affected --run --head df8cd4f48a — all runnable checks passed, exactCommit=true.
  • pnpm exec vitest run --project unit-core — 15,980 passed, 1 skipped.
  • pnpm exec vitest run --project provider-integration — 214 passed.
  • npx tsc -b clean.
  • Not device-facing: no dispatch, capture, interaction, app lifecycle, or native execution change.

Size

4 files, +87 / −23.


🤖 Generated with Claude Code