Improve VPN status reporting and recovery

This commit is contained in:
2026-10-13 06:53:52 +03:00
parent fdb15ae462
commit 7a716b4c9c
188 changed files with 6953 additions and 1911 deletions
+3
View File
@@ -0,0 +1,3 @@
# Goal: Compact Lemana VPN menu
Execute `docs/goals/compact-menu/PLAN.md` with Krypton Execution. Implement the approved mock while preserving all existing auth and connection actions. Use real CLI timestamps, retain older-CLI fallback, keep credentials in the CLI, and report that native visual/live acceptance is untested. Preserve all prior dirty changes.
+41
View File
@@ -0,0 +1,41 @@
# Compact Menu and Session Indicator Implementation Plan
**Intent:** Implement the approved compact macOS menu mockup with Yandex Key import and a small configurable session indicator.
**Current Behavior:** MenuBarExtra uses a plain menu, shield plus H:MM, dense module diagnostics and a login Terminal action. CLI stores started_at but does not expose it to events/status and resets it on repeated expiry lines.
**Expected Outcome:** A native 298-point popover matching the approved layout; import/login actions, preserved connection actions, a persisted ring/clock/time preference (16-point ring default), exact remaining time in menu, elapsed fill and green/amber/red urgency.
**Target-Perspective Output:** Synthetic CLI transcript produces stable started_at plus real expiry; compiled presentation checks demonstrate progress/time/color boundaries, reset/legacy handling and correctly quoted Terminal commands. Release app compiles. Native visual acceptance remains explicitly untested because no GUI testing was requested.
**Truth Owner:** CLI owns connection start/expiry metadata; Swift session model computes derived presentation. UserDefaults owns the indicator preference. CLI/Keychain still own import and credentials.
**Contract Boundary:** Add optional started_at to existing events and connected status; preserve all old fields and auth modes. Swift falls back to the first observed connection time with older CLI, never to an assumed duration.
**Cutover:** Replace the existing plain menu and shield+time connected presentation in place with the approved popover and selected indicator. Reuse the Terminal launcher for import; no second credential UI.
**Displaced Path:** Unconditional shield+time becomes a selected style; dense healthy module details leave the default surface, actionable setup/tunnel warnings remain visible. No new VPN connection/reconnect logic.
**Value Density:** Native SwiftUI window menu, AppStorage and AppKit image drawing; no dependencies, app installation or platform rewrite.
**Acceptance Evidence:** Automated event/status transcript, pure Foundation session checks, generated Zsh script execution against a fake CLI, existing regression suites and Swift build.
**Evidence Lane:** Automated only. No app/Terminal launch, GUI, screenshot, live VPN, real Keychain or installation.
**Kill Criteria:** No fixed 8h duration, repeated timing events cannot reset fill, disabled/disconnected states cannot show stale time, imported secret never enters Swift, new sources ship in installer, existing auth/cancel/copy/log/quit actions remain reachable.
**Architecture Slice:** `bin/vpn-lemanapro.sh` -> VPNEvent -> `VPNManager.swift` -> new Foundation `SessionTiming.swift` -> `LemanaVPNApp.swift`; installer explicitly downloads the new source; focused tests and README describe behavior. Existing dirty work is preserved.
**Plan Review Gate:** Requires PRE review before execution.
## Tasks
1. Root: persist one start timestamp per `_filter_output` connection, include it in connected/session_info/status output, test repeated expiry lines and status round-trip with fake tools. No SSO/DNS/reconnect changes.
2. Root: add session timing model, optional event timestamp, reset on lifecycle transitions, update presentation once per second, preserve legacy timestamp fallback; fix JSON line buffering if required to deliver the new contract through real pipe chunks. Test timing independently and via decoded event fixtures.
3. Root: approved native popover, 16px ring/clock and compact time option, persisted preference, localized accessible status, visible setup/tunnel failures, all existing actions. Reuse Terminal script builder for import and login, fixing the existing read-only Zsh `status` variable and using per-action success text; setup launch failure must not replace an active VPN state.
4. Root: update source packaging, README and narrow smoke wiring expectations; run automated checks and POST correctness/maintainability review. Review agents are read-only; implementation stays with root.
## Behavior details
- Fill = clamp((now - started_at) / (expires - started_at), 0...1) for valid positive duration. Unknown/invalid expiry uses a normal connection symbol and “Срок сессии неизвестен”.
- Remaining >60min green; <=60min amber; <=15min red, including expiry. Exact menu text and accessibility labels accompany color. No continuous decorative spinner.
- Expiry alone does not start another session or pretend the process ended; keep disconnect while the process is still connected. Disconnected/reconnecting/error use distinct system symbols and no stale countdown.
- Start/end reset on an actual new connection attempt and terminal states; repeated connected/session_info data in one attempt retains its origin. RefreshStatus remains a module refresh, without attaching to external VPN processes.
- Approved reference: `/Users/dokril/.codex/visualizations/2026/10/07/01a117e8-cae9-7f02-9dc7-c177c368a223/vpn-menu.html`. Mock-specific demonstration controls are not shipped.
## Completed implementation and evidence
- Native `.window` menu, 298 pt layout, AppStorage ring/clock/time preference, 16 pt AppKit ring/clock image, exact remaining-time labels, import/login Terminal actions and all connection/cancel/copy/log/quit paths implemented. Healthy raw module summaries removed; actionable failures remain visible.
- CLI event, status file and status response share a stable start timestamp. Foundation timing logic handles observed legacy starts, real duration, clamping, unknown/invalid intervals and 60/15-minute thresholds. Lifecycle transitions reset timing, and the UI updates once per second.
- `tests/menu.py`: PASS. Real shell filter/status transcript with synthetic dates, repeated expiry and reconnect; compiled Swift model, every byte boundary in UTF-8/JSON framing, old CLI sole manual-full action; generated Terminal scripts executed in Zsh against a fake quoted-path CLI for success and exit 7. No Terminal application launched.
- `tests/smoke.sh`, `tests/logging.py`, `tests/configuration.py` (10 checks), `tests/yandex_import.py`, shell syntax and `git diff --check`: PASS.
- Final `swift build -c release --package-path app`: PASS (3.07 s). One restricted attempt was blocked by the compiler cache sandbox; an approved build escalation succeeded.
- PRE and POST correctness/maintainability reviews completed. Fixed the sole legacy-action regression and made invalid timing consistently unknown; removed newly unused statusBarTitle/moduleSummary/summary paths.
- Native UI is implemented but unproven visually: no GUI, screenshots, real Terminal/import, installation or live VPN acceptance was run. Existing unrelated dirty changes remain intact.
+3
View File
@@ -0,0 +1,3 @@
# Goal: One configured login action
Use Krypton Execution on `docs/goals/configured-auth-ui/PLAN.md`. The user approved equal full automatic login for Bitwarden and a local imported key. Keep authentication truth in CLI/env, update all runtime/installer/import paths, preserve prior dirty work and validate through isolated automated fixtures. Native visual/live acceptance is not authorized.
+44
View File
@@ -0,0 +1,44 @@
# Configured Authentication and Clear Menu Implementation Plan
**Intent:** Make the saved login approach explicit and make Connect always use it; fix faint/uneven status icons shown in the supplied screenshot.
**Current Behavior:** The connect arrow chooses a one-off override, the saved approach is unclear while connected, Bitwarden uses hidden auto-submit while local imported OTP uses visible/manual submit. Ring track is faint and a round tiny arc looks like an isolated dot.
**Expected Outcome:** A persistent Manual/Automatic setting, with Automatic source Key on this Mac/Bitwarden. Both automatic sources use hidden autofill/OTP/submit (explicitly confirmed by the user). One Connect button without a launch-mode menu. All status symbols fit a consistent 16pt box; neutral visible track and non-rounded progress cap.
**Target-Perspective Output:** Synthetic save/status/launch checks prove that each selection survives restart/update, Connect uses stored choice and both auto sources produce the same launch flags. Swift build and existing regressions pass. Screenshot is user-provided evidence; no new screenshots or GUI checks.
**Truth Owner:** CLI/env own saved auth choice and last automatic source; Swift only sends a setter command and renders returned status. Existing Keychain and importer still own secrets.
**Contract Boundary:** Add canonical `keychain-auto`; retain `manual-keychain`, `bitwarden-auto`, legacy CLI aliases and additive status metadata. New `--set-auth=manual|auto|keychain|bitwarden` saves configuration only. The UI clears inherited auth-selector env overrides and passes no auth override when connecting.
**Cutover:** Existing `manual-keychain + LOCAL_OTP=1` resolves to keychain-auto under the newly approved behavior; explicit manual aliases force LOCAL_OTP=0. Installer preserves and renders the canonical profile; successful import activates keychain-auto.
**Displaced Path:** Remove the connect dropdown and one-off choices from the app UI. Replace old manual-local submit behavior with the user-approved automatic behavior. No parallel Swift preferences for auth.
**Value Density:** Keep existing CLI, Keychain, PyOTP and runtime patches. Use native pickers and an atomic env setter, not another auth service.
**Acceptance Evidence:** Automated temporary-home selection/save/status tests, fake client launch matrix, importer regressions, settings preservation, native build, icon source/geometry review and POST correctness review.
**Evidence Lane:** Automated checks and supplied screenshot inspection only. No installed config changes, real Keychain, live VPN, app/GUI launch or screenshots.
**Kill Criteria:** Connect never picks the first available override; the active setting remains visible in every state; both auto sources have identical browser/submit flags; failed save cannot falsely update the UI; changing settings cannot reconnect an active session; no unsupported claim that a local secret necessarily came from Yandex.
**Architecture Slice:** CLI normalization/status/setter/preflight/launch -> Swift saved settings UI; importer/installer template migration; status label drawing. Existing unrelated dirty work, DNS, SSO selectors and reconnect policy are excluded.
**Plan Review Gate:** Requires PRE review before execution.
## Tasks
1. Root adds keychain-auto, atomic setter and auth status metadata. `LEMANA_VPN_AUTO_SOURCE` records the preferred automatic source so toggling manual/automatic preserves the last source; it is not a separate active mode. Writes update owned MODE/LOCAL_OTP/AUTH_PROFILE/CREDENTIAL_SOURCE/USE_BITWARDEN/AUTO_SOURCE together and preserve other settings. Setter runs before connection or patch side effects, and never reads secret values.
2. Root updates installer defaults/templates/migration and importer activation/copy. Local source is labeled “Ключ на этом Mac” because Bitwarden and import share the existing Keychain entry. Preflight gives source-specific setup guidance; missing credentials stop auto launch instead of prompting invisibly.
3. Root replaces the app dropdown with one configured connect action and native saved-mode/source controls. Save requests disable concurrent save/connect/import; only successful CLI status updates the selection. An active connection keeps running; settings apply next time. Normalize icon boxes/contrast/caps from supplied evidence.
4. Root adds focused fixture tests, updates earlier expectations intentionally changed by full auto, runs regression ladder and records native visual acceptance as untested. Read-only agents perform PRE/POST and correctness/maintainability review.
## Approved behavior
| UI choice | Canonical mode | Password/code source | Browser / submit |
| --- | --- | --- | --- |
| Вручную | manual-keychain | Saved password + manually entered OTP | shown / off |
| Автоматически, Ключ на этом Mac | keychain-auto | Existing Keychain username/password/TOTP | hidden / on |
| Автоматически, Bitwarden | bitwarden-auto | Existing Bitwarden sync to Keychain | hidden / on |
`--manual-full` stays a diagnostic no-autofill command. `--debug` still makes the browser visible. The app will explain a missing/incompatible CLI and disable mode editing until refreshed; there is no automatic fallback to a different mode.
## Completed evidence
- UI now has two persisted modes and a separate automatic-source selector. Connect has no dropdown and sends no mode override. Status refresh is the only settings reader in Swift; connection events cannot overwrite saved settings. Saving, incomplete setup and in-progress authentication are guarded appropriately, with visible save errors.
- Canonical keychain-auto, approved legacy-local migration, importer activation, installer template/wizard and remembered auto source are integrated. Both automatic sources share the hidden/OTP/submit launch branch. The setter changes only the env file and preserves unrelated settings.
- Supplied screenshot/code review identified a faint track plus rounded short arc, rather than a demonstrable off-center circle. Track is now adaptive neutral at 0.45 alpha, progress cap is butt, and system/menu symbols use consistent boxes. No new native visual inspection was run.
- `tests/auth_settings.py`: PASS for all saved choices, last-auto-source restoration, failure/no side effects, local readiness and explicit manual override.
- `tests/smoke.sh`: PASS, including actual setter → ordinary no-override fake-client launch for manual, local automatic and Bitwarden automatic; both automatic sources produce hidden browser and submit enabled.
- `tests/configuration.py`: 11 checks PASS, including profile migration/update and explicit manual override. Importer, menu/timing/transport and logging regression checks PASS. Shell syntax and `git diff --check` PASS.
- Final `swift build -c release --package-path app`: PASS (3.75 s). PRE and POST correctness/maintainability reviews passed after fixing stale connection-module snapshots and hidden save errors.
- No real installation, live VPN, secret-store operation or GUI testing was performed. Native visual quality and real automatic SSO remain unverified in this turn; prior unrelated dirty work is preserved.
+3
View File
@@ -0,0 +1,3 @@
# Goal: Bounded logs and reliable settings
Execute `docs/goals/logging-and-config/PLAN.md`, preserving existing dirty authentication work. Keep one log writer per owner, CLI-owned settings, and backward-compatible events. Capture deterministic CLI/filesystem acceptance evidence; no GUI, live VPN or real install is authorized. Report any unproved behavior explicitly.
+51
View File
@@ -0,0 +1,51 @@
# Bounded Logs and Reliable Settings Implementation Plan
**Intent:** Stop unbounded disk usage and make existing settings predictable without changing authentication or tunnel behavior.
**Current Behavior:** Both log writers append forever. Installer interpolation corrupts special characters and replaces local settings; unknown runtime flags can fall through to connection.
**Expected Outcome:** Each log has a bounded current file and one bounded previous file (5 MiB each by default); custom settings survive updates; settings round-trip literally; mistyped commands fail before connecting.
**Target-Perspective Output:** Isolated CLI/filesystem examples show capped current/archive sizes and recent records, valid JSON with quoted settings, preserved configuration after two renders, and a rejected unknown flag.
**Truth Owner:** CLI env for runtime settings and the existing TOML for SSO. CLI owns the log limit reported to Swift; each process owns its log writer.
**Contract Boundary:** Keep current status/events/auth modes, adding optional logging metadata. Installer preserves unowned env lines and existing TOML profile/rules while updating the selected mode and username.
**Cutover:** Replace append-only writers in place; retain existing log names and add only `.1`. Do not mutate the installed setup or real logs during development.
**Displaced Path:** Raw sed template interpolation and unbounded writes are removed, with no parallel logging backend or new dependency.
**Value Density:** Bounded storage, correct literal settings, and update preservation address observable defects; avoid a general settings framework and wholesale shell/Swift rewrite.
**Acceptance Evidence:** Before/after automated regression checks using temporary files/homes and fake clients; shell smoke and Swift release build. Include long-running writes, oversized existing files/records, log I/O failure, quoted settings, update preservation, invalid flags.
**Evidence Lane:** Automated local CLI/filesystem only. No GUI, live connection, installation, Keychain writes, or deletion of real logs.
**Kill Criteria:** No append-only production writer; no raw sed settings interpolation; no unknown-flag path into connect; tests exercise the changed behavior.
**Architecture Slice:** `bin/vpn-lemanapro.sh` -> raw client stream/log + status JSON -> `VPNManager.swift` -> bounded app log. `install.sh` -> env templates/TOML -> same CLI. Authentication patches, DNS cleanup, health/reconnect semantics and unrelated dirty work are excluded.
**Plan Review Gate:** Requires PRE review before execution.
## Tasks and ownership
1. Root: bounded shell writer in `bin/vpn-lemanapro.sh`, Foundation-only `BoundedLog.swift` used by `VPNManager.swift`, log-limit setting and optional JSON metadata. Test both writers with actual temporary files, including existing oversized logs, repeated rollover and write failures; preserve event parsing. Unknown runtime flags fail with status 2. Literal JSON encoding for BW item. Fix `_prompt_read` EOF propagation and explicitly stop every caller before subsequent persistence/credential actions; empty Enter keeps existing behavior. Test username, secret and save-choice EOF with fake input and a timeout. Do not change auth modes/sources, health or reconnect semantics.
2. Configuration agent (parallel, disjoint): `install.sh`, env templates, new isolated configuration regression check. Precedence: flags > invocation env > saved env > defaults for supported installer settings; preserve arbitrary runtime settings and TOML custom profile/rules. Replace raw sed interpolation with stdlib literal escaping, reusing existing quoting conventions. Verify a second render preserves custom settings and special characters; installer environment mode takes effect. Do not edit runtime, Swift, README or smoke test.
3. Root integration: wire small regression checks into existing automated workflow as appropriate, document editable settings, precedence and rotation in README. Fix only evidence-backed issues in changed paths. Record remaining audit findings instead of opportunistic auth/refactoring work.
4. Read-only reviewer: POST contract/correctness/maintainability review. Root resolves blockers and runs syntax checks, focused regressions, `tests/smoke.sh`, `swift build -c release --package-path app`, `git diff --check`.
## Risks and evidence
`LEMANA_VPN_LOG_MAX_BYTES` defaults to 5242880; accept decimal integers 1..999999999, reject empty explicit values, zero, negative and malformed values before side effects. Swift defaults to 5242880 before status or with an old CLI and uses the validated optional CLI value thereafter. Verify saved env -> status JSON -> Swift decoding/writer setting. PRE review findings above are incorporated; proceed to execution.
| Contract | Risk | Independent oracle |
| --- | --- | --- |
| Log size and recent history | Disk growth, copying giant legacy file | Current and `.1` each <= configured byte limit, newest record retained, only two files |
| Log failures | VPN output consumer exits | Fake output still produces expected JSON event with unwritable log destination |
| Settings round-trip/update | Corruption, command substitution, silent resets | Exact recovered values, sentinel command never executed, custom TOML values preserved |
| CLI validation | Typo initiates SSO | Exit 2 before client/status file creation |
| Compatibility | Existing dirty auth work regresses | Baseline vs final smoke results and Swift build, live acceptance explicitly unrun |
## Deferred audit findings
- Arbitrary `OPENCONNECT_LITE_CONFIG_DIR` and XDG runtime path need a separate end-to-end contract; do not mask this with status-only changes.
- The pre-existing CLI `_persist_username` writer selects the first username-like TOML line; unlike the revised installer it is not scoped to `[credentials]`. Custom profiles containing another username need a follow-up shared configuration-write contract and regression before broadening editable SSO settings.
- Broader extraction of embedded SSO patches and reconnect/state-machine changes require their own evidence and are outside this slice.
## Execution evidence
- Baseline: shell syntax, existing smoke, release Swift build and diff whitespace checks passed before edits.
- Historical app log was measured read-only at 2,552,898,484 bytes. Repeated closed-terminal credential prompts dominated the sampled tail. Current baseline still reproduced a closed-input loop; the new regression times out on that baseline and passes with explicit EOF propagation.
- `python3 tests/configuration.py`: 10 checks pass, covering literal round-trips, saved settings, precedence, legacy inputs, TOML variants and fresh help without Python. The first six regression cases all failed against the saved baseline installer.
- `python3 tests/logging.py`: passes actual CLI/Swift file writes, bounded legacy/repeated/oversized records, reduced limits, private permissions, closed username/password/TOTP/Bitwarden/consent input, preserved empty Enter, JSON output on log failure, settings decoding and rejected flags/limits.
- Final shell syntax, `tests/smoke.sh` (`smoke ok`), `swift build -c release --package-path app` and `git diff --check`: PASS.
- PRE/POST correctness and maintainability reviews completed; findings in the changed slice resolved. CLI ordinary-write microbenchmark improved from the first implementation's 4.800 s to 1.806 s per 300 records (old append-only baseline 0.831 s); the remaining overhead checks file size before writing.
- Real logs and installed files were not changed. No GUI, live VPN, installation or credential-store operation was performed. Live connection acceptance remains untested.
+3 -11
View File
@@ -1,13 +1,5 @@
# Goal: Manual-First Auth Flow
# Goal Superseded
Use Krypton Execution to execute `docs/goals/manual-first-auth-flow/PLAN.md`.
Use `docs/goals/two-auth-modes/GOAL.md` instead.
Core rules:
- Treat PLAN.md as the source plan.
- Preserve intent, ownership, contract, cutover, evidence, and kill criteria.
- Make fresh install and default `vpn` / app connect manual-first.
- Keep the installer understandable for a non-IT user: plain wording, manual mode on Enter, optional conveniences explained as choices.
- Keep Bitwarden, Touch ID, saved TOTP, autosubmit, and headless mode as explicit opt-in features.
- Do not add a new dominant path without deleting, redirecting, demoting, or shimming the displaced Bitwarden/auto-first path.
- Capture acceptance evidence from a clean temporary HOME and the menu-bar app contract.
- Say "implemented but unproven" if that evidence cannot be captured.
The older manual-first goal is intentionally replaced by the two-mode contract.
+6 -436
View File
@@ -1,440 +1,10 @@
# Manual-First Auth Flow Implementation Plan
# Manual-First Auth Flow Plan Superseded
**Intent:** Перевернуть Lemana VPN из auto/Bitwarden-first в manual-first: новая установка и действие по умолчанию открывают видимый SSO-браузер без сохранённых секретов, а удобства включаются отдельными функциями.
**Current Behavior:** Fresh install по умолчанию выбирает `credential_source=bitwarden`, ставит Bitwarden/Touch ID, `vpn` запускает hidden browser с autofill+submit, а `manual-full` существует как отдельный диагностический режим.
**Expected Outcome:** Базовый путь не требует Bitwarden, Keychain, TOTP seed или автоотправки. Пользователь может поэтапно включить сохранение LDAP login/password в macOS Keychain, затем TOTP seed/autofill, затем Bitwarden sync, Touch ID, autosubmit/headless.
**Target-Perspective Output:** Новый пользователь запускает installer, ничего специально не выбирает, нажимает `vpn` или основную кнопку в `LemanaVPN.app`, видит Keycloak форму и вводит LDAP, пароль и одноразовый код руками. Если он включил сохранение LDAP в Keychain, username/password подставляются, а одноразовый код он вводит руками.
**Truth Owner:** `bin/vpn-lemanapro.sh` владеет runtime-решением режима; `~/.config/lemana-vpn/env` хранит выбранные capabilities; macOS Keychain хранит только секреты; Swift app только вызывает CLI и читает JSON.
**Contract Boundary:** Installer пишет env/config, CLI читает env/flags/Keychain и отдаёт JSON Lines/status JSON, Swift app потребляет только этот CLI-контракт. `openconnect-lite` остаётся SSO/VPN engine.
**Cutover:** Fresh installs become manual-first. Existing configs with `LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden` keep working, but docs/UI stop presenting Bitwarden as default. Legacy aliases `vpn-auto`, `vpn-manual`, `vpn-manual-full` stay as compatibility shims.
**Displaced Path:** The displaced dominant path is `Bitwarden + Touch ID + hidden autofill + autosubmit by default`. It becomes an explicit opt-in feature set, not the default route.
**Value Density:** First slice removes the biggest onboarding failure: users without Bitwarden can connect manually immediately, then add only the conveniences they need.
**Acceptance Evidence:** In a clean temporary HOME, `sh install.sh --dry-run --non-interactive` reports manual/default auth with Bitwarden and Touch ID off; the interactive installer copy reads like a step-by-step setup for a non-IT user; `bash bin/vpn-lemanapro.sh --json` without credentials reaches a visible browser launch without `username_required` or `keychain_required`; a Keychain-login-only fixture fills username/password but leaves OTP manual and submit disabled; the Swift app primary action maps to the manual-first path.
**Evidence Lane:** Non-live smoke ladder first: `sh -n`, `bash -n`, `tests/smoke.sh`, `swift build`, `git diff --check`. Live VPN is optional after status check and should start with visible manual mode only.
**Kill Criteria:** No fresh-install default may select Bitwarden, require a TOTP seed, autosubmit, or hide the browser. No docs may state Bitwarden is enabled by default. No default CLI/app path may require saved credentials before opening manual SSO.
**Architecture Slice:** Installer defaults + CLI auth profile/capability model + Swift launch mode labels + README/smoke coverage.
**Plan Review Gate:** Requires PRE review before execution.
This plan is superseded by `docs/goals/two-auth-modes/PLAN.md`.
## Короткий План
Do not execute this older plan. It described an intermediate four-profile model. The current approved contract keeps exactly two public modes:
🎯 Цель: fresh install, `vpn` и основная кнопка приложения стартуют с полностью ручного SSO; сохранение LDAP, TOTP, Bitwarden, autosubmit и headless включаются отдельно.
- `manual-keychain`
- `bitwarden-auto`
📦 Репозитории: только `/Users/dokril/git/home/lemana-vpn`.
🧩 Модули: `install.sh`, `bin/vpn-lemanapro.sh`, `app/Sources/LemanaVPN`, `tests/smoke.sh`, `README.md`; `templates/openconnect-lite-config.toml` только если login-only autofill потребует правки правил.
🛠 Основные изменения:
- Переключить fresh defaults с Bitwarden/auto-first на manual-first.
- Ввести явный auth profile/capability contract: manual, saved-login, saved-totp, auto.
- Разрешить Keychain хранить только LDAP username/password без обязательного TOTP seed.
- Оставить Bitwarden, Touch ID, autosubmit/headless как opt-in функции.
- Сделать Swift app CLI-wrapper поверх тех же режимов, с manual-first primary action.
- Зафиксировать всё в smoke-тестах и README.
✅ Проверка: `sh -n install.sh`, `sh -n uninstall.sh`, `bash -n bin/vpn-lemanapro.sh`, `sh -n tests/smoke.sh`, `tests/smoke.sh`, `swift build -c release --package-path app`, `git diff --check`.
⚠️ Риски: старые пользователи могут ожидать auto-connect на `vpn`; partial Keychain без TOTP надо проверить fake launch тестом и, при необходимости, видимым manual smoke.
➡️ Следующий шаг: начать с `install.sh` и зафиксировать новый env/flag contract для manual-first defaults.
## Architecture Slice
Files to create:
- None required for runtime. This goal package is the planning artifact.
Files to modify:
- `install.sh` - defaults, wizard wording, flags, env rendering, dry-run output.
- `bin/vpn-lemanapro.sh` - auth profile/capability model, default mode, partial Keychain support, JSON events.
- `app/Sources/LemanaVPN/VPNManager.swift` - launch mode enum/default, JSON handling if new events are added.
- `app/Sources/LemanaVPN/LemanaVPNApp.swift` - menu labels/order so manual-first is primary and auto is secondary.
- `tests/smoke.sh` - regression coverage for fresh defaults, manual launch without credentials, partial Keychain, app wiring.
- `README.md` - user-facing feature matrix and migration/update instructions.
- `templates/openconnect-lite-config.toml` only if login-only filling needs rule adjustments; otherwise avoid.
Files to avoid:
- `libexec/lemana-vpn-dns-cleanup` unless DNS behavior breaks.
- `uninstall.sh` except if new Keychain account names require cleanup.
- Direct edits inside installed `openconnect-lite`; use the existing runtime patch path from `bin/vpn-lemanapro.sh`.
Source of truth:
- Repo source for behavior.
- Installed env file for user-selected capabilities.
- Keychain for LDAP password and optional TOTP seed.
Read path:
- `install.sh` writes `~/.config/lemana-vpn/env`.
- `bin/vpn-lemanapro.sh` sources env, applies CLI flag overrides, checks Keychain capability presence, then launches `openconnect-lite`.
- `LemanaVPN.app` calls `~/bin/vpn-lemanapro.sh --json <mode>`.
Write path:
- `vpn --configure-login` or equivalent writes username/password only.
- `vpn --configure-keychain` remains the full saved-credentials path and writes password plus permanent TOTP seed.
- Bitwarden sync, when enabled, writes password plus optional TOTP seed into Keychain.
Contract boundary:
- CLI flags and JSON events are the public contract for the app and tests.
- Suggested new runtime contract:
- `auth_profile=manual`: visible browser, no preflight, no autofill, no submit.
- `auth_profile=saved-login`: visible browser, username/password autofill, OTP manual, no submit.
- `auth_profile=saved-totp`: visible browser, username/password/TOTP autofill, no submit.
- `auth_profile=auto`: hidden or visible automatic mode, full autofill and submit; explicit only.
- `credential_source=none|keychain|bitwarden`: where saved values come from, separate from whether submit/headless are enabled.
Integration points:
- macOS Keychain service `openconnect-lite`.
- Optional Bitwarden CLI item `LM LDAP`.
- `openconnect-lite` config autofill rules.
- Swift app menu and reconnect behavior.
- Existing aliases from shell install block.
Migration/cutover:
- Fresh installs write `credential_source=none` and `auth_profile=manual`.
- Existing `credential_source=bitwarden` installs continue as saved-totp/auto-capable, but `vpn` default should still prefer manual-visible unless the user explicitly chooses auto.
- `vpn-auto` remains the explicit old behavior.
- `--manual-full` remains compatibility alias for the new default manual profile.
Displaced path:
- Installer Enter=Bitwarden.
- CLI `CONNECT_MODE=auto`.
- Full password+TOTP seed as mandatory preflight for non-`manual-full`.
- App primary/default connect action pointing at `.auto`.
Acceptance evidence gate:
- A clean temp HOME proves fresh defaults.
- Captured interactive installer output proves that the default path is understandable without knowing Bitwarden, Keychain, TOTP seed, or `credential_source`.
- Fake `openconnect-lite` launch capture proves display/autofill/click env for each profile.
- App source/smoke proves primary UI wiring.
- Installed-bundle verification is required only when refreshing the local app after implementation.
## Пользовательский Процесс Установки
Цель этого сценария: человек, далёкий от IT, должен понимать, что сейчас произойдёт, что от него требуется, и что можно пропустить. Installer не должен начинать с технического выбора credential source.
### Первый Экран
Текст должен быть коротким:
```text
Lemana VPN установит приложение для подключения к корпоративному VPN.
По умолчанию ничего не сохраняется: при подключении откроется окно входа,
а вы введёте логин, пароль и одноразовый код сами.
Дополнительные удобства можно включить сейчас или позже.
```
Основное действие по Enter: продолжить с ручным безопасным режимом.
Запрещено на первом экране:
- `credential_source`
- `auth_profile`
- `TOTP seed`
- `openconnect-lite`
- `Bitwarden item`
- `headless`
### Шаг 1: Что Будет Установлено
Installer показывает не список пакетов, а понятные пункты:
```text
Будет установлено:
- приложение Lemana VPN в меню macOS;
- команда vpn для подключения из Terminal;
- VPN-компонент, который открывает корпоративную страницу входа;
- безопасная очистка DNS после отключения.
```
Технические зависимости (`openconnect`, `pipx`, `openconnect-lite`) можно показывать только в подробном логе ниже или в debug/dry-run.
### Шаг 2: Базовый Режим Входа
Вопрос:
```text
Как подключаться по умолчанию?
1) Вводить всё вручную (рекомендуется)
Откроется окно входа. Вы сами введёте логин, пароль и одноразовый код.
2) Запомнить логин и пароль на этом Mac
При входе логин и пароль подставятся сами. Одноразовый код вы введёте вручную.
3) Настроить автоматический вход
Нужно заранее сохранить пароль и постоянный код настройки 2FA.
Выбор [1/2/3, Enter=1]:
```
Поведение:
- `1` ставит `auth_profile=manual`, `credential_source=none`.
- `2` ставит `auth_profile=saved-login`, `credential_source=keychain`, после установки запускает настройку LDAP username/password.
- `3` открывает следующий уточняющий шаг и не должен быть default.
### Шаг 3: Автоматический Вход Только Для Продвинутого Пути
Если выбран пункт 3, installer объясняет риск простым языком:
```text
Автоматический вход работает только если на Mac сохранены:
- корпоративный пароль;
- постоянный код настройки 2FA.
Это не текущие 6 цифр из приложения-аутентификатора.
Если вы не знаете, что это такое, выберите ручной режим.
```
Варианты:
```text
1) Вернуться к ручному режиму
2) Сохранить пароль и постоянный код 2FA в macOS Keychain
3) Взять пароль и код 2FA из Bitwarden
```
Поведение:
- `1` возвращает к `auth_profile=manual`.
- `2` ставит `auth_profile=saved-totp`, `credential_source=keychain`, запускает full Keychain setup.
- `3` ставит `credential_source=bitwarden`; Touch ID спрашивается отдельным простым вопросом.
### Шаг 4: Bitwarden Как Дополнительная Функция
Bitwarden не должен выглядеть обязательным. Вопрос появляется только после явного выбора Bitwarden:
```text
Использовать Bitwarden для хранения VPN-пароля?
Подойдёт, если у вас уже есть запись с корпоративным VPN-паролем
и постоянным кодом настройки 2FA.
```
Если Bitwarden CLI не найден:
```text
Bitwarden не найден на этом Mac.
Можно установить его сейчас или вернуться к ручному режиму.
```
Default: вернуться к ручному режиму, если пользователь не уверен.
### Шаг 5: Touch ID Только После Bitwarden
Вопрос:
```text
Разрешить Touch ID для открытия Bitwarden?
Это не VPN-пароль. Touch ID только помогает достать данные из Bitwarden.
```
Default: нет, если пользователь не выбирал Bitwarden; да/нет можно спрашивать только в Bitwarden path.
### Шаг 6: Понятный Прогресс Установки
Progress log должен говорить человеческим языком:
```text
Проверяю компоненты для VPN...
Устанавливаю приложение Lemana VPN...
Добавляю команду vpn...
Настраиваю безопасное отключение VPN...
Готово.
```
Если macOS просит пароль администратора, installer заранее объясняет:
```text
macOS может запросить пароль от компьютера.
Он нужен, чтобы разрешить VPN подключаться и корректно очищать DNS после отключения.
```
### Финальный Экран
Для ручного режима:
```text
Готово. Как подключиться:
1. Откройте Lemana VPN из Applications или меню macOS.
2. Нажмите "Подключить".
3. В открывшемся окне введите корпоративный логин, пароль и одноразовый код.
Проверить состояние: vpn --status
```
Для режима с сохранённым логином и паролем:
```text
Готово. При подключении логин и пароль будут подставлены автоматически.
Одноразовый код нужно ввести вручную.
```
Для автоматического режима:
```text
Готово. Автоматический вход включён.
Если он не сработает, используйте ручной режим: vpn --manual-full
```
### Ошибки И Подсказки
Ошибки должны говорить, что делать дальше:
- Нет сохранённого пароля: `Запустите vpn --configure-login или подключитесь вручную.`
- Нет постоянного кода 2FA для auto mode: `Автоматический вход не настроен. Выберите ручной режим или добавьте постоянный код настройки 2FA.`
- Bitwarden заблокирован в приложении без Terminal: `Откройте Terminal и выполните vpn, либо подключитесь вручную.`
- Пользователь выбрал непонятный вариант: `Введите 1, 2 или 3. Если не уверены, нажмите Enter.`
### Acceptance Evidence Для Installer UX
- Interactive dry-run через `expect` должен захватить первый экран, выбор режима и финальную подсказку.
- Smoke должен проверять, что Enter выбирает ручной режим.
- README должен показывать тот же сценарий, что и installer.
- В обычном wizard не должно быть слов `credential_source`, `auth_profile`, `TOTP seed`; они допустимы только в help/debug/docs for advanced.
## Дополнительные Функции
1. **Core manual SSO (default)**
- Что делает: открывает видимый браузер Keycloak, не читает Bitwarden/Keychain, не заполняет поля, не нажимает submit.
- Когда включается: всегда на fresh install и при `vpn` без дополнительных настроек.
- Когда не включается: если пользователь явно запускает `vpn-auto` или выбрал auto profile.
2. **Saved LDAP login/password in macOS Keychain**
- Что делает: сохраняет LDAP username/password в Keychain и подставляет их в видимую форму.
- Когда включается: пользователь явно запускает настройку сохранения LDAP или выбирает этот пункт в installer.
- OTP: текущий одноразовый код вводится руками; TOTP seed не требуется.
3. **Saved TOTP seed**
- Что делает: сохраняет постоянный TOTP seed и позволяет `openconnect-lite` генерировать текущий код для autofill.
- Когда включается: пользователь явно добавил seed через full Keychain setup или Bitwarden sync.
- Ограничение: текущий 6-значный код нельзя сохранять как настройку.
4. **Autofill**
- Что делает: заполняет те поля, для которых есть capability: username/password, а затем optional TOTP.
- Когда включается: при `saved-login` или `saved-totp`.
- Submit: по умолчанию выключен; пользователь нажимает вход сам.
5. **Autosubmit/headless**
- Что делает: запускает старый удобный путь с hidden browser, полным autofill и нажатием submit.
- Когда включается: только явным `vpn-auto`, `--auto`, profile `auto` или отдельным opt-in флагом.
- Предусловие: saved login/password и TOTP seed должны быть доступны; без TOTP seed этот режим должен fail fast с понятной ошибкой.
6. **Bitwarden sync**
- Что делает: читает LDAP password и TOTP seed из Bitwarden item, нормализует TOTP seed и перекладывает в Keychain.
- Когда включается: только если пользователь выбрал Bitwarden provider.
- Fallback: если `bw` отсутствует или locked в GUI/no-tty, использовать уже сохранённый Keychain либо manual path, не блокировать default.
7. **Touch ID helper**
- Что делает: локально защищает чтение Bitwarden master password.
- Когда включается: только вместе с Bitwarden provider и явным согласием пользователя.
- Не делает: не заменяет LDAP password и не является хранением VPN credentials.
## Tasks
### Task 1: Rewrite user-facing contract and docs
- Scope: `README.md`.
- Change: describe manual-first default, feature matrix, migration from old Bitwarden-first install, and the non-IT installation scenario from this plan.
- Expected output: docs no longer say Bitwarden/auto are default; examples show `vpn` as manual and `vpn-auto` as opt-in; installation instructions use the same wording as the installer.
- Verification: `rg -n "по умолчанию|Bitwarden|vpn-auto|manual-full|одноразовый код|ручной режим" README.md`.
- Acceptance evidence: a new user can read the first install section and understand what will be installed, what they will type during first connect, and that Bitwarden is optional.
- Parallel: yes, but must be reconciled with installer flag names from Task 2.
### Task 2: Change installer defaults and capability selection
- Scope: `install.sh`.
- Change: default credential source becomes none/manual; Bitwarden and Touch ID default off; wizard Enter keeps manual; add or reuse flags for saved LDAP, full Keychain, Bitwarden provider, and auto profile; rewrite interactive copy using the "Пользовательский Процесс Установки" section.
- Expected output: dry-run shows manual/default auth and no Bitwarden/Touch ID install unless selected; interactive prompts say "вводить всё вручную", "запомнить логин и пароль", and "автоматический вход" instead of exposing low-level config names.
- Verification: `sh -n install.sh`; `sh install.sh --dry-run --non-interactive`; interactive dry-run through `expect` if available.
- Acceptance evidence: clean install dry-run does not try to install Bitwarden and does not schedule Keychain credential prompts by default; captured interactive output is understandable without IT vocabulary and Enter chooses manual mode.
- Parallel: no, because it defines env contract consumed by CLI/tests.
### Task 3: Refactor CLI auth profiles and partial Keychain support
- Scope: `bin/vpn-lemanapro.sh`.
- Change: replace auto-first/default credential preflight with an auth-profile resolver; add partial Keychain support for saved login without TOTP; keep full Keychain and Bitwarden sync as opt-ins.
- Expected output: no-credential default reaches visible manual browser; saved-login fills username/password and leaves OTP manual; saved-totp can fill OTP but still does not submit by default.
- Verification: `bash -n bin/vpn-lemanapro.sh`; fake `openconnect-lite` capture in `tests/smoke.sh`.
- Acceptance evidence: JSON events clearly state active auth profile and missing optional capabilities without treating them as fatal in manual profiles.
- Parallel: no, central truth owner.
### Task 4: Preserve automatic compatibility without keeping it dominant
- Scope: `bin/vpn-lemanapro.sh`, `install.sh`, shell aliases.
- Change: keep `vpn-auto`/`--auto` as explicit old behavior; guard autosubmit/headless behind full saved credentials; keep `--manual-full` compatibility alias for the default manual profile.
- Expected output: current power users can still run the old path intentionally.
- Verification: `tests/smoke.sh` assertions for `vpn-auto`, `--auto`, `--manual-full`, launch env.
- Acceptance evidence: default path and auto path are both test-covered and visibly separate.
- Parallel: can run after Task 3.
### Task 5: Update Swift menu-bar app surface
- Scope: `app/Sources/LemanaVPN/VPNManager.swift`, `app/Sources/LemanaVPN/LemanaVPNApp.swift`.
- Change: make the primary menu action manual-first; demote automatic connect to an explicit secondary action; keep app as a CLI wrapper; surface missing saved credentials as setup guidance, not hidden prompts.
- Expected output: app default action does not trigger Bitwarden or autosubmit unless the user chooses an explicit auto/saved mode.
- Verification: `swift build -c release --package-path app`; smoke greps for the new labels/mode wiring.
- Acceptance evidence: app source and built binary contain the manual-first label and map it to the manual profile.
- Parallel: yes after CLI mode names are stable.
### Task 6: Expand smoke coverage
- Scope: `tests/smoke.sh`.
- Change: update old assertions and add cases for fresh manual default, saved-login-only, saved-totp, Bitwarden opt-in, and app wiring.
- Expected output: smoke becomes the executable spec for the feature matrix and installer UX.
- Verification: `sh -n tests/smoke.sh`; `tests/smoke.sh`.
- Acceptance evidence: smoke fails if Bitwarden or auto become fresh defaults again, or if the basic wizard no longer defaults to manual mode with non-technical wording.
- Parallel: yes after Task 2 and Task 3 contracts are known.
### Task 7: Final verification and local update notes
- Scope: whole repo; installed app only if doing a local refresh.
- Change: run non-live ladder, then optionally install/update local checkout with `sh install.sh` after user approval for real local changes.
- Expected output: plan implemented with evidence and a clear "live VPN not run" or live manual evidence.
- Verification:
- `sh -n install.sh`
- `sh -n uninstall.sh`
- `bash -n bin/vpn-lemanapro.sh`
- `sh -n tests/smoke.sh`
- `tests/smoke.sh`
- `swift build -c release --package-path app`
- `git diff --check`
- Acceptance evidence: final answer includes how to run, how to check, how to update installed setup, and what live checks were intentionally skipped.
- Parallel: no, final integration gate.
## Non-Goals
- Do not replace `openconnect-lite` or implement a separate VPN client in Swift.
- Do not store current 6-digit OTP codes.
- Do not make the GUI collect secrets directly.
- Do not remove existing automatic mode; demote it to explicit opt-in.
- Do not change DNS cleanup behavior as part of this refactor.
## Risks
- Existing users may expect `vpn` to auto-connect. Mitigation: keep `vpn-auto` and document the cutover clearly.
- Partial Keychain support may need careful `openconnect-lite` behavior verification when TOTP is absent. Mitigation: fake launch tests first, then one visible manual smoke if needed.
- Swift app could lag behind installed source again. Mitigation: after implementation refresh installed bundle via `sh install.sh` and verify with app source/build or `strings` if doing local install.
- Credential language can confuse Bitwarden master password, LDAP password, TOTP seed, and current OTP. Mitigation: keep labels explicit in CLI and README.
## Execution Order
1. Task 2 defines the installer/env contract.
2. Task 3 implements the CLI truth owner.
3. Task 4 preserves explicit auto compatibility.
4. Task 5 updates the app wrapper.
5. Task 6 turns the matrix into smoke coverage.
6. Task 1 updates docs with final flag/profile names.
7. Task 7 verifies and prepares local update instructions.
Use `docs/goals/two-auth-modes/GOAL.md` for execution.
+19
View File
@@ -0,0 +1,19 @@
# Goal: Recover stale VPN DNS and ship the approved icons
Use Krypton Execution on `docs/goals/startup-dns-and-icons/PLAN.md`. Preserve user work and auth behavior. Root owns privileged DNS logic and integration; the icon agent has an explicitly disjoint write scope. Prove recovery using synthetic journal/process/DNS fixtures, never real network changes. Report unrun native/reboot acceptance and snapshotless legacy limits.
## Result — 2026-10-08
Implemented in source. Normal installation updates the root helper and ships the native icon renderer. The app gates connection on a capability-checked startup recovery, displays failures/external sessions, and retries through Refresh. CLI exit replaces its finished process before recovery, retaining the client's exit code. DNS snapshots are data-only, versioned, private, durable and serialized with native lockf; no arbitrary private-address reset remains.
PRE review passed after adding unresolved-journal retention, shared locking and verified owner ancestry. Post-review fixes cover Bash pipeline/progress siblings and custom DNS on newly enabled network services. Final integration review: no outstanding blockers.
Automated evidence:
- `python3 tests/dns_recovery.py`: all 16 cases passed; subsequent focused competing-engine regression under the same CLI also passed. Original helper replay fails the new reboot recovery contract as expected.
- `python3 tests/dns_cli.py`: noninteractive command, legacy helper rejection, fixed JSON, path quoting and preserved client exit codes passed.
- `python3 tests/menu.py` and `python3 tests/icons.py`: startup lifecycle, fake CLI, real session timing, both icon themes and offscreen pixel checks passed.
- `tests/smoke.sh`, `python3 tests/configuration.py`, `python3 tests/auth_settings.py`, `python3 tests/logging.py`, and the installed openconnect-lite Python running `tests/yandex_import.py`: passed.
- Shell syntax, `git diff --check` and `swift build -c release --package-path app`: passed.
Real installation, app/GUI launch, live VPN/DNS changes and actual reboot acceptance were not run. Snapshotless pre-upgrade DNS must first be restored to the user's normal settings; no software can infer an unrecorded custom value. Other network locations, deleted services or invalid journals fail visibly and preserve the record.
+40
View File
@@ -0,0 +1,40 @@
# Startup DNS Recovery and Approved Icons Implementation Plan
**Intent:** Install the approved shield icon family with gradual interior fill, and restore normal DNS at app startup after an interrupted VPN session.
**Current Behavior:** App init only refreshes status. vpnc-script persistently changes network-service DNS; abrupt shutdown skips its cleanup and /var/run backups disappear at boot. The existing helper resets any 10.x DNS on three service names and is skipped on ordinary reinstall. Runtime icons still differ from the approved set.
**Expected Outcome:** App startup runs a noninteractive guarded recovery before allowing Connect. A root-owned vpnc wrapper records original DNS and exact VPN DNS before changes, persists the journal across reboot, and restores matching leftovers only when no VPN/SSO is active. Approved vector geometry and real session progress are used by runtime icons.
**Target-Perspective Output:** Isolated reboot/crash fixtures recover recorded DHCP or custom DNS, preserve unrelated/changed DNS, skip active tunnels/SSO, survive reused PIDs and surface failed recovery. Icon bitmap/geometry checks and Swift build prove the native implementation without launching the GUI.
**Truth Owner:** Root-owned DNS wrapper/journal owns before/expected DNS evidence and mutations. CLI owns invocation/events/capability reporting. Swift only requests recovery and reports results. SessionTiming still owns derived icon progress.
**Contract Boundary:** Add `--repair-dns` to CLI; root helper protocol 2 has `--recover`, `--vpnc` and capability inspection. Existing JSON fields remain; DNS capability is additive. App calls recovery only after the CLI advertises support. OpenConnect receives the root wrapper through --script; the wrapper delegates normal routes/DNS setup to installed vpnc-script.
**Cutover:** One root helper serves hook, exit and startup recovery. Remove broad automatic 10.x reset and the CLI's unowned fallback. Installer always updates the root helper. Root journal lives under /var/db, not /var/run. Default runtime icon becomes shield; previously saved ring/clock/time selections remain.
**Displaced Path:** Ad hoc unconditional DNS resets and independent Swift icon shapes. No new daemon, VPN client, credential path, root Python from user-controlled locations or general network-settings API.
**Value Density:** Reuse native networksetup, vpnc-script and existing sudoers wrapper boundary. New files are limited to native icon drawing and focused regression checks; no dependencies.
**Acceptance Evidence:** Real helper source executed with substituted system tools/temp journal, synthetic OpenConnect hook environment and recorded command log; CLI recovery/fake client argument checks; startup wiring/build; headless icon rendering checks and existing suites.
**Evidence Lane:** Automated fixtures only. No actual reboot, live DNS mutation, real VPN, Keychain, app/GUI launch or screenshots.
**Kill Criteria:** Recovery cannot clear arbitrary non-default DNS; current values must match recorded VPN values. Live or indeterminate process checks cannot authorize cleanup. No connection begins during startup recovery. No stale-PID-only guard. New helper and Swift source must ship on normal update.
**Architecture Slice:** `libexec/lemana-vpn-dns-cleanup` + `bin/vpn-lemanapro.sh` + `VPNManager.swift` + installer/template/tests; independent `VPNIcons.swift` + `LemanaVPNApp.swift` + `SessionTiming.swift`. All prior dirty work is preserved.
**Plan Review Gate:** PRE review passed; implementation and post-review checks completed (see GOAL.md).
## DNS contract
- The helper uses fixed system commands and root-owned persistent files. Snapshot stores location, service names, original DNS (empty means DHCP), exact INTERNAL_IP4_DNS, boot identity and owning OpenConnect PID/start identity. It never sources state as code.
- All hook and recovery transactions share native `/usr/bin/lockf -k` in the protected root directory. The helper runs its embedded shell body under the lock, with no public unlocked entry point. The lock releases on process death and survives reboot as an unlocked file. Validate versioned journal data and reject symlink/untrusted state paths.
- Never overwrite an unresolved journal: recover a proven orphan under the same lock, otherwise fail the new hook visibly and retain the journal. A hook may exclude only its verified OpenConnect owner and actual ancestors from the live-process guard; startup excludes nobody. Owner identity is boot plus PID plus process start time.
- Hook connect stores a durable journal before invoking vpnc-script. Observe all enabled network services so interface changes during setup do not defeat capture. Capture only valid DNS output; no heuristic range inference.
- Startup recovery checks actual OpenConnect, openconnect-lite and non-status Lemana connect processes before restoring, and checks again before writes. Missing/failed process evidence blocks mutation. Reused non-VPN PIDs do not block a legitimate old-boot recovery.
- Restore only recorded services in the same network location when current DNS exactly matches the expected VPN list; no-op if already original. If the user changed DNS since the session, preserve it. Keep failed restores for retry.
- Normal hook disconnect preserves custom DNS too: capture the desired value before upstream's unconditional DHCP reset, then restore it only if upstream left empty/expected DNS and no competing tunnel is active. Owner mismatch never consumes another session's journal.
- The disconnect exception requires exact owner and network-location match before upstream invocation. A failed upstream call retains the journal. Tests must exercise custom DNS through upstream's unconditional Empty, partial recovery followed by new connect, location mismatch, and concurrent hook/recovery.
- Snapshotless pre-upgrade leftovers cannot be proven foreign from a 10.x address alone. Automatic repair must skip them and document the limitation, not invent the prior setting.
- CLI startup uses sudo -n and fixed JSON result states. Old/missing helper is detected by a read-only protocol marker, never by executing an old helper with an unknown flag. The app surfaces upgrade/permission/recovery failures and blocks Connect while checking. Recovery never creates a VPN session.
## Task board and parallel scopes
1. Root: DNS wrapper/journal/hook implementation, CLI repair/capability/hook args, installer helper update/template, DNS fixture tests. No icon files edited by root during parallel work.
2. Icon agent (explicitly parallel): ONLY `app/Sources/LemanaVPN/VPNIcons.swift`, `LemanaVPNApp.swift`, `SessionTiming.swift`, `tests/icons.py`. Exact approved family from `artifacts/icon-family-v1/draw_icons.py`: 24-grid, 1.75 stroke, neutral 14% body, 60% bottom-up progress fill and colored perimeter; no demo 8h. No VPNManager/install/DNS/README edits. Return packaging instructions to root.
3. Root after icon merge: startup recovery lifecycle/UI messages in manager and menu, installer icon download, docs and full focused regressions. Credentials/auth/reconnect policy remain unchanged.
4. Read-only correctness/safety/maintainability reviewers; fix blockers; run shell checks, new DNS/icons tests, smoke, auth/config/import/menu/logging checks, Swift release build and diff whitespace check.
## Known limits
No software can restore a pre-upgrade custom DNS value that was never recorded. Unrecognised root journal or network-location changes are preserved for inspection. Native visual quality and actual reboot/network acceptance remain unproven without a separate live-test request.
+13
View File
@@ -0,0 +1,13 @@
# Goal: Two Auth Modes
Use Krypton Execution to execute `docs/goals/two-auth-modes/PLAN.md`.
Core rules:
- Treat PLAN.md as the source plan.
- Preserve intent, ownership, contract, cutover, evidence, and kill criteria.
- Keep exactly two public modes: `manual-keychain` and `bitwarden-auto`.
- Do not introduce a third first-class user mode; old flags/env may exist only as compatibility shims mapped to one of the two modes.
- Manual-keychain stores only LDAP username/password in macOS Keychain and always leaves OTP manual.
- Bitwarden-auto preserves the old personal Bitwarden + Touch ID + full autofill/submit path.
- Capture acceptance evidence from both checkout source and installed local paths.
- Say "implemented but unproven" if installed-path or launch evidence cannot be captured.
+201
View File
@@ -0,0 +1,201 @@
# Two Auth Modes Implementation Plan
**Intent:** Схлопнуть Lemana VPN до двух понятных пользовательских режимов: ручной вход с сохранёнными LDAP login/password в macOS Keychain и старый личный automatic path через Bitwarden + Touch ID + полный autofill/submit.
**Current Behavior:** Checkout уже manual-first, но публичный контракт всё ещё размазан по `manual`, `saved-login`, `saved-totp`, `auto`, совместимым alias-командам и status/menu `available_modes`. Локально установленный `~/bin/vpn-lemanapro.sh` отстаёт от checkout, поэтому старые сборки и приложение могут видеть другую схему.
**Expected Outcome:** Installer, CLI, app menu, README и smoke tests показывают ровно два режима: `manual-keychain` и `bitwarden-auto`. Первый хранит только LDAP username/password в Keychain и всегда оставляет OTP ручным. Второй сохраняет старую рабочую конфигурацию с Bitwarden, Touch ID, permanent 2FA secret, hidden browser, autofill и submit.
**Target-Perspective Output:** Пользователь переустанавливает пакет, выбирает один из двух режимов, видит тот же выбор в `vpn --status` и в LemanaVPN.app. В ручном режиме он может подключиться с подстановкой логина/пароля, каждый раз вводит OTP руками и может обновить логин/пароль из меню. В Bitwarden-режиме работает прежний автоматический путь.
**Truth Owner:** Два repo profile-template файла владеют режимами; `install.sh` выбирает один профиль и пишет активный `~/.config/lemana-vpn/env`; `bin/vpn-lemanapro.sh` исполняет режим; Swift app только читает CLI JSON и запускает CLI.
**Contract Boundary:** Public contract: mode id `manual-keychain|bitwarden-auto`, CLI flags/status JSON/app menu labels. Internal compatibility may still map old flags to a mode, but UI/docs/installer no longer expose four profiles as first-class choices.
**Cutover:** Fresh install defaults to `manual-keychain` and offers `bitwarden-auto` as the old personal advanced mode. Existing env files with `manual`, `saved-login`, `saved-totp`, or `auto` are migrated or normalized to one of the two public modes during install/status.
**Displaced Path:** Displace public `manual`, `saved-login`, `saved-totp`, and `auto` profile matrix. Keep only minimal compatibility aliases where needed, and make them resolve to one of the two modes.
**Value Density:** The smallest high-value slice removes mode ambiguity and stale install drift while preserving the two flows the user actually needs.
**Acceptance Evidence:** Clean temp HOME install output shows only two modes. Source and installed `vpn --status --json` agree on mode/status JSON. Fake launch captures prove manual-keychain uses visible browser, login/password autofill, no TOTP autofill, no submit; bitwarden-auto uses Bitwarden sync, Touch ID when enabled, hidden browser, TOTP autofill, submit. Installed CLI shasum matches checkout and installed app binary contains the updated menu strings.
**Evidence Lane:** Non-live first: shell syntax, smoke tests, Swift build, `git diff --check`, source/installed status JSON and installed binary string checks. Live VPN check only after `vpn --status`, starting with manual-keychain visible flow; skip if connected/connecting unless explicitly approved.
**Kill Criteria:** No installer prompt, README section, app menu, or `available_modes` JSON may present more than two public modes. No fresh install may require Bitwarden. No manual-keychain path may save or require TOTP seed. No bitwarden-auto path may silently degrade into half-configured Keychain-only auto.
**Architecture Slice:** `install.sh`, `bin/vpn-lemanapro.sh`, two new profile-template files, Swift app menu contract, README, smoke tests, installed-path refresh checks.
**Plan Review Gate:** PRE self-review done against Krypton reviewer checklist; external subagent review not run because subagents require an explicit user request in this environment.
## Короткий План
🎯 Цель: оставить ровно два рабочих режима установки и подключения: ручной с Keychain login/password + ручной OTP, и старый Bitwarden/Touch ID automatic flow.
📦 Репозитории: только `/Users/dokril/git/home/lemana-vpn`.
🧩 Модули: `install.sh`, `bin/vpn-lemanapro.sh`, `app/Sources/LemanaVPN`, `templates/`, `tests/smoke.sh`, `README.md`; installed refresh checks для `~/bin/vpn-lemanapro.sh` и `~/Applications/LemanaVPN.app`.
🛠 Основные изменения:
- Создать два profile-template файла в `templates/`: `manual-keychain.env` и `bitwarden-auto.env`.
- Перевести installer на выбор только этих двух режимов; убрать публичные вопросы про `saved-login/saved-totp/auto`.
- В CLI ввести public mode `LEMANA_VPN_MODE=manual-keychain|bitwarden-auto` и нормализовать старые env/flags в один из двух режимов.
- Сделать app menu и status JSON двухрежимными: `Подключить`, `Подключить старым автоматическим способом`, `Изменить логин и пароль`.
- Оставить старые flags/aliases только как compatibility shims, без отдельного третьего режима в UI/docs.
- Переписать smoke tests под два режима и добавить installed-path evidence: `shasum`, `strings`, `vpn --status --json`.
- Сжать README до двух сценариев установки и восстановления.
✅ Проверка: `sh -n install.sh`, `sh -n uninstall.sh`, `bash -n bin/vpn-lemanapro.sh`, `sh -n tests/smoke.sh`, `tests/smoke.sh`, `swift build -c release --package-path app`, `git diff --check`, затем installed checks после `sh install.sh`.
⚠️ Риски: старые env с `saved-totp`/`auto` могут мигрировать не туда; installed CLI/app могут снова отставать; live auto-flow зависит от Bitwarden/Touch ID и permanent 2FA secret.
➡️ Следующий шаг: начать с mode contract и двух template-файлов, затем подключить installer/runtime normalization.
## Architecture Slice
Files to create:
- `templates/manual-keychain.env` - source profile for the normal manual mode.
- `templates/bitwarden-auto.env` - source profile for the old personal automatic mode.
Files to modify:
- `install.sh` - two-mode wizard, env rendering from templates, migration from old flags/env, final next steps.
- `bin/vpn-lemanapro.sh` - public mode parser, compatibility mapping, status JSON, app menu JSON, runtime env for autofill/click/TOTP.
- `app/Sources/LemanaVPN/VPNManager.swift` - two launch modes, menu labels, status decoding if JSON shape changes.
- `app/Sources/LemanaVPN/LemanaVPNApp.swift` - remove multi-mode rendering that exposes old profiles; keep credential edit action.
- `tests/smoke.sh` - replace four-profile assertions with two-mode assertions plus compatibility tests.
- `README.md` - rewrite mode docs around two supported configurations.
- `docs/goals/manual-first-auth-flow/*` - mark superseded or point to this plan to avoid executing the older four-profile plan.
Files to avoid:
- `libexec/lemana-vpn-dns-cleanup` - unrelated to auth mode split.
- `templates/openconnect-lite-config.toml` unless fake launch evidence shows separate openconnect-lite configs are necessary.
- Direct installed `openconnect-lite` source edits; keep using the existing runtime patch path.
Source of truth:
- Repo templates define the two modes.
- Active install writes exactly one selected mode into `~/.config/lemana-vpn/env`.
- Keychain stores LDAP password for `manual-keychain`; Bitwarden remains the upstream source for `bitwarden-auto` and syncs into Keychain at runtime.
Read path:
- `install.sh` reads selected mode and profile template, writes env.
- `bin/vpn-lemanapro.sh` reads env, normalizes legacy fields, checks required credentials for the selected mode, emits JSON/status, launches `openconnect-lite`.
- Swift app reads only `vpn --status --json` and passes a CLI argument for the chosen mode.
Write path:
- `vpn --configure-login` writes or updates username/password only for `manual-keychain`.
- Bitwarden path reads item `LM LDAP`, unlocks through Touch ID/manual master password, then writes password and normalized permanent TOTP secret into Keychain for `bitwarden-auto`.
- Installer never writes the current 6-digit OTP.
Contract boundary:
- Public mode ids: `manual-keychain`, `bitwarden-auto`.
- Public status JSON should expose `mode.id`, `mode.label`, `mode.ready`, `mode.setup_hint`, and a maximum of two user actions.
- Compatibility inputs allowed: `--saved-login`, `--manual-full`, `--manual`, `--auto`, old `LEMANA_VPN_AUTH_PROFILE`, old `LEMANA_VPN_CREDENTIAL_SOURCE`.
Integration points:
- macOS Keychain service `openconnect-lite`.
- Optional Bitwarden CLI item `LM LDAP`.
- Optional Touch ID helper `~/bin/keychain-fingerprint`.
- `templates/openconnect-lite-config.toml` and runtime autofill patches.
- Swift app JSON contract and installed app binary refresh.
Migration/cutover:
- `saved-login` -> `manual-keychain`.
- `manual`/`manual-full` -> `manual-keychain`, but if no saved password exists, status shows setup hint and connection can still offer full manual fallback as diagnostic.
- `saved-totp`/`auto` + `credential_source=bitwarden` -> `bitwarden-auto`.
- `saved-totp`/`auto` + `credential_source=keychain` is no longer a public mode; installer should ask to switch to either `manual-keychain` or `bitwarden-auto`, or keep a hidden compatibility shim for one release with a warning.
Displaced path:
- The old four-profile matrix.
- App `available_modes` rendering multiple profile buttons.
- README/installer wording that teaches `saved-login`, `saved-totp`, and `auto` as equal modes.
Acceptance evidence gate:
- Fresh install dry-run contains only the two human mode names.
- `tests/smoke.sh` proves both launch profiles and legacy compatibility.
- `~/bin/vpn-lemanapro.sh --status --json` matches source behavior after `sh install.sh`.
- `strings ~/Applications/LemanaVPN.app/Contents/MacOS/LemanaVPN` proves updated menu labels are installed.
## Tasks
### Task 1 - Define Two Mode Templates
Scope: create `templates/manual-keychain.env` and `templates/bitwarden-auto.env`; add a tiny renderer/loader in `install.sh`.
Expected output: the selected profile writes one active public mode and its derived legacy compatibility vars.
Verification: `sh install.sh --dry-run --non-interactive --mode manual-keychain` and `--mode bitwarden-auto` show the correct env writes without exposing old profile names.
Acceptance evidence: dry-run output and rendered temp env contents.
Parallel: no; this is the source-of-truth base.
### Task 2 - Normalize Runtime Modes
Scope: update `bin/vpn-lemanapro.sh` to parse `LEMANA_VPN_MODE`, map old env/flags to the two public modes, and derive autofill/TOTP/submit settings.
Expected output: manual-keychain uses visible browser, password autofill, TOTP off, submit off; bitwarden-auto uses Bitwarden sync, TOTP on, hidden browser, submit on.
Verification: fake `openconnect-lite` capture in `tests/smoke.sh`; `bash -n bin/vpn-lemanapro.sh`.
Acceptance evidence: JSON events and capture files for both modes.
Parallel: after Task 1 only.
### Task 3 - Simplify Installer And CLI UX
Scope: installer questions, flags, usage, aliases, final next steps, and legacy migration warning.
Expected output: installer asks one question with two choices, configures login/password for manual-keychain, and reserves Bitwarden/Touch ID setup for bitwarden-auto.
Verification: expect-based interactive smoke and dry-run smoke.
Acceptance evidence: captured installer transcript with no public `saved-login/saved-totp/auto` wording.
Parallel: can run after Task 1 while Task 2 is in progress, but integration proof must wait.
### Task 4 - Update Swift Menu Contract
Scope: `VPNManager.swift` and `LemanaVPNApp.swift`.
Expected output: disconnected menu is two-mode aware and does not render old profile buttons. Credential edit remains a Terminal launcher for `vpn --configure-login`.
Verification: Swift source smoke, `swift build -c release --package-path app`, installed binary `strings` after install.
Acceptance evidence: menu string checks and build output.
Parallel: can run after Task 2 JSON shape is decided.
### Task 5 - Rewrite Docs And Supersede Old Plan
Scope: `README.md`, `docs/goals/manual-first-auth-flow/PLAN.md`, `docs/goals/manual-first-auth-flow/GOAL.md`.
Expected output: docs teach only two modes and point the older four-profile plan to this plan.
Verification: `rg -n "saved-totp|auth_profile=|credential_source=|four-profile|manual-full" README.md docs/goals`.
Acceptance evidence: only compatibility sections mention old names.
Parallel: can run after Tasks 1-3 settle final names.
### Task 6 - Final Verification And Installed Refresh
Scope: run non-live ladder, install locally, compare installed CLI/app, and optionally perform a live manual check if status is safe.
Expected output: checkout and installed paths agree; user can run the chosen mode from Terminal and app.
Verification: `sh -n install.sh`, `sh -n uninstall.sh`, `bash -n bin/vpn-lemanapro.sh`, `sh -n tests/smoke.sh`, `tests/smoke.sh`, `swift build -c release --package-path app`, `git diff --check`, `sh install.sh`, `vpn --status --json`, `shasum bin/vpn-lemanapro.sh ~/bin/vpn-lemanapro.sh`, installed app `strings`.
Acceptance evidence: command outputs plus a note if live VPN was skipped because status was connected/connecting.
Parallel: no; final integration gate.
## PRE Self-Review
Mode: PRE
Verdict: aligned
Findings:
- minor: "two files" could mean runtime shell modules rather than env templates. Smallest correction: execution should confirm with the user before choosing a different file split, but the current plan makes templates the truth owner because the stated pain is reinstall/config drift.
- minor: legacy `saved-totp` + keychain-only users are not part of the requested two modes. Smallest correction: keep one-release compatibility warning or migration prompt rather than deleting the path blindly.
Recommended next gate: execute Task 1 and Task 2 together as the contract slice, then run smoke before touching Swift/docs.
+3
View File
@@ -0,0 +1,3 @@
# Goal: Import Yandex Key OTP locally
Use Krypton Execution to execute `docs/goals/yandex-key-import/PLAN.md`. Preserve the two auth modes and previous dirty work. Reuse Keychain/PyOTP, verify the selected record before writes, and keep secrets out of logs. Capture synthetic end-to-end wizard/launch evidence; no real backup, Keychain, GUI or VPN tests are authorized.
+45
View File
@@ -0,0 +1,45 @@
# Local Yandex Key OTP Import Implementation Plan
**Intent:** Import the user's selected VPN OTP from an official local Yandex Key export and generate it on this Mac without Bitwarden.
**Current Behavior:** Keychain/PyOTP and OTP autofill already exist, but manual-keychain disables OTP and the auto mode uses Bitwarden; there is no backup importer.
**Expected Outcome:** Installer and CLI expose a local wizard: choose file and entry, verify the current phone code, save only the selected secret in Keychain and enable local OTP. Existing manual setup remains the default.
**Target-Perspective Output:** A synthetic export imported through the real wizard yields one verified Keychain write and the persisted local OTP flag; a fake client receives shown browser, OTP on, submit off. Cancellation/invalid input yields no writes or launch.
**Truth Owner:** `openconnect-lite` Keychain account `totp/<LDAP username>` owns the secret; `env` owns `LEMANA_VPN_LOCAL_OTP`; CLI owns status and launch flags. Python helper owns backup parsing and verified import only.
**Contract Boundary:** New CLI `--import-yandex-key`, installer option/question, optional status metadata. No secret in shell arguments, env, output, errors or logs. Only the helper accesses the selected secret in memory.
**Cutover:** Opt-in `LEMANA_VPN_LOCAL_OTP=1` changes manual-keychain OTP from off to on after verification and persistence. Browser stays visible, submit stays off. Existing bitwarden-auto and full-manual behavior remain.
**Displaced Path:** The unconditional manual-mode OTP-off branch becomes a validated optional setting; no new generator, cloud extractor, third auth mode or alternate Keychain store.
**Value Density:** Reuse existing keyring/PyOTP dependencies and existing runtime patches. One helper next to CLI, installed/uninstalled with it.
**Acceptance Evidence:** Deterministic tests of parsing, real PyOTP RFC fixture verification, fake Keychain writes/rollback, CLI dispatch and fake-client capture; status readiness and installer preservation; full automated regression ladder.
**Evidence Lane:** Temporary homes, synthetic public test secrets and stub Keychain only. No actual user export, Keychain, GUI, live VPN, install or uninstall.
**Kill Criteria:** No unsupported OTP silently accepted; no writes before verification; failed activation restores prior Keychain entry; no OTP enabled by mere presence of a saved seed; full-manual never fills OTP.
**Architecture Slice:** `install.sh` -> installed CLI -> sibling `lemana-vpn-import-yandex.py` via OC Python -> existing Keychain + atomic env update -> CLI status/launch -> Swift status. `uninstall.sh` removes the helper. DNS/reconnect/runtime SSO patches and unrelated dirty work are excluded.
**Plan Review Gate:** Requires PRE review before execution.
## Input and lifecycle contract
- Verified plaintext format: JSON array with `name`, `secret`, `techInfo` (OTP URI); primary implementations: [GNOME Authenticator](https://world.pages.gitlab.gnome.org/Authenticator/src/authenticator/backup/yandex.rs.html), [Yandex export help](https://yandex.ru/support/id/ru/authorization/twofa-copy).
- Accept only `otpauth://totp`, SHA1, 6 digits, 30 seconds, valid Base32. Reject duplicate critical parameters, secret mismatch, HOTP/YAOTP and unsupported parameters. Never fetch metadata URLs. Mixed backups may contain unsupported records; validate the selected entry.
- Bound file reading to 4 MiB and entries to 1000; sanitize/cap display names. Require a real terminal at production entry, use hidden input for current phone code, allow EOF/interrupt to cancel without repeated prompts.
- Require saved LDAP username/password first (`vpn --configure-login`). Show destination account and the visible/no-submit behavior. Verify current/adjacent 30-second code locally, then write selected secret and enable manual-keychain/local OTP. Prepare the mode-0600 env replacement before changing Keychain; roll back the old secret if activation fails. Preserve unrelated env settings, no backup copy retained or removed automatically.
- `LEMANA_VPN_LOCAL_OTP` is 0 by default, only 0/1 accepted; invocation environment overrides env file. Reset to 0 in env to return to manual codes while retaining the saved seed. Bitwarden ignores this flag; full-manual ignores it for launch/preflight.
- Status must distinguish local OTP readiness and missing-seed guidance without reading/displaying secrets. Existing older CLI/Swift decoding remains compatible.
## Ordered tasks
1. Root implements Python helper and focused tests (`bin/lemana-vpn-import-yandex.py`, `tests/yandex_import.py`). Read-only explorer/review agents validate format/security. Test synthetic success, wrong code/selection, malformed/unsupported input, EOF, secret-safe output, storage/config failures and rollback.
2. Root integrates CLI flag/config, local preflight, status and launch (`bin/vpn-lemanapro.sh`) plus manual readiness decoding (`VPNManager.swift`). Preserve old command/event shapes where unchanged. Add fake-launch/status/no-TTY tests to `tests/smoke.sh` and focused wizard tests.
3. Root integrates installer question/flag and helper packaging/removal (`install.sh`, `uninstall.sh`), documents workflow and limits (`README.md`). Existing unowned-env preservation retains local OTP across updates; add evidence to `tests/configuration.py`. No extra GUI controls.
4. Read-only correctness/maintainability/POST plan review, fix blockers, then final syntax, wizard/config/logging tests, smoke and Swift release build. Main agent owns edits and integration; independent read-only work can run in parallel.
## Risks
Wrong OTP type/parameters produces silent login failures; unsupported settings must fail before persistence. Wrong selected account is addressed by explicit selection and phone-code verification. File/Keychain errors must not silently switch modes or replace usable credentials. Real macOS Keychain prompts and live SSO remain unverified without a separate current request.
## Completed evidence
- PRE and POST plan/maintainability reviews passed. Security review findings resolved: saved/effective/runtime LDAP binding, strict parameter allowlist, case/whitespace-safe secret masking before label truncation, Keychain readback, and a consistent commit point with interruption handling.
- `tests/yandex_import.py` passes with the existing OC Python. Synthetic RFC TOTP data exercise valid import, mixed exports, unsupported types/parameters, malformed/oversized files, EOF/wrong code, Keychain/config failures, rollback/commit and silent-backend failure.
- Automated PTY drives the actual CLI and sibling helper against a temporary fake Keychain: verified one-account write, unchanged LDAP password, no echoed phone code/secret, persisted setting and ready CLI JSON. Runtime account mismatches and explicit local-OTP override are checked.
- Shell syntax, `tests/smoke.sh`, `tests/configuration.py` (10 checks), `tests/logging.py`, `git diff --check`: PASS. Smoke captures local OTP on, full-manual off, missing seed blocking launch, unchanged Bitwarden flow, installer action/packaging and uninstall removal.
- `swift build -c release --package-path app`: PASS (3.29 s). Restricted run hit compiler cache and nested sandbox restrictions; approved build escalation completed without launching the app.
- No real export, real Keychain writes, real installation, GUI or live VPN acceptance was performed. Abrupt process death/power loss across separate Keychain/filesystem stores remains a documented limitation.