fix: pos config secret values corrupted by stray newline in captured input

cfg_read_secret is invoked via $() inside _cfg_edit_one, so the echo that
advanced the cursor after hidden input landed on the capture pipe instead of
the terminal — every secret value stored with pos config on a real TTY got a
leading newline (e.g. AI_GEMINI_API_KEY="\n<key>"), which neither cfg_value
nor the ai/telegram load_config parsers could read back: the menu showed
'(not set)' and pos ai gemini kept demanding a key.

Fix: send that newline to stderr (>&2). Defense in depth: cfg_write and
write_config_key now strip CR and truncate multi-line pastes, and the readers
(cfg_value, ai + telegram load_config) strip CR. Reproduced and verified on a
pseudo-TTY; piped-stdin tests never caught it because non-TTY input skips the
stty/echo path.
This commit is contained in:
Your Name
2026-08-09 15:08:29 +00:00
parent 23eede637a
commit 1832a88c71
6 changed files with 18 additions and 3 deletions
+2
View File
@@ -16,6 +16,8 @@ summary (newest last).
## Done ## Done
- **2026-08-09** — Fixed `pos config` secret-value corruption: `cfg_read_secret`'s cursor-advance `echo` went to stdout and, since the function is called via `$(...)`, a leading `\n` ended up inside every secret value → the env file got `AI_GEMINI_API_KEY="\n<key>"`, unreadable by `cfg_value`/`load_config` (menu showed `(not set)`, `pos ai gemini` demanded a key). The newline now goes to the terminal (`echo >&2`). Defense in depth: `cfg_write`/`write_config_key` strip CR and truncate multi-line pastes (warn), `cfg_value` and the ai/telegram `load_config`s strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path).
- **2026-08-09** — `ai` category — `pos ai gemini` (ask/chat/models) via Google Gemini REST API. `ask` prints only the answer (pipe/script/Telegram-friendly), `chat` is a multi-turn REPL (q/quit/Ctrl+C, `/reset`, empty input re-prompts), `models` lists generateContent-capable ids and flags the default; `--model` override; default `gemini-2.5-flash`. Config scope `ai` (`AI_GEMINI_API_KEY` secret + `AI_GEMINI_MODEL`) in `~/.config/linux_post_install/ai.env`, edited via `pos config ai`; `config/ai.env` template installed no-clobber by postinstall; `ai-gemini` added to `INTERACTIVE_CMDS`. Telegram listener now answers non-command messages starting with `ai ` via `pos ai gemini ask` (owner chat only; error replies carry the `pos config ai` hint) — future intents (reminders) slot in as more case arms in `handle_message`. Docs: POS.md `ai` section + listener bridge, howto/ai.md, HOWTO/README index rows, `bin/pos` usage example. - **2026-08-09** — `ai` category — `pos ai gemini` (ask/chat/models) via Google Gemini REST API. `ask` prints only the answer (pipe/script/Telegram-friendly), `chat` is a multi-turn REPL (q/quit/Ctrl+C, `/reset`, empty input re-prompts), `models` lists generateContent-capable ids and flags the default; `--model` override; default `gemini-2.5-flash`. Config scope `ai` (`AI_GEMINI_API_KEY` secret + `AI_GEMINI_MODEL`) in `~/.config/linux_post_install/ai.env`, edited via `pos config ai`; `config/ai.env` template installed no-clobber by postinstall; `ai-gemini` added to `INTERACTIVE_CMDS`. Telegram listener now answers non-command messages starting with `ai ` via `pos ai gemini ask` (owner chat only; error replies carry the `pos config ai` hint) — future intents (reminders) slot in as more case arms in `handle_message`. Docs: POS.md `ai` section + listener bridge, howto/ai.md, HOWTO/README index rows, `bin/pos` usage example.
- **2026-08-09** — Entertainment plugins `gold` + `weather` now emit emoji-visualized Telegram messages. Gold: headline is USD/**gram** (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (`+00:00`/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode. - **2026-08-09** — Entertainment plugins `gold` + `weather` now emit emoji-visualized Telegram messages. Gold: headline is USD/**gram** (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (`+00:00`/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode.
+2 -2
View File
@@ -560,9 +560,9 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) | | `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
<!-- GEN:START filetable --> <!-- GEN:START filetable -->
| `bin/pos` | 277 | CLI dispatcher with smart arg matching + logging + category help | | `bin/pos` | 277 | CLI dispatcher with smart arg matching + logging + category help |
| `bin/pos-ai-gemini` | 197 | Chat with Google Gemini (ask, chat, models) | | `bin/pos-ai-gemini` | 198 | Chat with Google Gemini (ask, chat, models) |
| `bin/pos-communication-telegram-listener` | 526 | Telegram bot listener: map /command → bash, run them on chat messages | | `bin/pos-communication-telegram-listener` | 526 | Telegram bot listener: map /command → bash, run them on chat messages |
| `bin/pos-communication-telegram-sender` | 220 | Send Telegram messages/files/links/stickers via Bot API (send, test) | | `bin/pos-communication-telegram-sender` | 221 | Send Telegram messages/files/links/stickers via Bot API (send, test) |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) | | `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
| `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) | | `bin/pos-docker-compose` | 366 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
| `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) | | `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) |
+1
View File
@@ -52,6 +52,7 @@ load_config() {
\#*) continue ;; \#*) continue ;;
esac esac
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
v="${v//$'\r'/}"
if [ -z "${!k:-}" ]; then if [ -z "${!k:-}" ]; then
export "$k"="$v" export "$k"="$v"
fi fi
+1
View File
@@ -67,6 +67,7 @@ load_config() {
\#*) continue ;; \#*) continue ;;
esac esac
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
v="${v//$'\r'/}"
if [ -z "${!k:-}" ]; then if [ -z "${!k:-}" ]; then
export "$k"="$v" export "$k"="$v"
fi fi
+10 -1
View File
@@ -171,6 +171,7 @@ cfg_value() {
local file="$1" key="$2" v local file="$1" key="$2" v
[ -f "$file" ] || return 0 [ -f "$file" ] || return 0
v="$(sed -n "s|^${key}=||p" "$file" | tail -1)" v="$(sed -n "s|^${key}=||p" "$file" | tail -1)"
v="${v//$'\r'/}"
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}" v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
printf '%s' "$v" printf '%s' "$v"
} }
@@ -189,6 +190,11 @@ cfg_write() {
chmod 600 "$file" chmod 600 "$file"
return 0 return 0
fi fi
val="${val//$'\r'/}"
if [[ "$val" == *$'\n'* ]]; then
val="${val%%$'\n'*}"
warn "multi-line paste — using first line only"
fi
tmp="$(mktemp)" tmp="$(mktemp)"
grep -v "^${key}=" "$file" 2>/dev/null >"$tmp" || true grep -v "^${key}=" "$file" 2>/dev/null >"$tmp" || true
printf '%s="%s"\n' "$key" "$val" >>"$tmp" printf '%s="%s"\n' "$key" "$val" >>"$tmp"
@@ -230,6 +236,9 @@ cfg_validate() {
# Read a value with echo off (secret flags). Falls back to plain read when # Read a value with echo off (secret flags). Falls back to plain read when
# stdin is not a TTY (e.g. piped menu input). # stdin is not a TTY (e.g. piped menu input).
# The cursor-advancing newline after the hidden input MUST go to the terminal
# (>&2), not stdout — cfg_read_secret is called via $(), so anything on stdout
# is captured into the value; a stray leading newline corrupted ai.env.
cfg_read_secret() { cfg_read_secret() {
local val echo_off=0 local val echo_off=0
if [ -t 0 ]; then if [ -t 0 ]; then
@@ -238,7 +247,7 @@ cfg_read_secret() {
read -r val || true read -r val || true
if [ "$echo_off" -eq 1 ]; then if [ "$echo_off" -eq 1 ]; then
stty echo 2>/dev/null || true stty echo 2>/dev/null || true
echo echo >&2
fi fi
printf '%s' "$val" printf '%s' "$val"
} }
+2
View File
@@ -25,6 +25,8 @@ config_value() {
write_config_key() { write_config_key() {
local key="$1" val="$2" tmp local key="$1" val="$2" tmp
val="${val//$'\r'/}"
val="${val%%$'\n'*}"
mkdir -p "$CONFIG_DIR" mkdir -p "$CONFIG_DIR"
tmp="$(mktemp)" tmp="$(mktemp)"
grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true grep -v "^${key}=" "$CONFIG_FILE" 2>/dev/null >"$tmp" || true