24 KiB
Keychain TOTP Provider Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a free, non-Bitwarden credential path where the user stores LDAP password plus a permanent TOTP seed in macOS Keychain, and openconnect-lite keeps generating one-time login codes from that seed.
Architecture: Introduce an explicit credential-source layer with two first-class sources: bitwarden and keychain. bitwarden keeps the existing vault-sync behavior and writes LDAP password plus TOTP seed into the existing openconnect-lite Keychain entries; keychain skips vault sync and treats those Keychain entries as the source of truth. Both sources converge before the current openconnect-lite SSO/autofill flow, so runtime patches and Keycloak selectors stay unchanged.
Tech Stack: Bash (bin/vpn-lemanapro.sh), POSIX shell installer (install.sh), macOS Keychain via Python keyring inside the openconnect-lite venv, Swift menu-bar app JSON decoding, tests/smoke.sh.
File Structure
- Modify
bin/vpn-lemanapro.sh- Own runtime credential-source selection.
- Normalize raw Base32 and
otpauth://TOTP inputs. - Split source orchestration from source-specific sync.
- Keep the existing Keychain service/accounts:
openconnect-lite/<username>andopenconnect-lite/totp/<username>.
- Modify
install.sh- Add
LEMANA_VPN_CREDENTIAL_SOURCE. - Keep
--with-bitwardenand--without-bitwardenas backward-compatible aliases. - Add a direct
--credential-source bitwarden|keychainflag. - Disable Touch ID automatically for the
keychainsource because Touch ID currently gates Bitwarden master-password retrieval only.
- Add
- Modify
tests/smoke.sh- Assert installer and runtime source separation.
- Assert
otpauth://input is normalized before storing. - Assert no-tty app path still fails with the terminal recovery command when Keychain credentials are missing.
- Modify
app/Sources/LemanaVPN/VPNManager.swift- Decode the new
credentialsmodule object. - Keep compatibility with older CLI JSON by making the new object optional.
- Log source-specific credential events without changing connection state semantics.
- Decode the new
- Modify
README.md- Document the two credential sources and the free Keychain/TOTP-seed setup.
- Keep update/run/check instructions aligned with
AGENTS.md.
Task 1: Add Smoke Tests For Credential Source Separation
Files:
-
Modify:
tests/smoke.sh -
Step 1: Write the failing tests
Edit tests/smoke.sh so the initial dry-run expectations and missing-credential check cover the new keychain source. Replace:
printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
with:
printf '%s\n' "$output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
After the existing status JSON assertions:
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$status_json" | grep -q '"modules":'
printf '%s\n' "$status_json" | grep -q '"app":'
insert:
keychain_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=keychain bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","keychain_ready":false}'
printf '%s\n' "$keychain_status_json" | grep -q '"bitwarden":{"enabled":false'
bitwarden_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$bitwarden_status_json" | grep -q '"credentials":{"source":"bitwarden","keychain_ready":false}'
printf '%s\n' "$bitwarden_status_json" | grep -q '"bitwarden":{"enabled":true'
Replace the missing-credentials environment:
LEMANA_VPN_USE_BITWARDEN=0 \
with:
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
After the missing-credentials block, add an otpauth:// normalization check:
fake_oc_python="$TMP_DIR/fake-oc-python"
captured_totp="$TMP_DIR/captured-totp"
cat > "$fake_oc_python" <<'SH'
#!/bin/sh
cat >/dev/null
printf '%s\n' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
SH
chmod +x "$fake_oc_python"
configure_output="$(
printf 'ldap-password\notpauth://totp/Lemana:test?secret=abcd2345efgh6723&issuer=Lemana\n' |
HOME="$HOME" \
LEMANA_VPN_USERNAME="lemana-configure-$$" \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
LEMANA_VPN_OC_PYTHON="$fake_oc_python" \
LEMANA_VPN_CAPTURE_TOTP="$captured_totp" \
bash "$ROOT/bin/vpn-lemanapro.sh" --configure-keychain
)"
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
- Step 2: Run the test to verify it fails
Run:
tests/smoke.sh
Expected: FAIL because current installer output does not print credential_source=keychain, current status JSON has no "credentials" object, and --configure-keychain stores the raw otpauth:// URI instead of the extracted Base32 secret.
- Step 3: Commit the failing tests
git add tests/smoke.sh
git commit -m "test: cover keychain credential source"
Task 2: Add Runtime Credential Source And TOTP Normalization
Files:
-
Modify:
bin/vpn-lemanapro.sh -
Test:
tests/smoke.sh -
Step 1: Capture and restore the new environment variable
In bin/vpn-lemanapro.sh, add this line after _ENV_LEMANA_VPN_USE_BITWARDEN=...:
_ENV_LEMANA_VPN_CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE+x}${LEMANA_VPN_CREDENTIAL_SOURCE-}"
Add this line after the existing LEMANA_VPN_USE_BITWARDEN restore line:
[[ "${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:0:1}" == "x" ]] && LEMANA_VPN_CREDENTIAL_SOURCE="${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:1}"
- Step 2: Replace implicit Bitwarden mode with explicit credential source
Replace:
USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN:-1}"
with:
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
if [[ "${LEMANA_VPN_USE_BITWARDEN:-1}" == "1" ]]; then
CREDENTIAL_SOURCE="bitwarden"
else
CREDENTIAL_SOURCE="keychain"
fi
fi
case "$CREDENTIAL_SOURCE" in
bitwarden|keychain) ;;
*)
printf 'Unknown credential source: %s. Use bitwarden or keychain.\n' "$CREDENTIAL_SOURCE" >&2
exit 2
;;
esac
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
USE_BITWARDEN="1"
else
USE_BITWARDEN="0"
fi
- Step 3: Add a TOTP normalizer
Insert this function immediately before _store_keychain():
_normalize_totp_secret() {
_VPN_TOTP_INPUT="$1" python3 - <<'PY'
import os
import re
import sys
import urllib.parse
value = os.environ.get("_VPN_TOTP_INPUT", "").strip()
if value.lower().startswith("otpauth://"):
parsed = urllib.parse.urlparse(value)
query = urllib.parse.parse_qs(parsed.query)
value = query.get("secret", [""])[0]
value = re.sub(r"[\s-]+", "", value).upper()
if not value:
print("", end="")
sys.exit(0)
if not re.fullmatch(r"[A-Z2-7]+=*", value):
print("Invalid TOTP seed. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
sys.exit(1)
print(value)
PY
}
- Step 4: Normalize manual setup before storing
In _configure_keychain(), replace:
_store_keychain "$password" "$totp_secret"
with:
if [[ -n "$totp_secret" ]]; then
totp_secret="$(_normalize_totp_secret "$totp_secret")"
fi
_store_keychain "$password" "$totp_secret"
- Step 5: Normalize Bitwarden TOTP before storing
In _sync_bitwarden(), replace:
_store_keychain "$bw_password" "$bw_totp_secret"
with:
if [[ -n "$bw_totp_secret" ]]; then
bw_totp_secret="$(_normalize_totp_secret "$bw_totp_secret")"
fi
_store_keychain "$bw_password" "$bw_totp_secret"
- Step 6: Update status JSON
In _module_status_json(), add this local variable line after the existing keychain_* locals:
local credential_keychain_ready
After keychain_totp_seed=..., add:
credential_keychain_ready="$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf true || printf false)"
Replace the printf format string:
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
with:
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","keychain_ready":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
Add these arguments immediately after "$oc_config_present" \:
"$CREDENTIAL_SOURCE" \
"$credential_keychain_ready" \
- Step 7: Update human status
In _module_status_human(), change the prefix:
printf 'Modules: %s %s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core"
to:
printf 'Modules: %s %s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$CREDENTIAL_SOURCE"
- Step 8: Run smoke test
Run:
tests/smoke.sh
Expected: still FAIL because source orchestration and installer output are not separated yet, but the otpauth:// normalization assertion now passes.
- Step 9: Commit runtime model
git add bin/vpn-lemanapro.sh tests/smoke.sh
git commit -m "feat: add credential source runtime model"
Task 3: Split Bitwarden Sync From Keychain Source Flow
Files:
-
Modify:
bin/vpn-lemanapro.sh -
Test:
tests/smoke.sh -
Step 1: Replace
_ensure_keychain_credentials()messages
Inside _ensure_keychain_credentials(), replace the ready branch:
if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then
if [[ "$USE_BITWARDEN" == "1" ]]; then
_emit '{"event":"keychain_ready","source":"keychain"}' "LDAP credentials are ready in macOS Keychain for $KC_USERNAME."
else
_emit '{"event":"keychain_ready","source":"keychain","bitwarden":false}' "Bitwarden is disabled. Using saved LDAP password and TOTP seed from macOS Keychain for $KC_USERNAME."
fi
return 0
fi
with:
if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
_emit '{"event":"keychain_ready","source":"bitwarden"}' "Bitwarden source synced LDAP credentials into macOS Keychain for $KC_USERNAME."
else
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and TOTP seed are available for $KC_USERNAME."
fi
return 0
fi
Replace the required branch:
if [[ "$USE_BITWARDEN" == "1" ]]; then
_emit '{"event":"keychain_required","bitwarden":true}' "Bitwarden sync did not produce complete Keychain credentials."
else
_emit '{"event":"keychain_required","bitwarden":false}' "Bitwarden is disabled and saved LDAP credentials are incomplete."
fi
with:
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
_emit '{"event":"keychain_required","source":"bitwarden"}' "Bitwarden source did not produce complete Keychain credentials."
else
_emit '{"event":"keychain_required","source":"keychain"}' "Keychain source is selected and saved LDAP credentials are incomplete."
fi
- Step 2: Add source orchestration
Insert this function after _sync_bitwarden():
_sync_credentials() {
case "$CREDENTIAL_SOURCE" in
bitwarden)
_sync_bitwarden
;;
keychain)
_emit '{"event":"credential_source","source":"keychain"}' "Credential source: macOS Keychain"
;;
esac
}
- Step 3: Use the source orchestrator
Near the bottom of the script, replace:
_sync_bitwarden
_ensure_keychain_credentials
with:
_sync_credentials
_ensure_keychain_credentials
- Step 4: Update
--helptext
Replace the usage line:
Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--configure-keychain] [--patch-only]
with:
Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--configure-keychain] [--patch-only]
Keep the usage line unchanged, and replace the configure text:
--configure-keychain Prompt for LDAP password and TOTP secret, then save them to Keychain
with:
--configure-keychain Configure the keychain credential source: LDAP password plus permanent TOTP seed or otpauth:// URI
- Step 5: Run smoke test
Run:
tests/smoke.sh
Expected: FAIL only on installer output and Swift/README checks that have not been updated yet. Runtime JSON checks for LEMANA_VPN_CREDENTIAL_SOURCE=keychain and bitwarden pass.
- Step 6: Commit provider split
git add bin/vpn-lemanapro.sh tests/smoke.sh
git commit -m "feat: split credential sources"
Task 4: Update Installer Flags And Config
Files:
-
Modify:
install.sh -
Test:
tests/smoke.sh -
Step 1: Add installer state
In install.sh, add this variable after BW_ITEM=...:
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
Add this variable after BITWARDEN_FORCED=0:
CREDENTIAL_SOURCE_FORCED=0
- Step 2: Extend usage
Add this option after --without-bitwarden:
--credential-source VALUE Credential source: bitwarden or keychain
Add this example after sh install.sh --minimal --configure-keychain:
sh install.sh --credential-source keychain --configure-keychain
- Step 3: Parse
--credential-source
Add this case arm before --with-touchid):
--credential-source)
shift
[ "$#" -gt 0 ] || { echo "--credential-source requires bitwarden or keychain" >&2; exit 1; }
CREDENTIAL_SOURCE="$1"
CREDENTIAL_SOURCE_FORCED=1
case "$CREDENTIAL_SOURCE" in
bitwarden)
USE_BITWARDEN=1
;;
keychain)
USE_BITWARDEN=0
USE_TOUCHID=0
;;
*)
echo "--credential-source requires bitwarden or keychain" >&2
exit 1
;;
esac
;;
In the --with-bitwarden) arm, add:
CREDENTIAL_SOURCE="bitwarden"
In the --without-bitwarden) arm, add:
CREDENTIAL_SOURCE="keychain"
USE_TOUCHID=0
In the --minimal) arm, add:
CREDENTIAL_SOURCE="keychain"
- Step 4: Keep module decisions consistent
At the start of choose_modules(), immediately after print_detected_state, add:
case "$CREDENTIAL_SOURCE" in
bitwarden)
USE_BITWARDEN=1
;;
keychain)
USE_BITWARDEN=0
USE_TOUCHID=0
;;
*)
die "Unknown credential source: $CREDENTIAL_SOURCE"
;;
esac
Replace:
if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
with:
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
Replace:
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$USE_BITWARDEN" -eq 0 ]; then
with:
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "keychain" ]; then
- Step 5: Write source to config
In install_config(), change env_content to:
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
LEMANA_VPN_CREDENTIAL_SOURCE=\"$CREDENTIAL_SOURCE\"
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
LEMANA_VPN_DNS_CLEANUP=\"$DNS_CLEANUP\""
- Step 6: Update installer logging
Replace:
log_info "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
with:
log_info "Modules: credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
In maybe_login_bitwarden(), replace:
if [ "$USE_BITWARDEN" -ne 1 ]; then
log_skip "Bitwarden module disabled; credentials будут браться из macOS Keychain."
with:
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
log_skip "Credential source is keychain; пропускаю Bitwarden login."
- Step 7: Run smoke test
Run:
tests/smoke.sh
Expected: PASS for installer/runtime shell smoke assertions.
- Step 8: Commit installer changes
git add install.sh tests/smoke.sh
git commit -m "feat: configure credential source during install"
Task 5: Update Swift Menu-Bar Status Decoding
Files:
-
Modify:
app/Sources/LemanaVPN/VPNManager.swift -
Test:
tests/smoke.sh -
Step 1: Add optional credentials decoding
Inside ModuleStatus, add this struct after ToggleModule:
struct Credentials: Decodable {
var source: String
var keychain_ready: Bool
}
Add this property before var bitwarden: ToggleModule:
var credentials: Credentials?
- Step 2: Reflect source in the module summary
In var summary: String, add this line after let coreState = ...:
let credentialState = credentials.map { "🔐 \($0.source)" } ?? "🔐 legacy"
Replace the return line:
return [coreState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
with:
return [coreState, credentialState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
- Step 3: Log keychain source event
In handleEvent(_:), add this switch case before case "bw_cached"::
case "credential_source":
if let message = event.message { log(" \(message)") }
return
- Step 4: Add a smoke assertion for Swift source display
In tests/smoke.sh, after:
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
add:
grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
- Step 5: Build the app
Run:
swift build -c release --package-path app
Expected: PASS.
- Step 6: Run smoke test
Run:
tests/smoke.sh
Expected: PASS.
- Step 7: Commit Swift changes
git add app/Sources/LemanaVPN/VPNManager.swift tests/smoke.sh
git commit -m "feat: show credential source in menu app"
Task 6: Update Documentation
Files:
-
Modify:
README.md -
Step 1: Update module summary text
In README.md, replace the current Bitwarden-only module description near the top with:
**Credential sources:** `bitwarden` syncs LDAP password and TOTP seed from Bitwarden into macOS Keychain; `keychain` stores LDAP password and a permanent TOTP seed directly in macOS Keychain. Both sources use the same `openconnect-lite` SSO/autofill runtime.
- Step 2: Add keychain source setup commands
In the “Если Bitwarden нет” section, replace the install command block with:
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
| sh -s -- --credential-source keychain --configure-keychain
Add this paragraph after the command:
`--credential-source keychain` is the free built-in path: no Bitwarden account, paid Bitwarden TOTP, or external password manager is required. The setup prompt asks for the corporate LDAP password and a permanent TOTP seed. The seed may be pasted as raw Base32 or as an `otpauth://totp/...?...secret=BASE32` URI.
- Step 3: Update config example
In the ~/.config/lemana-vpn/env example, add:
LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
For the keychain source, add this separate example:
LEMANA_VPN_USERNAME="60103293"
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
LEMANA_VPN_USE_BITWARDEN="0"
LEMANA_VPN_USE_TOUCHID="0"
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
- Step 4: Document current-code rejection
Add this warning in the TOTP explanation:
Do not paste the current 6-digit authenticator code into `vpn --configure-keychain`. Lemana VPN stores the permanent TOTP seed in Keychain, and `openconnect-lite` uses that seed to generate fresh one-time codes during each SSO login.
- Step 5: Commit docs
git add README.md
git commit -m "docs: document keychain credential source"
Task 7: Full Verification
Files:
-
Verify:
install.sh -
Verify:
uninstall.sh -
Verify:
bin/vpn-lemanapro.sh -
Verify:
tests/smoke.sh -
Verify:
app/Sources/LemanaVPN/VPNManager.swift -
Verify:
README.md -
Step 1: Run shell syntax checks
Run:
sh -n install.sh
sh -n uninstall.sh
bash -n bin/vpn-lemanapro.sh
sh -n tests/smoke.sh
Expected: all commands exit 0.
- Step 2: Run smoke tests
Run:
tests/smoke.sh
Expected: prints smoke ok.
- Step 3: Build Swift menu-bar app
Run:
swift build -c release --package-path app
Expected: build exits 0.
- Step 4: Check source status JSON
Run:
LEMANA_VPN_CREDENTIAL_SOURCE=keychain bin/vpn-lemanapro.sh --status --json
Expected output contains:
"credentials":{"source":"keychain"
Run:
LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bin/vpn-lemanapro.sh --status --json
Expected output contains:
"credentials":{"source":"bitwarden"
- Step 5: Check installer dry-runs
Run:
sh install.sh --dry-run --non-interactive --credential-source keychain --configure-keychain --without-app
Expected output contains:
Modules: credential_source=keychain bitwarden=0 touchid=0
Run:
sh install.sh --dry-run --non-interactive --credential-source bitwarden --without-app
Expected output contains:
Modules: credential_source=bitwarden bitwarden=1
- Step 6: Check whitespace
Run:
git diff --check
Expected: no output.
- Step 7: Live VPN decision
Run:
vpn --status
vpn --status --json
If the installed status is connected, connecting, or reconnecting, do not start another VPN session. Record in the final report that live reconnect was intentionally skipped. If the installed status is disconnected and live validation is approved in the execution session, run:
vpn --manual
Expected: visible browser opens, LDAP password and generated TOTP are filled from Keychain, and submit is not pressed.
- Step 8: Final commit
git status --short
git add bin/vpn-lemanapro.sh install.sh tests/smoke.sh app/Sources/LemanaVPN/VPNManager.swift README.md
git commit -m "feat: add keychain credential source"
Self-Review
- Spec coverage: The plan covers a free Keychain/TOTP-seed alternative, separates Bitwarden from Keychain source logic, preserves the existing
openconnect-liteKeychain boundary, updates installer configuration, updates menu-bar status decoding, documents run/check/update flows, and includes non-live plus live-safe verification. - Placeholder scan: The plan contains concrete file paths, exact commands, exact code snippets, and explicit expected outputs.
- Type consistency: The runtime source name is consistently
CREDENTIAL_SOURCEin shell,LEMANA_VPN_CREDENTIAL_SOURCEin config/env,"credentials":{"source":...}in JSON, andModuleStatus.Credentials.sourcein Swift.