Files
lemana-vpn/docs/superpowers/plans/2026-05-24-keychain-totp-provider.md

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> and openconnect-lite / totp/<username>.
  • Modify install.sh
    • Add LEMANA_VPN_CREDENTIAL_SOURCE.
    • Keep --with-bitwarden and --without-bitwarden as backward-compatible aliases.
    • Add a direct --credential-source bitwarden|keychain flag.
    • Disable Touch ID automatically for the keychain source because Touch ID currently gates Bitwarden master-password retrieval only.
  • 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 credentials module object.
    • Keep compatibility with older CLI JSON by making the new object optional.
    • Log source-specific credential events without changing connection state semantics.
  • 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 --help text

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-lite Keychain 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_SOURCE in shell, LEMANA_VPN_CREDENTIAL_SOURCE in config/env, "credentials":{"source":...} in JSON, and ModuleStatus.Credentials.source in Swift.