fix: pos ai — honor legacy AI_API_KEY as fallback (provider key stays primary)
gates / consistency-and-conventions (push) Successful in 27s
gates / consistency-and-conventions (push) Successful in 27s
Architect decision C on the API-key contract mismatch: docs claimed
AI_API_KEY was the required primary key, but resolve_key() only read
provider-specific keys (7ae2e77 removed shared-key priority to fix
cross-provider leakage; docs never updated).
- bin/pos-ai resolve_key(): provider key wins, legacy AI_API_KEY honored
read-only when the provider's own key is empty, llamacpp unchanged;
cmd_providers() configured check mirrors the same set
- require_key() error messages byte-stable (test-locked)
- AI_API_KEY NOT re-added to the # POS_CONFIG:/# PROVIDER_CONFIG: registry
- Docs reworded: POS.md rows 91/96/98 + precedence sentence, howto/ai.md
first-run hints, HOWTO.md row, AGENT_Context prose (2 spots), config/ai.env
legacy comment
- New regression tests/t-ai-key-resolution.sh: 24 checks / 10 cases
(provider-key-only, AI_API_KEY-only, both -> provider wins, env-wins,
llamacpp no-key, missing-key message, providers configured status)
Verified: suite 19 files / 440 checks / 0 fail / 0 skip; make gen
byte-idempotent; make check OK; make lint 0 FAIL, 0 WARN; bash -n clean;
git diff --check clean; Reviewer APPROVE_WITH_NOTES (mutation disproof:
inverted precedence -> C3/C6 fail)
This commit is contained in:
@@ -42,6 +42,8 @@ summary (newest last).
|
||||
|
||||
## Done
|
||||
|
||||
- **2026-09-07** — `pos ai` API-key contract mismatch fixed (Architect→Builder→Reviewer; docs/history evidence): docs claimed `AI_API_KEY` was the required primary key, but `resolve_key()` read only provider-specific keys (`7ae2e77` had removed shared-key priority to fix cross-provider leakage; docs never updated). Decision C: provider key stays primary (leakage guard intact), legacy `AI_API_KEY` honored as backward-compat fallback when the provider's own key is empty; `cmd_providers()` "configured" mirrors it; `require_key()` messages byte-stable; `AI_API_KEY` NOT re-added to `# POS_CONFIG:`/`# PROVIDER_CONFIG:` registry. Docs reworded (POS.md rows 91/96/98 + precedence, howto/ai.md first-run hints, HOWTO.md, AGENT_Context 2 prose spots, config/ai.env comment). New regression `tests/t-ai-key-resolution.sh` (24 checks / 10 cases: gemini+openrouter via provider key only, via AI_API_KEY only, both→provider wins, env-wins, llamacpp no-key, missing-key message, providers configured status). Verified: suite 19 files / 440 checks / 0 fail / 0 skip; `make gen` byte-idempotent; `make check` OK; `make lint` 0 FAIL, 0 WARN; Reviewer APPROVE_WITH_NOTES with mutation-based disproof (inverted precedence → C3/C6 fail).
|
||||
|
||||
- **2026-09-07** — opencode project skill: created `.opencode/skills/linux-post-install/SKILL.md` (repo had no `.opencode/`). Skill encodes the repo's operational playbook for agents: repo shape, `pos` tool model (`# POS:` header system, exec-bit, deps-guard-before-help, INTERACTIVE_CMDS, determinism), doc authority order (MAINTENANCE Phase 0: templates → DEV.md → AGENTS.md → code), Definition of Done gates (`make gen` ×2 idempotent → `make check` → `make lint` 0/0 → `make test` 18 files/416 checks), test conventions (hard-skip contract, negative controls), and repo commands (ci-status, gitea API, pos tree/config). Frontmatter validated (name matches folder, description with trigger keywords); auto-discovered at `.opencode/skills/` — no opencode.json change needed; restart opencode to load.
|
||||
|
||||
- **2026-09-06** — Repo cleanup: removed 114 temp/process files (AgentsReport/ 86 + reportAgents/ 28 agent reports), stale task/plan/audit docs (tmp_request.md, FINAL_SUMMARY.md, IMPLEMENTATION_PLAN.md, AUDIT.md, AUDIT_TABLE.md, registry-design doc), stray `To`/`.n`/`reports/` artifacts; gitignored AgentsReport/ so agent process reports stay local-only. DOC/, tools-docs/ytsync.md, AGENT_TODO.md, bin/lib/apps/tests/config/scripts etc. kept untouched.
|
||||
|
||||
@@ -488,7 +488,7 @@ All `.service` files in `systemd/` are automatically copied to `/etc/systemd/sys
|
||||
- `~/.config/linux_post_install/entertainment.env` — entertainment plugin defaults: weather location + `ENABLED` auto-trigger list (`plugin, interval` pairs scheduled via `pos entertainment enable/disable`, systemd user timers); auto-installed from `config/entertainment.env` by `postinstall.sh` (no clobber, template printed)
|
||||
- `~/.config/linux_post_install/system.env` — shared "system" tool settings (loaded by `pos system health` / `pos system backup` via `load_system_env()` in `lib/common.sh`; env already exported wins over the file); template `config/system.env`
|
||||
- `~/.config/linux_post_install/notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`, comma-separated = fan out); read by `lib/notify.sh`; template `config/notify.env`
|
||||
- `~/.config/linux_post_install/ai.env` — AI provider config (`AI_PROVIDER`, `AI_API_KEY` secret, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_MAX_TOKENS`, `AI_SESSION_TURNS`, plus legacy fallbacks `AI_GEMINI_API_KEY`, `AI_GEMINI_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`); read by `pos ai`; template `config/ai.env`, auto-installed by postinstall, edit with `pos config ai`
|
||||
- `~/.config/linux_post_install/ai.env` — AI provider config (`AI_PROVIDER`, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_MAX_TOKENS`, `AI_SESSION_TURNS`, provider keys `AI_GEMINI_API_KEY`/`OPENROUTER_API_KEY` (secrets), legacy shared fallback `AI_API_KEY` accepted when the provider's key is unset, plus model fallbacks `AI_GEMINI_MODEL`, `OPENROUTER_MODEL`); read by `pos ai`; template `config/ai.env`, auto-installed by postinstall, edit with `pos config ai`
|
||||
- `~/.bashrc` — Modified by postinstall (PATH, bash completion)
|
||||
|
||||
### Feature Flags
|
||||
@@ -655,7 +655,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
|
||||
| `bin/pos-system-schedule` | 151 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
|
||||
| `bin/pos-system-uninstall` | 517 | Remove pos toolkit binaries, services, shell integration, config, and data |
|
||||
| `bin/pos-ai` | 709 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| `bin/pos-ai` | 714 | AI assistant: ask, chat, sessions, capture, models, providers |
|
||||
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
|
||||
| `bin/pos-tree` | 118 | Show the pos CLI command tree: categories, commands, and subcommands |
|
||||
| `completions/pos.bash` | 314 | Dynamic bash completion |
|
||||
@@ -692,7 +692,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
| Modify Music→USB sync logic | Edit `bin/pos-media-sync` / shared USB layer `lib/usb-lib.sh` (seams `MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST`/`USB_MOUNT_BASE`/`USB_BYID` in `~/.config/linux_post_install/system.env`) |
|
||||
| Modify YouTube channel sync logic | Edit `bin/pos-media-ytsync` (state in `~/.local/share/linux_post_install/ytsync`; config scope `ytsync` via `pos config ytsync`; research notes `tools-docs/ytsync.md`) |
|
||||
| Modify the scheduler / scheduled jobs | Edit `bin/pos-system-schedule` / `lib/scheduler-lib.sh` (jobs in `~/.config/linux_post_install/schedule.d/`) |
|
||||
| Modify AI logic | Edit `bin/pos-ai` (main tool) + `lib/ai-providers/*.sh` (provider adapters); config scope `ai` via `pos config ai`; `AI_API_KEY`/`AI_MODEL`/`AI_PROVIDER` in `~/.config/linux_post_install/ai.env` |
|
||||
| Modify AI logic | Edit `bin/pos-ai` (main tool) + `lib/ai-providers/*.sh` (provider adapters); config scope `ai` via `pos config ai`; `AI_PROVIDER`, `AI_MODEL`, per-provider `AI_GEMINI_API_KEY`/`OPENROUTER_API_KEY` (legacy shared `AI_API_KEY` accepted as fallback) in `~/.config/linux_post_install/ai.env` |
|
||||
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
|
||||
| Modify pos logging | Edit log setup in `bin/pos` |
|
||||
| Modify install phases/flags | Edit arg parsing in `install.sh` |
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ templates (without overwriting an existing file):
|
||||
| `system.env` | `pos system health`, `pos system backup` | `BACKUP_SERVICE_ROOTS`, `HEALTH_BACKUP_MAX_AGE_DAYS` |
|
||||
| `compose.env` | `pos docker compose` | `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` |
|
||||
| `entertainment.env` | `pos entertainment *` | plugin keys (`WEATHER_LAT`…), `ENABLED` |
|
||||
| `ai.env` | `pos ai` | `AI_PROVIDER`, `AI_API_KEY`, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_MAX_TOKENS`, `AI_SESSION_TURNS`, `AI_GEMINI_API_KEY`, `AI_GEMINI_MODEL`, `OPENROUTER_API_KEY`, `OPENROUTER_MODEL` |
|
||||
| `ai.env` | `pos ai` | `AI_PROVIDER`, `AI_MODEL`, `AI_SYSTEM_PROMPT`, `AI_MAX_TOKENS`, `AI_SESSION_TURNS`, provider keys `AI_GEMINI_API_KEY` / `OPENROUTER_API_KEY` (+ legacy `AI_API_KEY` fallback) |
|
||||
| `schedule.d/` | `pos system schedule` | one `<name>.env` per job: `INTERVAL`, `NOTIFY`, `MSG`, `RULE`, `COMMAND` |
|
||||
|
||||
```bash
|
||||
|
||||
+4
-4
@@ -88,17 +88,17 @@ Backward compatibility: `pos ai gemini`, `pos ai openrouter`, and `pos ai llamac
|
||||
| Key | Required | Default | Purpose |
|
||||
|-----|----------|---------|---------|
|
||||
| `AI_PROVIDER` | no | `gemini` | Active provider (gemini\|openrouter\|llamacpp) |
|
||||
| `AI_API_KEY` | yes | — | API key for the active provider (secret — masked in `pos config ai`) |
|
||||
| `AI_API_KEY` | no (legacy fallback) | — | Legacy shared API key, used when the active provider's key is empty; not part of the `pos config ai` prompt (set via env or hand-edit); secret |
|
||||
| `AI_MODEL` | no | per provider | Model id used by `ask`/`chat`/`models` |
|
||||
| `AI_SYSTEM_PROMPT` | no | built-in terse prompt | Custom system prompt (overrides built-in; empty to reset) |
|
||||
| `AI_MAX_TOKENS` | no | `2048` | Max output tokens per request (OpenRouter/Gemini cost cap) |
|
||||
| `AI_SESSION_TURNS` | no | `40` | Session message cap — 2 per exchange; 10 = last 5 exchanges |
|
||||
| `AI_GEMINI_API_KEY` | fallback | — | Legacy: Gemini API key (used when `AI_API_KEY` is empty) |
|
||||
| `AI_GEMINI_API_KEY` | yes (gemini) | — | Gemini API key (the active key when provider is gemini; secret — masked in `pos config ai`) |
|
||||
| `AI_GEMINI_MODEL` | fallback | `gemini-2.5-flash` | Legacy: Gemini model id (used when `AI_MODEL` is empty) |
|
||||
| `OPENROUTER_API_KEY` | fallback | — | Legacy: OpenRouter API key (used when `AI_API_KEY` is empty) |
|
||||
| `OPENROUTER_API_KEY` | yes (openrouter) | — | OpenRouter API key (the active key when provider is openrouter; secret — masked in `pos config ai`) |
|
||||
| `OPENROUTER_MODEL` | fallback | `openrouter/auto` | Legacy: OpenRouter model id (used when `AI_MODEL` is empty) |
|
||||
|
||||
Model precedence: `--model` flag > `AI_MODEL` env > provider-specific fallback (`AI_GEMINI_MODEL`/`OPENROUTER_MODEL`) > provider default. API key precedence: `AI_API_KEY` env > provider-specific fallback (`AI_GEMINI_API_KEY`/`OPENROUTER_API_KEY`) > error. `postinstall.sh` copies the repo's `config/ai.env` template to `~/.config/linux_post_install/ai.env` on install (no clobber). Dependencies: `curl` + `jq` (both in `preinstall.sh` PACKAGES). Sessions are stored in OpenAI `messages` format universally; old Gemini-format sessions (`contents[]`) are auto-migrated on load.
|
||||
Model precedence: `--model` flag > `AI_MODEL` env > provider-specific fallback (`AI_GEMINI_MODEL`/`OPENROUTER_MODEL`) > provider default. API key precedence: `<provider>_API_KEY` (`AI_GEMINI_API_KEY` for gemini / `OPENROUTER_API_KEY` for openrouter) > legacy `AI_API_KEY` fallback > error. `AI_API_KEY` is an internal adapter shim and a backward-compat input — it is not offered by `pos config ai`. `postinstall.sh` copies the repo's `config/ai.env` template to `~/.config/linux_post_install/ai.env` on install (no clobber). Dependencies: `curl` + `jq` (both in `preinstall.sh` PACKAGES). Sessions are stored in OpenAI `messages` format universally; old Gemini-format sessions (`contents[]`) are auto-migrated on load.
|
||||
|
||||
**Messaging bridges:** the Telegram and Matrix listeners forward non-command messages starting with `ai ` (case-insensitive) to `pos ai ask` and reply with the model's answer — see [communication → listener](#communication). The Telegram bridge uses one session per chat (`telegram-<chat id>`), the Matrix bridge one per room (`matrix-<room>`).
|
||||
|
||||
|
||||
+6
-3
@@ -69,7 +69,7 @@ exactly as before (no added blank lines), so scripting stays byte-stable.
|
||||
2. Configure it (masked input):
|
||||
|
||||
```bash
|
||||
pos config ai # enter AI_API_KEY (or AI_GEMINI_API_KEY)
|
||||
pos config ai # enter AI_GEMINI_API_KEY
|
||||
```
|
||||
|
||||
3. Test:
|
||||
@@ -81,7 +81,10 @@ exactly as before (no added blank lines), so scripting stays byte-stable.
|
||||
```
|
||||
|
||||
`ai.env` lives at `~/.config/linux_post_install/ai.env` (chmod 600); `pos config ai`
|
||||
is the only place the key is written. The key is never printed by `pos`.
|
||||
is where the provider keys are written, and keys are never printed by `pos`. The
|
||||
legacy shared `AI_API_KEY` is still honored as a fallback when the provider's own
|
||||
key is unset — set it via the shell environment or by hand-editing `ai.env` (it is
|
||||
not part of the `pos config ai` prompt).
|
||||
|
||||
## OpenRouter — many providers, one key
|
||||
|
||||
@@ -98,7 +101,7 @@ pos ai openrouter ask "hi"
|
||||
Configure the API key:
|
||||
|
||||
```bash
|
||||
pos config ai # enter AI_API_KEY (or OPENROUTER_API_KEY)
|
||||
pos config ai # enter OPENROUTER_API_KEY
|
||||
```
|
||||
|
||||
The default model is `openrouter/auto` (OpenRouter picks the best available
|
||||
|
||||
+10
-5
@@ -156,10 +156,14 @@ load_config() {
|
||||
resolve_key() {
|
||||
load_config
|
||||
local p="${PROVIDER:-gemini}"
|
||||
# Each provider has its own API key — set AI_API_KEY internally for adapters
|
||||
# Provider-specific key wins (leakage guard); legacy shared AI_API_KEY is a
|
||||
# read-only fallback when the provider's own key is empty — load_config/env
|
||||
# already exported it (env-wins), so never overwrite it here.
|
||||
case "$p" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && export AI_API_KEY="$AI_GEMINI_API_KEY" && return 0 ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && export AI_API_KEY="$OPENROUTER_API_KEY" && return 0 ;;
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && export AI_API_KEY="$AI_GEMINI_API_KEY" && return 0
|
||||
[ -n "${AI_API_KEY:-}" ] && return 0 ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && export AI_API_KEY="$OPENROUTER_API_KEY" && return 0
|
||||
[ -n "${AI_API_KEY:-}" ] && return 0 ;;
|
||||
llamacpp) return 0 ;; # No API key needed for local server
|
||||
esac
|
||||
return 1
|
||||
@@ -626,9 +630,10 @@ cmd_providers() {
|
||||
[ -n "$pmodel" ] || pmodel="unknown"
|
||||
# Check if API key exists for this provider
|
||||
configured="not configured"
|
||||
# Same key set as resolve_key(): provider key, or legacy AI_API_KEY fallback
|
||||
case "$name" in
|
||||
gemini) [ -n "${AI_GEMINI_API_KEY:-}" ] && configured="configured" ;;
|
||||
openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && configured="configured" ;;
|
||||
gemini) { [ -n "${AI_GEMINI_API_KEY:-}" ] || [ -n "${AI_API_KEY:-}" ]; } && configured="configured" ;;
|
||||
openrouter) { [ -n "${OPENROUTER_API_KEY:-}" ] || [ -n "${AI_API_KEY:-}" ]; } && configured="configured" ;;
|
||||
llamacpp) configured="configured" ;; # Local server — always configured
|
||||
esac
|
||||
current=""
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# API keys (each provider needs its own):
|
||||
# AI_GEMINI_API_KEY=<key> # Gemini API key from aistudio.google.com
|
||||
# OPENROUTER_API_KEY=<key> # OpenRouter API key from openrouter.ai
|
||||
# Legacy fallback: AI_API_KEY=<key> is honored when the provider's own key is unset
|
||||
#
|
||||
# Model overrides (optional, defaults per provider):
|
||||
# AI_GEMINI_MODEL=<model> # Gemini model id (default: gemini-2.5-flash)
|
||||
|
||||
@@ -42,6 +42,7 @@ silently.
|
||||
| --- | --- |
|
||||
| `t-ai-server-flags.sh` | `pos-ai-server` ExecStart flag set: defaults, CLI, config, dedupe, one-token-per-flag |
|
||||
| `t-ai-hf-download.sh` | `pos ai hf download` success + failure honesty (no `.hf-meta` on partial failure, rc != 0) |
|
||||
| `t-ai-key-resolution.sh` | `pos ai` API-key contract: provider key > legacy `AI_API_KEY` fallback > error; leakage guard, env-wins, providers status sync |
|
||||
| `t-ai-llama-detect.sh` | `pos ai-server status` version detection, "unknown", missing-binary failure |
|
||||
| `t-unsupported-flags.sh` | unsupported-option handling: CLI/config/env hard errors, dropped defaults, word-boundary match |
|
||||
| `t-systemd-unit.sh` | generated unit: one ExecStart, quoted paths, `systemd-analyze verify` |
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# t-ai-key-resolution.sh — `pos ai` API-key resolution contract:
|
||||
# provider-specific key (AI_GEMINI_API_KEY / OPENROUTER_API_KEY)
|
||||
# > legacy shared AI_API_KEY fallback > error; llamacpp needs no key.
|
||||
#
|
||||
# Regression for the 7ae2e77 "per-provider keys" migration: AI_API_KEY must be
|
||||
# honored ONLY when the active provider's own key is empty (backward-compat for
|
||||
# howto-taught single-key configs) and must NEVER override a provider key
|
||||
# (cross-provider leakage guard). Also pins env-wins (exported env beats
|
||||
# ai.env) and `pos ai providers` agreeing with resolve_key().
|
||||
#
|
||||
# Seam: `pos ai models` with a curl stub on PATH that records -H headers to a
|
||||
# log and returns deterministic per-provider JSON (no sessions are written by
|
||||
# `models`, unlike ask/chat).
|
||||
|
||||
run_test() {
|
||||
require_cmd jq "pos ai key resolution" || return 0
|
||||
|
||||
local sandbox cfg home stubs curl_log
|
||||
sandbox="$(mksandbox ai-key-resolution)"
|
||||
cfg="$sandbox/cfg"
|
||||
home="$sandbox/home"
|
||||
stubs="$sandbox/stubs"
|
||||
mkdir -p "$cfg" "$home" "$stubs"
|
||||
curl_log="$sandbox/curl.log"
|
||||
: > "$curl_log"
|
||||
: > "$cfg/ai.env"
|
||||
|
||||
# curl stub: log every -H <header> pair; reply with provider-shaped JSON +
|
||||
# the trailing \n%{http_code} line the adapters' --write-out expects.
|
||||
cat > "$stubs/curl" <<STUB
|
||||
#!/usr/bin/env bash
|
||||
all="\$*"
|
||||
while [ \$# -gt 0 ]; do
|
||||
case "\$1" in
|
||||
-H) printf 'HDR %s\n' "\$2" >> "$curl_log"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
case "\$all" in
|
||||
*generativelanguage.googleapis.com*)
|
||||
printf '%s\n200' '{"models":[{"name":"models/gemini-2.5-flash","supportedGenerationMethods":["generateContent"]}]}' ;;
|
||||
*openrouter.ai*)
|
||||
printf '%s\n200' '{"data":[{"id":"openrouter/auto"}]}' ;;
|
||||
*)
|
||||
printf '%s\n200' '{"data":[{"id":"loaded-model"}]}' ;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$stubs/curl"
|
||||
|
||||
local posai="$ROOT/bin/pos-ai"
|
||||
# Hermetic env: `env -i` drops every inherited variable (incl. any ambient
|
||||
# AI_*), so each case exercises exactly the values declared below.
|
||||
local path_env=(-i PATH="$stubs:/usr/bin:/bin" CONFIG_FILE="$cfg/ai.env" HOME="$home")
|
||||
|
||||
# ── Case 1: gemini via provider key only ──
|
||||
printf 'AI_GEMINI_API_KEY=GEMFILEKEY\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" models
|
||||
check_rc "C1 gemini via AI_GEMINI_API_KEY works" 0 "$TR_RC"
|
||||
check_contains "C1 gemini header carries provider key" "x-goog-api-key: GEMFILEKEY" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 2: gemini via legacy AI_API_KEY only (core regression) ──
|
||||
: > "$curl_log"
|
||||
printf 'AI_API_KEY=AIKEYONLY\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" models
|
||||
check_rc "C2 gemini via legacy AI_API_KEY works" 0 "$TR_RC"
|
||||
check_contains "C2 gemini header carries legacy key" "x-goog-api-key: AIKEYONLY" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 3: gemini with both → provider key wins (leakage guard) ──
|
||||
: > "$curl_log"
|
||||
printf 'AI_GEMINI_API_KEY=GEMWIN\nAI_API_KEY=AIKEYLOSE\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" models
|
||||
check_rc "C3 both set still works" 0 "$TR_RC"
|
||||
check_contains "C3 provider key wins" "x-goog-api-key: GEMWIN" "$(cat "$curl_log")"
|
||||
check_not_contains "C3 shared key not leaked" "AIKEYLOSE" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 4: openrouter via provider key only ──
|
||||
: > "$curl_log"
|
||||
printf 'OPENROUTER_API_KEY=ORFILEKEY\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" --provider openrouter models
|
||||
check_rc "C4 openrouter via OPENROUTER_API_KEY works" 0 "$TR_RC"
|
||||
check_contains "C4 openrouter auth carries provider key" "Authorization: Bearer ORFILEKEY" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 5: openrouter via legacy AI_API_KEY only ──
|
||||
: > "$curl_log"
|
||||
printf 'AI_API_KEY=AIKEYONLY\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" --provider openrouter models
|
||||
check_rc "C5 openrouter via legacy AI_API_KEY works" 0 "$TR_RC"
|
||||
check_contains "C5 openrouter auth carries legacy key" "Authorization: Bearer AIKEYONLY" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 6: openrouter with both → provider key wins ──
|
||||
: > "$curl_log"
|
||||
printf 'OPENROUTER_API_KEY=ORWIN\nAI_API_KEY=AIKEYLOSE\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" --provider openrouter models
|
||||
check_rc "C6 both set still works" 0 "$TR_RC"
|
||||
check_contains "C6 openrouter provider key wins" "Authorization: Bearer ORWIN" "$(cat "$curl_log")"
|
||||
check_not_contains "C6 shared key not leaked" "AIKEYLOSE" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 7: env-wins — exported provider key beats ai.env file value ──
|
||||
: > "$curl_log"
|
||||
printf 'AI_GEMINI_API_KEY=FILEVALUE\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" AI_GEMINI_API_KEY=ENVVALUE -- "$posai" models
|
||||
check_rc "C7 env-wins run works" 0 "$TR_RC"
|
||||
check_contains "C7 exported env key used" "x-goog-api-key: ENVVALUE" "$(cat "$curl_log")"
|
||||
check_not_contains "C7 file value suppressed by env" "FILEVALUE" "$(cat "$curl_log")"
|
||||
|
||||
# ── Case 8: llamacpp needs no key ──
|
||||
: > "$curl_log"
|
||||
: > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" --provider llamacpp models
|
||||
check_rc "C8 llamacpp works with no key" 0 "$TR_RC"
|
||||
check_contains "C8 llamacpp lists models" "Local llama.cpp models:" "$TR_OUT"
|
||||
|
||||
# ── Case 9: missing key → rc 1 + byte-stable message ──
|
||||
test_run_env "${path_env[@]}" -- "$posai" models
|
||||
check_rc "C9 missing key exits nonzero" 1 "$TR_RC"
|
||||
check_contains "C9 stable error message" "No Gemini API key — run 'pos config ai' and set AI_GEMINI_API_KEY" "$TR_OUT"
|
||||
|
||||
# ── Case 10: providers status agrees with resolve_key (AI_API_KEY only) ──
|
||||
local prov
|
||||
printf 'AI_API_KEY=SHAREDKEY\n' > "$cfg/ai.env"
|
||||
test_run_env "${path_env[@]}" -- "$posai" providers
|
||||
prov="$(printf '%s\n' "$TR_OUT" | grep configured || true)"
|
||||
check_contains "C10 gemini row shows configured" "gemini" "$prov"
|
||||
check_contains "C10 openrouter row shows configured" "openrouter" "$prov"
|
||||
check_not_contains "C10 no 'not configured' row" "not configured" "$TR_OUT"
|
||||
}
|
||||
Reference in New Issue
Block a user