17-point code-level audit executed via Explorer->Architect->Builder->Tester->Reviewer;
Reviewer accepted (APPROVE_WITH_NOTES; 3 block-list items resolved):
- security: telegram sender-owner AND-gate + TELEGRAM_OWNER_ID, matrix
MATRIX_ROOM_ID fail-closed, gpg --passphrase-fd 3 (no argv secret),
/dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE),
eval deny-by-default + --no-command-execution carried by both chat bridges,
tty-gated --trust; config/{telegram,matrix}.env reference templates
- ai: all ExecStart flags validated against installed llama.cpp
(requested->error, default->omit+warn, CONFIG_REQUESTED_FLAGS); single-file
hf download failure rc=1 + no .hf-meta; LLAMACPP_HOST coherent;
POS_SUBCMDS + metadata gaps closed
- tooling: lint-conventions Bash-native rewrite (~24-30x faster, rules and
output byte-identical, :num restored); pos system uninstall covers all 12
libs + scale-tail + flags dir + systemd user units (|| true) + plugin
markers; anchored .bash_completion/.bashrc removal replaces sed -i '/pos/d'
- config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins,
XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to
wrappers; docker-compose deliberately unmigrated (source semantics)
- tests: first committed regression suite — tests/run-tests.sh zero-dep
runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip
contract; systemd-analyze verify on generated unit PASS
Verified: make gen idempotent; make check green; make lint 0 FAIL, 0 WARN;
make test green; bash -n clean; git diff --check clean. Audit deliverables +
agent reports + AGENT_TODO Done entry included.
13 KiB
Builder Report — 2026-09-06 — D-D Config-Loader Centralization
TL;DR
Status: IMPLEMENTED
Objective: Canonical load_env_file in lib/config-ui.sh; migrate 9 hand-rolled loaders; collapse entertainment-lib read/write; decide load_system_env/download/docker-compose adoption; docs; probes.
Files changed (this task): lib/config-ui.sh (+loader, +supersession note), 9 migrated tools (pos-ai, pos-ai-hf, pos-ai-server, pos-communication-{telegram-sender,matrix-listener,telegram-listener,matrix-sender,scrcpy}, pos-media-grab), pos-network-download (load_secret + inline duplicate), lib/entertainment-lib.sh (collapsed helpers + config-ui.sh source), DOC/DEV.md (canonical-loader paragraph), DOC/AGENT_Context_Project.md (hand-maintained line-count row via make gen), this report.
Verification: bash -n all touched files OK; make gen idempotent (zero diff between runs); make check OK; make lint 0 FAIL, 0 WARN; behavioral probes: telegram-sender + pos-ai + pos-network-download env-wins/CRLF/comments OK; ai-server D-F tracking survives (hard error names --ctx-size); entertainment-lib wrapper smoke OK (read/quote-strip/CR-strip/write/delete/chmod-600); docker-compose confirmed NOT migrated (source semantics preserved).
Follow-ups (out of scope, flagged): bin/pos-network-download NET_PROBE line (pre-existing, from a parallel uncommitted change) crashes under set -u — $1/$2 in a single-quoted default inside ${NET_PROBE:-...} are expanded by the outer shell; every invocation without the env var set hits "unbound variable". Owner: the parallel track that introduced exec 3<>/dev/tcp/$1/$2. Also residual hand-rolled loops remain ONLY in lib/common.sh (load_system_env — kept per D-D) and lib/entertainment-plugin-lib.sh (plugin-only lib, must stay self-contained — by design).
Step 0: Scope confirmation
Read the inputs per AGENTS.md before touching code: AgentsReport/architect/2026-09-06_stabilization-design.md (D-D section, esp. the loader contract at ~line 310-350 and the XDG-path replacement rule at line 326-328), AgentsReport/explorer/2026-09-06_tooling-audit.md (Task 3) + AgentsReport/explorer/2026-09-06_ai-audit.md (M3), plus the current working-tree state of every target file (repo carries uncommitted changes from parallel tracks D-A/D-B/D-F/security — always read before editing).
Rules honored: don't touch scripts/lint-conventions.sh, bin/pos-system-uninstall, tests/, apps/, AGENT_TODO.md, listeners' authorization logic; DOC/AGENT_Context_Project.md generated blocks only through make gen; gates = bash -n + make gen×2 idempotent + make check + make lint (0 FAIL, 0 WARN); self-contained tools get source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh".
[DONE]
Step 1: load_env_file in lib/config-ui.sh
Added the canonical loader immediately before cfg_value (now lib/config-ui.sh:331-356). Contract, matching the D-D design and every migrated tool's historic behavior:
load_env_file <file> [scope]—[scope]is informational/reserved.- A bare basename (no
/) resolves under${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}/; full paths used as-is. - Reads
^[A-Z_]+=lines; skips blank +#comments; strips CR from every value (CRLF-safe); trims one pair of surrounding quotes. - Exports a key ONLY when
${!k:-}is empty → env always wins over file (precedence: CLI flags > env > file > defaults — CLI/defaults stay in the tools). - Appends loaded keys to global
LOADED_ENV_KEYS(init guarded viadeclare -p,set -u-safe). - Never creates files, never chmods (chmod-600 stays with
cfg_write); missing/unreadable file = quiet no-op, rc=0. - Doc comment notes it SUPERSEDES
lib/common.sh'sload_system_env()(kept for its 3 existing callers per D-D §"Deprecate load_system_env";common.shmust NOT source config-ui.sh — circular-dependency risk).
Smoke-tested in /tmp/opencode/cfg-probe before wiring tools: env-wins, CRLF strip, comments, XDG basename vs absolute path, missing-file rc=0, LOADED_ENV_KEYS content (only actually-loaded keys).
[DONE]
Step 2: Migrate the 9 hand-rolled loaders
Per-tool migration (each = add config-ui.sh source + replace the hand-rolled while-read body with load_env_file, preserving tool-specific extras):
| Tool | Before (loader) | After | Tool-specific preservation |
|---|---|---|---|
pos-communication-telegram-sender |
inline env-wins loop (no CR strip) | load_config() { load_env_file "$CONFIG_FILE"; } |
— (self-contained: owns guarded CONFIG_DIR) |
pos-communication-matrix-listener |
inline env-wins loop (no CR strip) | load_config() { load_env_file "$CONFIG_FILE"; } |
— |
pos-communication-telegram-listener |
inline env-wins loop (no CR strip) | load_config() { load_env_file "$CONFIG_FILE"; } |
— |
pos-communication-matrix-sender |
inline env-wins loop + CR strip | load_config() { load_env_file "$CONFIG_FILE"; } |
— |
pos-communication-scrcpy |
inline env-wins loop + CR strip; hardcoded $HOME/.config/.../scrcpy.env |
load_config() { load_env_file "$CONFIG_FILE"; }; CONFIG_FILE="$CONFIG_DIR/scrcpy.env" |
XDG path fix (design line 328) |
pos-ai |
load_config loop + legacy two-file loop |
load_env_file "$CONFIG_FILE" + legacy loop stays as second load_env_file per legacy path |
legacy LEGACY_GEMINI_CONFIG/LEGACY_OPENROUTER_CONFIG loop structure kept; both legacy vars now :--guarded with $CONFIG_DIR defaults; CONFIG_FILE gets env-override seam (${CONFIG_FILE:-$CONFIG_DIR/ai.env}) |
pos-ai-server |
load_config loop + CR strip |
load_env_file "$CONFIG_FILE" |
D-F tracking untouched: CONFIG_REQUESTED_FLAGS/requested_from_env_config is VALUE-based (reads exported LLAMACPP_* after load) → unaffected by loader; CONFIG_FILE="${CONFIG_FILE:-$CONFIG_DIR/ai.env}" keeps the env-override seam (tests/t-ai-server-flags.sh passes CONFIG_FILE=…) and fixes XDG |
pos-ai-hf |
load_hf_config loop + CR strip |
load_hf_config() { load_env_file "$CONFIG_FILE"; } |
CONFIG_FILE :--guarded, $CONFIG_DIR default |
pos-media-grab |
load_grab_config loop (no CR strip); hardcoded $HOME/.config/.../grab.env |
load_grab_config() { load_env_file "$CONFIG_DIR/grab.env"; } |
XDG path fix |
Acceptance criterion 1 (D-D): grep -rn 'while IFS.*read.*k.*v' bin/pos-communication-* bin/pos-ai* bin/pos-media-grab → zero matches (only lib/config-ui.sh:346 — the canonical loader — and lib/common.sh:149 load_system_env + lib/entertainment-plugin-lib.sh:25 remain anywhere in bin/+).
[DONE]
Step 3: pos-network-download load_secret + inline duplicate
- Added config-ui.sh source.
load_secret()nowload_env_file "$CONFIG_FILE"+RPC_SECRET="${RPC_SECRET:-}"normalization (kept) — env-wins identical to the old guard; CR/quote-strips are no-ops for the machine-written unquoted hex file.- The duplicate inline
grep RPC_SECRETincmd_startreplaced with aload_secretcall (guarded on[ -z "$RPC_SECRET" ]like before).
[DONE]
Step 4: pos-docker-compose — NOT migrated (deliberate)
load_global_config still source "$CONFIG_ENV" (bin/pos-docker-compose:90). Converting to load_env_file would change behavior: the compose.env file is shell-EXECUTED (variable expansion, file-beats-defaults layering, and config set writes unquoted TS_AUTHKEY=… values that the loader's quote/CR handling + env-wins would invert). Documented as the deliberate exception in DOC/DEV.md (new canonical-loader paragraph). Also noted but NOT changed (out of D-D loader scope): pos-docker-compose:10 still hardcodes $HOME/.config/... for CONFIG_ENV.
[DONE]
Step 5: lib/entertainment-lib.sh collapse
config_value/write_config_keyare now thin wrappers overcfg_value/cfg_write(lib/entertainment-lib.sh:33-43) — identical call signatures, chmod-600 preserved,"-"delete supported.- entertainment-lib now sources
config-ui.shvia a 4-way fallback chain mirroring the user-timers source block. - Documented minor delta:
cfg_writeemits a stderr-only warning on multi-line paste and truncates to the first line (oldwrite_config_keytruncated silently); write RESULT identical. config_valuenow also strips a trailing CR (old entertainment version did not) — alignment with the other migrated tools.- Pre-existing contract documented in the lib header: must be sourced AFTER
lib/common.sh(defines CONFIG_DIR); verified still true. - Verified callers:
bin/pos-entertainment-config(1-argconfig_value, 2-argwrite_config_key,"-"delete at line 77) andbin/pos-entertainment-statusall match the new signatures.
[DONE]
Step 6: Docs
DOC/DEV.md§"Config files (if needed)": added the canonical-loader bullet (precedence contract, CR/quote handling, LOADED_ENV_KEYS, XDG basename resolution, fallback source pattern, load_system_env supersession, docker-compose exception).DOC/AGENT_Context_Project.md: hand-maintainedlib/entertainment-lib.shline-count row updated (311 → 300);make genregenerated the filetable counts for the migrated tools (byte-order deterministic: second run produced zero diff) andcompletions/pos.bash.
[DONE]
Step 7: Gates
bash -non all 12 touched code files: OK.make gen: run 1 vs run 2 diff — zero bytes (idempotent).make check(scripts/check-sync.sh): OK.make lint(scripts/lint-conventions.sh):0 FAIL, 0 WARN.
[DONE]
Step 8: Behavioral probes (all PASS)
Stub harness in /tmp/opencode/cfg-probe/ (XDG_CONFIG_HOME + stub curl/llama-server/systemctl/aria2c; throwaway per DEV.md):
- telegram-sender wiring — telegram.env with
# comment, blank line,TELEGRAM_BOT_TOKEN=file-token\r\n,TELEGRAM_CHAT_ID=1289; exportedTELEGRAM_CHAT_ID=env-chat. Stub curl logged:botfile-tokenin URL (CR stripped, comment skipped) andchat_id=env-chat(env wins over file). Zero CR anywhere. - pos-ai wiring — ai.env with
AI_PROVIDER=gemini+AI_GEMINI_API_KEY=file-key\r\n;pos ai ask hellowith stub curl →stub-replyprinted; headerx-goog-api-key: file-key(CR stripped from file). WithAI_GEMINI_API_KEY=env-keyexported → headerenv-key(env wins). - ai-server D-F survival — ai.env
LLAMACPP_CTX_SIZE=8192\r\n; stubllama-serverwhose--helplacks--ctx-size;pos-ai-server start whatever.gguf→ERROR: installed llama.cpp 1.2.3 does not expose --ctx-size — remove it or upgrade llama.cpp, exit 1. D-F "hard error on config/env-requested but unsupported flag" contract intact after the loader migration. - network-download load_secret — download.env
RPC_SECRET=file-secret\r\n; daemon faked active via stub systemctl;status→ RPC payloadtoken:file-secret(CR stripped, from file); withRPC_SECRET=env-secretexported →token:env-secret(env wins). - docker-compose — spot-confirmed
source "$CONFIG_ENV"unchanged (deliberate). - entertainment-lib wrappers — direct smoke:
config_valueread (CR strip), quote-stripped read,write_config_keyoverwrite,"-"delete, chmod-600 preserved; pluspos entertainment statusend-to-end (ENABLEDweather,1hparsed, "every 1h" interval). - residual-loader rate — only canonical
load_env_file+ keptload_system_env+ plugin-onlyentertainment-plugin-libremain.
[DONE]
Step 9: Discoveries (out of scope, flagged — not fixed)
bin/pos-network-downloadNET_PROBE defaulttimeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ 8.8.8.8 53(introduced by a PARALLEL uncommitted change; HEAD has'</dev/tcp/8.8.8.8/53>'): underset -u, the single quotes inside${NET_PROBE:-…}are literal, so the OUTER shell expands$1/$2(unset) →line 31: $2: unbound variableon every run without the env var set. Real blocker for the tool's runtime; owner = the parallel track (likely the retry/healer work). D-D probes bypassed it by exporting a non-empty NET_PROBE — do not fix here (unapproved component boundary, unclear ownership).
[DONE]
Scope compliance
In-scope: loader addition + 9-tool migration + network-download load_secret + entertainment-lib collapse + config-ui.sh supersession note + DOC/DEV.md canonical-loader paragraph + hand-maintained line-count row + report. Out-of-scope changes: none (NET_PROBE bug intentionally NOT fixed; docker-compose NOT migrated; load_system_env NOT rewritten).
Remaining risks / follow-ups:
- NET_PROBE
set -ucrash (see Step 9) — needs the owning parallel track / Architect decision. pos-docker-composeCONFIG_ENVstill hardcodes$HOME/.config/…(pre-existing; XDG seam gap for that tool; explorer-flagged, not in D-D's 9).lib/entertainment-plugin-lib.sh:25keeps its own read loop (plugin contract: plugins must not source config libs) — an intentional residual, could get a documented rationale later.load_system_envremains duplicated logic inlib/common.sh:146-159— supersession documented, legacy callers (pos system health/backup,pos media sync) untouched.
Recommended next agent: Reviewer (independent adversarial review of the loader migration + wrapper collapse) — or Orchestrator if the NET_PROBE ownership question should be routed to the owning track first. Not handed to Architect: no scope/design boundary dispute remains after the decisions above were recorded.