- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
23 KiB
Implementation Plan: Opt-in Bitwarden SSH Agent
Overview
Build the SSH-agent design in docs/ideas/ssh-agent.md as an optional,
disabled-by-default feature. The existing Quickshell panel remains the only
component that invokes bw and owns BW_SESSION; a supervised Rust companion
implements the local SSH-agent protocol, holds private SSH keys only while the
vault is unlocked, and makes signing contingent on a live approval or bounded
process grant. Before any agent work, the ordinary vault read is sanitized
outside QML so unsupported cipher types and sshKey.privateKey cannot enter
the long-lived panel process.
Detailed tasks and checkpoints are tracked in tasks/todo.md. No feature code
should be written until this plan has human approval.
Development Worktree and Live Checkpoints
- Develop on branch
feature/ssh-agentin.worktrees/ssh-agent, based on the latest fetchedorigin/master. Keep the primarymastercheckout free of feature changes. - The enabled Omarchy plugin path
~/.config/omarchy/plugins/io.github.elevate08.qs-bitwarden-climust resolve to this worktree before any live test. - At the end of every task slice, run its focused tests plus the applicable
regression/QML/lint/manifest gates, verify the plugin symlink target, run
omarchy restart shell, confirmomarchy-shell shell ping, summon the Bitwarden panel, and call its non-secretstatusIPC method. - Pause after that live reload so the user can exercise the plugin manually. Do not start the next task until the user confirms the checkpoint or reports issues to fix.
Scope
The first release includes:
- a public-only, read-only SSH-key item experience in the panel;
- Ed25519 and RSA SHA-2 authentication/signing through a stable Unix socket;
- explicit per-signature approval and short per-process grants;
- opt-in unlock-on-demand, safe UWSM client routing, and advisory diagnostics;
- public-key file projection for normal Git SSH-signing configuration;
- an x86_64 GNU helper bundled with the plugin and verified by reproducible CI.
It does not include key creation/import, SSH item editing/cloning, master- password re-prompt keys, forwarding, persistent grants, GPG-agent support, item types 6–8, aarch64, or a Bitwarden SDK/direct service integration.
Architecture Decisions
- Sanitize
bw list itemswith staticjqprograms before QML parses it. QML receives intact types 1–4 and a public-only type-5 projection; all other types fail closed out of the display model. - Keep one panel-owned
bw list itemsread per unlock/sync. When the agent is enabled, a boundedteebranch sends only eligible type-5 private material to a nonce-framed FIFO. Failure of that optional branch must never prevent the panel list from loading. - Use one Rust companion and one stable socket under
$XDG_RUNTIME_DIR. The companion spawns no child processes, receives neitherBW_SESSIONnor unrelated vault data, and exits when its control channel closes. - Make approval, epoch checks, request limits, and final allow/deny decisions authoritative in the companion. Process information is prompt context, not an authentication boundary.
- Treat public-key file export as v1 scope because normal Git SSH signing needs file paths. The planning assumption is that the companion owns this projection from its validated public keystore; Task 4 records or corrects that ownership before implementation proceeds.
- Ship only
x86_64-unknown-linux-gnuinitially. Commit the source, lockfile, pinned toolchain, release bytes, and checksum; compare clean CI output byte for byte before anything reachesmasteror a release.
Dependency Graph
Task 1 sanitized read contract
└── Task 2 panel SSH-key slice
└── Task 3 prerequisites and diagnostics
Task 4 Rust dependency/security decision
└── Task 5 protocol and signing
└── Task 6 epoch keystore
├── Task 7 nonce FIFO loading
└── Task 8 approvals and grants (also depends on Task 5)
└── Task 9 companion lifecycle (also depends on Task 7)
Tasks 3 + 9
└── Task 10 QML supervision
├── Task 11 opt-in setup
└── Task 12 shared vault fan-out (also depends on Tasks 1 + 7)
└── Task 13 vault lifecycle (also depends on Tasks 8 + 9)
├── Task 14 approval UI
└── Task 15 public-key projection
Task 9
└── Task 16 reproducible build
└── Task 17 pull-request gates
Tasks 10 + 16
└── Task 18 bundled-helper validation
└── Task 19 protected release (also depends on Task 17)
Tasks 3 + 11 + 14 + 15 + 19
└── Task 20 documentation and release validation
Task List
Phase 1: Remove the Existing Exposure
- Task 1: Introduce the sanitized vault-read contract
- Task 2: Deliver the public SSH-key panel slice
- Task 3: Enforce SSH support prerequisites
Checkpoint: Safe Panel Baseline
- QML-facing fixture output contains no SSH private-key marker or unknown cipher-type marker.
- SSH keys are public-only and cannot reach generic detail/edit paths.
- All current JavaScript and QML tests pass; the plugin validates and lints at its existing baseline.
- Human review approves the security boundary before Rust work continues.
Phase 2: Prove the Headless Agent Core
- Task 4: Pin the Rust security foundation
- Task 5: Implement bounded SSH protocol signing
- Task 6: Implement the vault-epoch keystore
Checkpoint: Rust Primitives
-
Ed25519 and both RSA SHA-2 modes pass protocol-vector tests.
-
Lock, malformed input, mismatch, and limit paths fail closed.
-
The dependency/zeroization decision is recorded and reviewed.
-
Task 7: Implement nonce-framed FIFO loading
-
Task 8: Authorize signatures with bounded grants
-
Task 9: Complete the supervised companion lifecycle
Checkpoint: Headless Companion
- Disposable-key tests cover identities, signing, multiple clients, approval, grants, lock races, and FIFO rejection.
- The helper serves only its private stable socket, has no vault credential, spawns no process, and exits on control EOF.
- Manual headless authentication and Git SSH-signing smoke tests pass.
- Human review approves proceeding to panel integration.
Phase 3: Integrate the Optional Feature
- Task 10: Establish companion supervision
- Task 11: Deliver opt-in session setup
- Task 12: Feed the companion from the shared vault read
Checkpoint: Optional Data Plane
-
Disabled mode starts no helper and creates no socket, FIFO, or private-key branch.
-
Enabled mode loads keys from the same vault read without delaying or breaking the ordinary list on helper failure.
-
QML remains responsive during blocked SSH clients and helper events.
-
Task 13: Enforce vault lifecycle transitions
-
Task 14: Deliver signing authorization UX
-
Task 15: Project validated public-key files
Checkpoint: End-to-End Feature
- Lock, logout, account change, suspend, screen lock, disable, crash, and Quickshell reload all satisfy the state machine.
- Authentication, commit signing, and multi-commit signing work with no private key on disk.
- Public projections survive lock but clear on logout/account change/disable.
- Human security and usability review approves packaging.
Phase 4: Establish the Artifact Trust Path
- Task 16: Make the release build reproducible
- Task 17: Add read-only pull-request gates
Checkpoint: Candidate Artifact
-
Two clean pinned builds produce identical stripped bytes.
-
Existing, Rust, audit, license, and end-to-end gates pass with no PR secrets or write token.
-
Task 18: Validate the bundled helper at launch
-
[~] Task 19: Protect release provenance -- workflow, environment and tests are in place; the first protected tag run is still outstanding
Checkpoint: Shippable Artifact
- The tracked helper is executable, native-tested, checksum-consistent, protocol-compatible, and byte-identical to the protected build.
- A release produces provenance attestation, SBOM, and dependency/license report with narrowly scoped permissions.
Phase 5: Document and Release
- [~] Task 20: Complete release documentation -- README, CHANGELOG, manifest and the design draft are done; the manual release matrix is not
Checkpoint: Complete
- Every task's acceptance criteria and the project Definition of Done pass.
- Setup, conflict, logout/login, upgrade, verification, and uninstall paths are documented and manually exercised.
- No private key, session token, signed payload, or signature appears in QML state, argv, logs, files, CI artifacts, or test diagnostics.
- Human review approves merge and release.
Verification Strategy
Use focused tests after each task and these full gates at checkpoints:
for test_file in tests/*.test.js; do node "$test_file" || exit 1; done
QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml
mkdir -p /tmp/qs-imports
ln -sfn /usr/share/omarchy/shell /tmp/qs-imports/qs
/usr/lib/qt6/bin/qmllint -I /tmp/qs-imports Panel.qml FormPickerRow.qml
omarchy plugin validate .
cargo fmt --manifest-path agent/Cargo.toml --check
cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings
cargo test --manifest-path agent/Cargo.toml --locked --all-targets
cargo deny --manifest-path agent/Cargo.toml check
The final runtime matrix must include a disposable fixture vault, a fake panel controller, real OpenSSH clients, Git authentication, Git SSH signing, lock during every sensitive transition, helper crash/reload, and both unlock-on- demand modes. CI must never use a real vault or credential.
Parallelization Opportunities
- After Task 3, the panel baseline can remain stable while Tasks 4–9 build the headless Rust core; do not parallelize work that changes the sanitized read contract until Task 1 is merged.
- After Task 9, Task 16's reproducible-build work can run alongside Tasks 10–11 because it consumes the frozen helper interface rather than QML state.
- After Task 13, Tasks 14 and 15 can proceed independently if Task 4 has fixed the public-export owner and the control protocol is frozen.
- Tasks 17 and 18 may proceed in parallel after Task 16, but Task 19 waits for both so release policy matches launch-time validation.
Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| QML receives private SSH material before sanitization | High | Make Tasks 1–3 a standalone prerequisite gate with marker-based real-pipeline tests. |
| Selected Rust key types retain secret clones after lock | High | Fail the Task 4/6 spike if parsed private components cannot be bounded and best-effort-zeroized. |
| A lock races a load, approval, grant, or signature | High | Use vault epochs, an atomic deny linearization point, candidate keystores, bounded acknowledgments, and kill-on-timeout. |
| Same-UID FIFO injection replaces the key set | High | Require a fresh 128-bit load nonce delivered only over the private control pipe and reject duplicate/stale payloads. |
| A slow or broken agent branch stalls the core vault list | High | Bound input/output/time, drain eagerly, supervise one process group, and retry the list once without the optional branch. |
| Process attribution creates false security confidence | Medium | Enforce peer UID; present PID/path/start-time as context only; scope grants narrowly and revalidate at sign time. |
| UWSM setup overwrites another agent configuration | High | Use one plugin-owned atomic file, refuse symlinks/unexpected contents, show conflicts, and require confirmation plus re-login. |
| Bundled binary drifts from source | High | Pin every build input and compare clean release bytes byte-for-byte before merge/release. |
| Fork PR artifact policy becomes impossible to satisfy | Medium | Upload read-only candidates for source-only fork PRs; require the matching bytes before merge to master, not inside the untrusted fork job. |
| Older/self-hosted servers expose partial SSH support | Medium | Enforce the CLI floor, report capability as unconfirmed when appropriate, and document the 2026.8.0 malformed-item fix. |
| Feature complexity degrades the ordinary vault | High | Keep disabled mode inert, isolate helper failure, checkpoint every 2–3 tasks, and run the full existing regression suite throughout. |
Open Questions for Human Review
- Public export scope: Revision 2 says both that public-key export is required in v1 and that it remains later work. This plan follows the stronger end-to-end requirement and includes it in v1. Confirm that choice.
- Public export owner: This plan recommends the companion write the projection from its validated public keystore, using an absolute data path supplied at launch. Task 4 must record the final owner and its path/cleanup contract before implementation.
- Dependency outcome: The Rust crate set, Bitwarden v2 agent status, RFC status, zeroization behavior, and license/audit surface must be re-verified from current primary sources during Task 4; the design draft's review date is not sufficient evidence.
- Release governance: Resolved during Task 19. The protected environment
is
release; its required reviewer is@Elevate08, with self-review permitted because this repository has one maintainer -- the gate exists so that write, OIDC and attestation credentials never come into existence without a deliberate human click, not to simulate a second pair of eyes that does not exist. Deployments are restricted tov*tags. CODEOWNERS names@Elevate08throughout, and calls out/.github/workflows/release.ymlseparately as the only workflow that can write, mint a token, or sign.
Follow-up Plan: Centered SSH Approval Popup
The approved follow-up specification is
docs/ideas/ssh-approval-popup.md. Add a disabled-by-default presentation
choice without changing the companion protocol or authorization rules.
Dependency order
setting contract
-> centered approval surface
-> locked-vault unlock surface
-> documentation and full verification
Architecture decisions
- Keep request and credential state in
Panel.qml; popup components are views that call the same approve, deny, PIN, fingerprint, and password functions. - Use a full-output transparent
PanelWindow,ExclusionMode.Ignore, the Wayland overlay layer, and a brief Exclusive-to-OnDemand focus prime, matching Omarchy's centered transient surfaces. - Reuse
SshApprovalScreen.qmlbetween both presentation modes so the key, process, grant, loading, and deadline semantics cannot drift. - Keep account login in the full panel. The centered prompt handles only the signed-in locked/unlocked states involved in SSH requests.
Risks and mitigations
| Risk | Mitigation |
|---|---|
| Popup accidentally becomes a second authorization authority | It only calls existing Panel.qml request functions; helper checks remain unchanged. |
| PIN/fingerprint results are discarded because the anchored panel is closed | Define one transient-auth-surface predicate used by all unlock acceptance checks. |
| The invisible experience leaves a password or pending process behind | Dismissal denies the request and runs popup-specific credential/prewarm cleanup. |
| A request label injects markup | Every request-derived Text remains Text.PlainText. |
| Overlay traps input after the request ends | Visibility and Wayland keyboard focus bind directly to the live pending request. |
Implementation Plan: Colorized Menu-Bar Icon
Overview
Add an opt-in colorizeIcon boolean setting to the Bitwarden panel. When
enabled, the primary menu-bar shield follows Omarchy's live Color.accent
theme color; when disabled, it keeps the current bar foreground color. The
locked padlock, missing-dependency badge, and urgent/error indicators remain
unchanged so color personalization does not erase status meaning.
The existing settings renderer already turns boolean schema entries into keyboard-operable toggle rows. No custom color picker, palette model, new dependency, or arbitrary color persistence is required.
Architecture Decisions
- Store only
colorizeIcon: boolean; derive the actual color fromColor.accentat render time so theme changes are inherited automatically. - Default the setting to
falseso existing installations retain their current appearance. - Apply the setting only to the primary shield glyph in
shieldIconComp. Leave the padlock and setup/error badges on their existing bindings. - Put the row in the General settings group, using the existing schema-driven
toggle and
omarchy bar setpersistence path. - Treat malformed external values as
false, using the existing strict boolean-setting behavior.
Dependency Graph
manifest default/schema + model schema
│
├── settings persistence/value lookup tests
│
└── Panel color binding + schema-driven General toggle
│
└── QML/static checks + runtime/theme verification
Task List
Phase 1: Settings Contract
- Task 1: Add the colorization setting contract
Checkpoint: Settings Contract
colorizeIconexists in bothmanifest.jsonandBitwardenModel.jswith boolean type and afalsedefault.- Valid booleans round-trip through the existing writer, while malformed
values resolve to
false. - Focused settings/model tests pass before UI wiring begins.
Phase 2: Vertical UI Slice
- Task 2: Wire the theme-accent shield and General toggle
Checkpoint: Colorized Icon Behavior
- With the setting off, the primary shield still uses the bar foreground.
- With the setting on, the primary shield uses
Color.accent. - Lock/setup/error badges retain their existing foreground or urgent colors.
- The settings row is keyboard-operable and persists through the existing shell reload path.
Phase 3: Verification and Documentation
- [~] Task 3: Add regression coverage and document the setting
Checkpoint: Complete
- Focused and full JavaScript tests pass.
- QML tests/lint and plugin validation pass where the Omarchy environment is available.
- Manual verification covers both toggle states, a theme accent change, locked state, setup-required state, and an error/urgent state.
- README or user-facing feature documentation explains the toggle and its theme-derived behavior.
- Human review approves the implementation before merge; no commit or push is performed automatically.
Task Details
Task 1: Add the colorization setting contract
Description: Register colorizeIcon as a General boolean setting in the
manifest and model schema, expose its default through the widget defaults, and
ensure the existing settings value/read/write paths recognize it without
special-case persistence code.
Acceptance criteria:
- The manifest and model declare the same
colorizeIconkey, boolean type, label, description, andfalsedefault. boolSetting("colorizeIcon", true)returns true; malformed values such as strings and numbers fall back to false.- The setting is grouped under General and included in visible settings without changing existing group ordering or setting semantics.
Verification:
- Tests pass:
node tests/setup-settings.test.js - Tests pass:
node tests/lock-state.test.js - Static check confirms manifest/model schema parity.
Dependencies: None
Files likely touched:
manifest.jsonBitwardenModel.jstests/setup-settings.test.jstests/lock-state.test.js
Estimated scope: Medium: 3–4 files
Task 2: Wire the theme-accent shield and General toggle
Description: Add the root property that reads colorizeIcon, use it only
for the primary shield glyph in the menu-bar icon component, and rely on the
existing schema-driven settings delegate to render and persist the toggle.
Add focused static assertions for the binding and for the unchanged status
badge color paths.
Acceptance criteria:
colorizeIconis read from the live setting with a false-safe default.- The primary shield resolves to
Color.accentwhen enabled and the existing bar foreground when disabled. - The padlock, missing-tool badge, and urgent/error color bindings are not redirected through the new preference.
Verification:
- Tests pass:
node tests/settings-screen.test.js - Focused source assertions pass for shield color precedence and badge independence.
- QML lint passes for
Panel.qmlwith the repository's Omarchy import path.
Dependencies: Task 1
Files likely touched:
Panel.qmltests/settings-screen.test.js
Estimated scope: Small: 1–2 files
Task 3: Add regression coverage and document the setting
Description: Complete the feature's regression matrix and user-facing documentation. Verify that persistence survives the shell reload path and that the selected accent is inherited from the active theme while status indicators remain recognizable.
Acceptance criteria:
- Tests cover default-off behavior, enabling/disabling through the settings path, malformed persisted values, and preservation of badge colors.
- User-facing documentation says the toggle follows the active Omarchy theme accent and does not offer arbitrary color selection.
- No unrelated panel colors or status semantics change.
Verification:
- Full JavaScript suite passes:
for test_file in tests/*.test.js; do node "$test_file" || exit 1; done - QML suite passes:
env -u DISPLAY -u WAYLAND_DISPLAY -u QT_QPA_PLATFORMTHEME QML_XHR_ALLOW_FILE_READ=1 QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml - Plugin validation passes:
omarchy plugin validate . - Manual runtime check confirms both toggle states and theme-derived color behavior when the desktop environment is available.
Dependencies: Task 2
Files likely touched:
README.mdor the relevant feature documentationtests/setup-settings.test.jstests/settings-screen.test.jstests/lock-state.test.js
Estimated scope: Medium: 3–4 files
Risks and Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| Accent color has poor contrast on a supported theme | Medium | Check light/dark themes manually; retain the existing bar foreground as the safe default and do not alter urgent badges. |
| The setting is added to one schema but not the other | Medium | Keep manifest/model parity assertions in the focused settings tests. |
| The setting accidentally recolors status badges | High | Limit the binding change to the primary shield Text and assert badge bindings remain independent. |
| Shell reload does not refresh the bar icon immediately | Low | Verify the existing omarchy bar set hot-reload behavior; do not add a second persistence mechanism. |
Open Questions
- Confirm final user-facing label: Colorize menu-bar icon versus Use theme accent for icon. The plan assumes the former.
- Confirm which user-facing documentation file should receive the short setting
note;
README.mdis the default unless project conventions preferdocs/features.md.