From c922d2c26ff36661867b2ea2e04a85edb232806d Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Wed, 19 Aug 2026 08:57:50 +0200 Subject: [PATCH] fix(homeassistant): name the missing token variable The work laptop reported only that the Home Assistant config was unavailable, which lumped together three unrelated causes and left the real one to guesswork. When haToken uses ${ENV_VAR} expansion and the variable is missing from the opencode process environment, the log now names that variable. A token that only partly expands is also reported, since it would otherwise be sent truncated and rejected by Home Assistant. Documents that expansion reads the opencode process environment rather than an interactive shell, that opencode does not read .env files, and that a mise [env] entry does reach the process. Without this the failure is invisible: the notification arrives and tapping it does nothing. --- packages/homeassistant/README.md | 18 ++++++++++++++++ packages/homeassistant/src/index.ts | 33 ++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/homeassistant/README.md b/packages/homeassistant/README.md index 268ca65..7963e83 100644 --- a/packages/homeassistant/README.md +++ b/packages/homeassistant/README.md @@ -210,6 +210,24 @@ To enable this, add the following to your config: | `permissionResponseEntity` | `input_text.opencode_permission_response` | Entity the plugin polls for the user's response | | `permissionTimeout` | `120` | Seconds to wait for a response before giving up | +> [!IMPORTANT] +> `${ENV_VAR}` expansion reads the **opencode process environment**, not your interactive shell. Exporting the variable in `.zshrc` is not enough when opencode is launched from a GUI app, a launch agent, or any non-login context, and opencode does not read `.env` files. If the variable is missing, remote replies are silently skipped: the notification still arrives, but tapping it does nothing. +> +> A reliable option is a mise `[env]` entry, which does reach opencode: +> +> ```toml +> # ~/.config/mise/config.local.toml (machine-local, untracked) +> [env] +> HA_LONG_LIVED_TOKEN = "eyJhbGciOi..." +> ``` +> +> To check what opencode actually sees, run it with `--log-level DEBUG` and answer a prompt. The plugin names the exact missing variable: +> +> ```text +> not polling because haToken references HA_LONG_LIVED_TOKEN, +> which is not set in the opencode process environment +> ``` + **How it works:** 1. Plugin sends the `waiting` webhook (with `waiting.id` set to the permission ID or question request ID) diff --git a/packages/homeassistant/src/index.ts b/packages/homeassistant/src/index.ts index 2cb345a..cf6115f 100644 --- a/packages/homeassistant/src/index.ts +++ b/packages/homeassistant/src/index.ts @@ -211,6 +211,29 @@ export const HomeAssistantPlugin: Plugin = async ({ client, directory }) => { ); staleSessionSweep.unref(); + function unsetTokenVars(): string[] { + if (!config.haToken) return []; + return [...config.haToken.matchAll(/\$\{([^}]+)}/g)] + .map((match) => match[1] as string) + .filter((name) => !process.env[name]); + } + + function describeUnsetTokenVars(names: string[]): string { + return `haToken references ${names.join(', ')}, which ${ + names.length > 1 ? 'are' : 'is' + } not set in the opencode process environment`; + } + + function describeMissingHaConfig(): string | undefined { + if (!config.haApiUrl) return 'haApiUrl is not set'; + if (!config.haToken) return 'haToken is not set'; + if (resolveEnvVars(config.haToken)) return undefined; + + const unset = unsetTokenVars(); + if (unset.length > 0) return describeUnsetTokenVars(unset); + return 'haToken resolved to an empty value'; + } + function resolveHaConfig(): { apiUrl: string; token: string; entity: string } | undefined { if (!config.haApiUrl || !config.haToken) return undefined; const token = resolveEnvVars(config.haToken); @@ -226,12 +249,20 @@ export const HomeAssistantPlugin: Plugin = async ({ client, directory }) => { const ha = resolveHaConfig(); if (!ha) { await report( - `${requestId}: Home Assistant config unavailable (haApiUrl, haToken or ${'${ENV_VAR}'} expansion missing), not polling`, + `${requestId}: not polling because ${describeMissingHaConfig() ?? 'the Home Assistant config is unavailable'}`, undefined, ); return { kind: 'no-config' }; } + const partiallyUnset = unsetTokenVars(); + if (partiallyUnset.length > 0) { + await report( + `${requestId}: ${describeUnsetTokenVars(partiallyUnset)}, so the token is incomplete`, + undefined, + ); + } + const timeoutMs = (config.permissionTimeout ?? DEFAULT_PERMISSION_TIMEOUT) * 1000; const deadline = Date.now() + timeoutMs; activePermissionPolls.add(requestId);