- 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
47 KiB
Task List: Opt-in Bitwarden SSH Agent
Source design: docs/ideas/ssh-agent.md, revision 2 (2026-08-26).
Every task must satisfy its acceptance criteria plus the repository-wide
Definition of Done: focused and regression tests, runtime verification, lint
and formatting, scoped changes, documentation for user-visible behavior,
security review for sensitive paths, and human approval before merge/release.
Each task also ends with the live checkpoint protocol in tasks/plan.md: the
dev-linked plugin is restarted and opened for user testing before work begins
on the next task.
Task 1: Introduce the sanitized vault-read contract
Description: Add an out-of-process, positive-allowlist jq transform for
the ordinary bw list items result without switching the live panel yet. It
must emit one bounded object containing intact types 1–4 and a public-only
projection of type 5, while excluding types 6+ and keeping SSH private keys out
of every QML-facing byte.
Acceptance criteria:
- Fixture markers prove types 1–4 remain intact, type 5 exposes only the approved public fields, and no type-5 private marker or type-6+ marker reaches QML-facing output.
- A type 1–4 object carrying a top-level
sshKeysubtree rejects the whole read rather than mutating an ordinary object or risking a private-key leak. - Raw input and QML output have producer-side caps; malformed, truncated, or filter-failing input returns an error rather than a partial model.
- The filter is static, receives values only through safe
jq --arginputs, and preserves the repository's no-secret-in-argv policy.
Verification:
- Tests pass:
node tests/ssh-items.test.js - Regression suite passes:
for test_file in tests/*.test.js; do node "$test_file" || exit 1; done - Manual check: run the command against marker fixtures and inspect both stdout and failure exit codes.
Dependencies: None
Files likely touched:
BitwardenModel.jstests/ssh-items.test.js
Estimated scope: Small: 2 files
Task 2: Deliver the public SSH-key panel slice
Description: Switch the panel's item load to the sanitized envelope and add type-5 SSH keys as public-only, read-only items. They participate in All, Favorites, global search, counts, and type filtering, but cannot fall through to generic detail fetching, creation, editing, or cloning.
Acceptance criteria:
- Existing types render from
items, while public SSH keys render fromsshKeyswith name, public key, fingerprint, organization/folder, favorite, and re-prompt availability only. - SSH keys appear in normal list/search/count flows and contextual suggestions remain login-only.
- Generic
bw get item, create, edit, and clone paths reject type 5; a read-only detail view never displays or requests private material.
Verification:
- Tests pass:
node tests/ssh-items.test.js && node tests/items.test.js - QML tests pass:
QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml - Manual check: open a fixture vault containing types 1–8 and inspect list, filters, search, counts, detail, and disabled actions.
Dependencies: Task 1
Files likely touched:
BitwardenModel.jsPanel.qmltests/ssh-items.test.jstests/items.test.jstests/qml/tst_ssh_items.qml
Estimated scope: Medium: 5 files
Task 3: Enforce SSH support prerequisites
Description: Make jq a required dependency, establish the supported
Bitwarden CLI floor, and expose accurate diagnostics for old CLI versions,
unconfirmed server capability, and the pre-2026.8 malformed-SSH-item hazard.
Ordinary non-SSH vault behavior must remain usable whenever possible.
Acceptance criteria:
- First-run dependency checks report missing
jqas required without changing optional dependency semantics. - SSH UI/agent setup is unavailable below
bw2025.1.2 and reports unconfirmed capability separately from a confirmed empty key set. - A whole-list failure attributable to the known upstream SSH-item issue names the 2026.8.0 fix without exposing raw CLI output.
Verification:
- Tests pass:
node tests/setup-settings.test.js && node tests/ssh-items.test.js - Plugin validates:
omarchy plugin validate . - Manual check: exercise missing
jq, oldbw, empty supported vault, and malformed-item fixture diagnostics.
Dependencies: Task 2
Files likely touched:
BitwardenModel.jsPanel.qmlmanifest.jsontests/setup-settings.test.jstests/ssh-items.test.js
Estimated scope: Medium: 5 files
Checkpoint: Safe Panel Baseline (Tasks 1–3)
- Full JavaScript and QML suites pass.
- Qt6 lint remains at the documented baseline and the manifest validates.
- Marker tests prove no SSH private key or unknown cipher reaches QML.
- Human review approves the prerequisite boundary.
Task 4: Pin the Rust security foundation
Description: Time-box the dependency spike, scaffold the Rust crate with an exact toolchain and lockfile, and record the selected protocol, crypto, zeroization, async, and Linux-runtime dependencies. The decision also fixes public-key export ownership and documents rejected/deprecated alternatives.
Acceptance criteria:
- Current primary sources confirm maintenance status, required Ed25519/RSA support, peer credentials, licenses, audit surface, and protocol hooks.
- The decision identifies how parsed private components are zeroized or explicitly stops the project if credible lock-time erasure is impossible.
cargo test --lockedbuilds a minimal headless crate onx86_64-unknown-linux-gnuwith no vault/network dependency.
Verification:
- Tests pass:
cargo test --manifest-path agent/Cargo.toml --locked - Build succeeds:
cargo build --manifest-path agent/Cargo.toml --locked - Manual check: security review the ADR, dependency tree, licenses, enabled features, and public-export ownership decision.
Dependencies: Task 3
Files likely touched:
agent/Cargo.tomlagent/Cargo.lockagent/rust-toolchain.tomlagent/src/lib.rsdocs/decisions/0001-ssh-agent-dependencies.md
Estimated scope: Medium: 5 files
Task 5: Implement bounded SSH protocol signing
Description: Implement the allowlisted agent frame decoder and a signing slice for request-identities and sign-request. Support Ed25519 and RSA SHA-256/SHA-512 flags, reject all mutation/forwarding/unknown operations, and check frame lengths before allocating.
Acceptance criteria:
- OpenSSH-compatible vectors pass for identity listing, Ed25519, RSA SHA-256, and RSA SHA-512 signatures.
- Frames over 256 KiB, truncation, invalid lengths/UTF-8, mutation requests, and unknown extensions fail with normal bounded agent failures.
- No error or debug path emits private keys, payloads, signatures, or raw parser data.
Verification:
- Tests pass:
cargo test --manifest-path agent/Cargo.toml --locked --test protocol - Lint passes:
cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings - Manual checkpoint approved: the existing panel remained functional after
the protocol slice. The real
ssh-add -Land disposable-key smoke test remains part of the Task 9 socket-harness checkpoint.
Dependencies: Task 4
Files likely touched:
agent/src/protocol.rsagent/src/signing.rsagent/src/lib.rsagent/tests/protocol.rs
Estimated scope: Medium: 4 files
Task 6: Implement the vault-epoch keystore
Description: Add the single-owner private keystore and explicit vault state machine. Loads build a bounded candidate set, validate derived public material, deduplicate identities, and swap atomically; lock first denies authorization, then drops private material while retaining only the allowed public cache.
Acceptance criteria:
- Candidate loads enforce 128 keys, 64 KiB per PEM, and 8 MiB total; one bad key is skipped, while framing/schema/limit failures reject the whole candidate without mixing old and new private keys.
- Public blob and derived fingerprint must match vault metadata; duplicates advertise once and re-prompt items never enter the private keystore.
- Lock/epoch tests prove no authorization crosses after the atomic deny point and secret containers are dropped/zeroized without long-lived clones.
Verification:
- Tests pass:
cargo test --manifest-path agent/Cargo.toml --locked --test keystore - Formatting passes:
cargo fmt --manifest-path agent/Cargo.toml --check - Manual check: private owners have no clone/revealing-debug path; all seven load/lock/logout tests pass under Heaptrack and Valgrind. Both tools attribute their small exit-time retention only to Rust/loader test-runtime bookkeeping, with no project or crypto allocation leak path.
Dependencies: Task 5
Files likely touched:
agent/src/keystore.rsagent/src/state.rsagent/src/lib.rsagent/tests/keystore.rs
Estimated scope: Medium: 4 files
Checkpoint: Rust Primitives (Tasks 4–6)
- Protocol, crypto, mismatch, limit, and lock tests pass.
- Dependency and secret-memory findings support the stated lock semantics.
- Human review approves continuing the headless implementation.
Task 7: Implement nonce-framed FIFO loading
Description: Create the private runtime directory and key-load FIFO, keep
the FIFO open O_RDWR, and drain one nonce-framed candidate load into secret
buffers under hard size/time limits. Prove the intended tee backpressure and
exit-status behavior without letting the companion spawn production children.
Acceptance criteria:
- Runtime directory/FIFO ownership, type, and modes are verified; stale, symlinked, wrong-owner, and wrong-type paths are refused safely.
- Missing, stale, duplicate, truncated, malformed, or nonce-mismatched payloads fail the whole load and never publish a partial key set.
- Stress tests prove bounded draining and document when the two-read
fallback must replace the preferred
teedesign.
Verification:
- Tests pass:
cargo test --manifest-path agent/Cargo.toml --locked --test load - Build succeeds:
cargo build --manifest-path agent/Cargo.toml --locked - Manual check: run FIFO/
teetests with slow readers, branch failure, duplicate writers, and the full 8 MiB limit.
Dependencies: Task 6
Files likely touched:
agent/src/load.rsagent/src/runtime.rsagent/src/lib.rsagent/tests/load.rstests/ssh-agent-pipeline.test.js
Estimated scope: Medium: 5 files
Task 8: Authorize signatures with bounded grants
Description: Make the companion authoritative for request correlation, peer context, approvals, and grants. Enforce peer UID, bounded queues/deadlines, single-use approval, and process-scoped grants keyed by PID plus start time and executable path, with a final epoch/state/key check immediately before signing.
Acceptance criteria:
- At most four sign requests exist, each expires within 30 seconds and is cancelled on disconnect; late/duplicate/old-epoch approvals are rejected.
- Grants are capped at 900 seconds, scoped to one key/live process, and revoked by every specified lifecycle event or identity mismatch.
- Same-UID peer enforcement and PID-reuse/re-exec tests pass while prompt metadata remains explicitly non-authoritative.
Verification:
- Tests pass:
cargo test --manifest-path agent/Cargo.toml --locked --test approvals - Lint passes:
cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings - Manual checkpoint approved with the existing panel intact. Headless approve-once, grant, expiry, re-exec, PID reuse, overflow, disconnect, and lock-at-final-check races.
Dependencies: Tasks 5 and 6
Files likely touched:
agent/src/approvals.rsagent/src/peer.rsagent/src/server.rsagent/src/lib.rsagent/tests/approvals.rs
Estimated scope: Medium: 5 files
Task 9: Complete the supervised companion lifecycle
Description: Assemble the headless binary around the stable socket,
versioned NDJSON control channel, keystore, and approvals. Enforce singleton
startup with flock, harden process dump behavior before secrets arrive, use
bounded concurrent tasks/channels, and close the signing gate on EOF or protocol
mismatch.
Acceptance criteria:
- The helper creates a mode-0600 socket in the private runtime directory,
holds the singleton lock, advertises a versioned
ready, and rejects a concurrent stale-instance race. - Control lines over 64 KiB, wrong versions/types, full channels, slow clients, or stdin EOF fail closed without blocking lock processing.
- The release process sets
RLIMIT_CORE=0/PR_SET_DUMPABLE=0, spawns no children, needs noPATH/HOME/vault credential, and cleans runtime paths.
Verification:
- Tests pass:
cargo test --manifest-path agent/Cargo.toml --locked --test lifecycle - Build succeeds:
cargo build --manifest-path agent/Cargo.toml --locked - Manual/real-client checkpoint: disposable-key signing with the real
OpenSSH
ssh-keygen -Y signclient, multiple clients, control EOF, singleton restart, and runtime cleanup pass; the existing panel remains intact after its live reload. Full panel-managed authentication begins after Task 10 adds supervision.
Dependencies: Tasks 7 and 8
Files likely touched:
agent/src/main.rsagent/src/control.rsagent/src/runtime.rsagent/src/server.rsagent/tests/lifecycle.rs
Estimated scope: Medium: 5 files
Checkpoint: Headless Companion (Tasks 7–9)
- All Rust tests, format, Clippy, and initial dependency audit pass.
- Real OpenSSH/Git smoke tests pass with disposable keys and a fake panel.
- No vault credential or production child process enters the helper.
- Human review approves the control protocol and threat boundary.
Task 10: Establish companion supervision
Description: Add an inert-by-default Quickshell supervisor that launches a development helper by absolute plugin-relative path, keeps stdin open, parses NDJSON asynchronously from startup, performs the version handshake, applies capped restart backoff, and isolates helper errors from the ordinary vault.
Acceptance criteria:
- Disabled mode starts nothing; enabled mode uses a tracked non-detached
Process, minimal environment, absolute path, and compatible handshake. - Quickshell never waits synchronously; overlong/malformed output, EOF, mismatch, or crash closes the signing gate and reaches a bounded error state.
- A crash loop stops restarts and leaves login, unlock, list, copy, sync, edit, Send, and generator flows usable.
Verification:
- Tests pass:
node tests/ssh-agent-control.test.js - QML tests pass:
QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml - Manual check:
tests/ssh-agent-control.test.jsdrives the real reducer against real child processes -- fake helpers that answer the handshake, stall silently, emit non-JSON, emit an oversized line, close stdin, and die at once -- plus the real helper binary, which completes the v1 handshake with onlyXDG_RUNTIME_DIR, reports paths under that directory, and cleans up its socket when stdin closes. Live: the shell restarted,omarchy-shell shell pinganswered, the panel opened, andstatusreturnedlockedwith no helper process, socket, or FIFO created in the default disabled mode.
Dependencies: Tasks 3 and 9
Files likely touched:
BitwardenModel.jsPanel.qmltests/ssh-agent-control.test.jstests/qml/tst_ssh_agent.qml
Estimated scope: Medium: 4 files
Task 11: Deliver opt-in session setup
Description: Add the three SSH-agent settings and explicit disabled,
enabled, and error states. Implement the user-confirmed UWSM environment
fragment lifecycle and report SSH_AUTH_SOCK only as advisory terminal-routing
diagnostics, never as a condition for running the helper.
Acceptance criteria:
sshAgentEnabled=false,sshAgentUnlockOnDemand=false, and a clampedsshAgentApprovalWindowSec=120default are consistent across manifest, model schema, and settings UI.- Managed UWSM setup/removal uses safe parent creation, atomic mode-safe writes, symlink refusal, unexpected-content refusal, conflict confirmation, and explicit logout/login guidance.
- Diagnostics distinguish matching, elsewhere, and unset client sockets and print the terminal check without gating companion startup.
Verification:
- Tests pass:
node tests/ssh-agent-setup.test.js && node tests/setup-settings.test.js - Plugin validates:
omarchy plugin validate . - Manual check:
tests/ssh-agent-setup.test.jsexercises the real scripts against real temporary homes -- fresh setup, an idempotent rewrite, removal, a hand-written fragment, a symlink over a real file, a directory at the path, and an unset HOME -- asserting in each case that foreign content and symlink targets are left byte-identical. Another agent, unset socket, and matching socket are covered by the diagnostic cases. Live: the shell restarted,omarchy-shell shell pinganswered, the panel and its settings screen opened, and with the feature off no helper, socket, FIFO, or routing file exists. A new graphical login is the user's to exercise; nothing in this task writes the fragment without an explicit click.
Dependencies: Task 10
Files likely touched:
manifest.jsonBitwardenModel.jsPanel.qmltests/ssh-agent-setup.test.js
Estimated scope: Medium: 4 files
Task 12: Feed the companion from the shared vault read
Description: Extend the single sanitized vault read with the optional
tee/FIFO branch. Coordinate key_load_begin/key_load_end, fresh load IDs,
process-group supervision, and a one-time retry without the agent branch so an
optional load can never break the ordinary item list.
Acceptance criteria:
- Enabled loads send only eligible type-5 key fields and matching nonce to the FIFO while QML stdout still contains neither private nor unrelated markers; disabled loads have no private branch at all.
- Whole-pipeline, branch, helper, timeout, cap, or validation failure leaves no partial private set and retries the core list once without the branch.
- Unlock/sync/startup use one successful
bw list itemsread, and a lock can terminate/reap the entirebw/cap/tee/jqprocess group.
Verification:
- Tests pass:
node tests/ssh-agent-pipeline.test.js && cargo test --manifest-path agent/Cargo.toml --locked --test load - Regression suite passes:
for test_file in tests/*.test.js; do node "$test_file" || exit 1; done - Manual check:
tests/ssh-agent-pipeline.test.jsruns the real pipeline against a real FIFO for a missing FIFO, a regular file squatting the path, an undrained FIFO, an absent nonce, malformed/truncated/non-array input, and abwthat exits nonzero after a valid document -- the item list survives every one. Its end-to-end case drives the real companion with a disposable key and confirmsssh-add -Llists exactly the key the vault held. Helper death during a stop was found to leave the socket and FIFO behind and is now fixed; see the graceful-shutdown note below.
Dependencies: Tasks 1, 7, and 10
Files likely touched:
BitwardenModel.jsPanel.qmltests/ssh-agent-pipeline.test.jsagent/tests/load.rs
Estimated scope: Medium: 4 files
Checkpoint: Optional Data Plane (Tasks 10–12)
- Disabled mode is inert and core panel regressions pass.
- Enabled mode loads from one read with bounded fallback behavior.
- QML responsiveness is verified under blocked clients and failed helpers.
- Human review approves the opt-in and data-minimization behavior.
Task 13: Enforce vault lifecycle transitions
Description: Wire the companion state machine into unlock, remembered-
session startup, sync, lock, logout, account change, screen lock, suspend,
disable, and panel shutdown. Lock must atomically deny first, cancel pending
work, clear grants/private keys, and enforce the two-second acknowledgment kill
fallback without delaying bw lock.
Acceptance criteria:
- Every lifecycle path advances the epoch and produces the specified public cache, private cache, grants, pending requests, and process state.
- No signature response from the previous epoch returns after lock acknowledgment; timeout kills/reaps the helper while the vault lock proceeds.
- Starting beside a remembered unlocked session performs an initial key load, while logout/account change/disable clear public projections too.
Verification:
- Tests pass:
node tests/ssh-agent-lifecycle.test.js && node tests/lock-triggers.test.js && node tests/lock-state.test.js - Rust tests pass:
cargo test --manifest-path agent/Cargo.toml --locked --test lifecycle - Manual check: live, with a real vault -- a shell restart into a
keyring-remembered unlocked session loaded keys (found and fixed a race
where the handshake and the first
bw statuscould each be second), a lock dropped the private set while the helper survived its acknowledgment, and disabling stopped the helper and emptied the runtime directory. Screen lock and suspend route through the same lock path. Logout, account switch, and the locked-with-cache identity listing are covered end to end over the real socket byagent/tests/lifecycle.rs::a_locked_vault_still_lists_identities_but_refuses_to_sign. NOT yet confirmed live: the locked-with-cache listing needs an unlocked vault, and the approval/signing/grant lock races belong to Task 14.
Dependencies: Tasks 8, 9, 10, and 12
Files likely touched:
Panel.qmltests/ssh-agent-lifecycle.test.jstests/lock-triggers.test.jstests/lock-state.test.js
Estimated scope: Medium: 4 files
Task 14: Deliver signing authorization UX
Description: Add one-at-a-time approval UI, the opt-in unlock-on-demand flow, sanitized process/key context, request cancellation, cooldown, live grant status, individual/all revoke controls, and panel focus rules that never prompt over screen lock.
Acceptance criteria:
- Prompts clearly offer deny, approve once, and process grant only when enabled; they show key/process/forwarding context without overstating PID authority.
- Locked cached keys prompt at sign time; no-cache identity listing prompts only when unlock-on-demand is enabled, with coalescing and denial cooldown.
- Four-request/deadline/disconnect behavior is visible and bounded, and live grants update/revoke without freezing or exposing secret control data.
Verification:
- Tests pass:
node tests/ssh-agent-ui.test.js - QML tests pass:
QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml - Manual check: live, against a real vault and real OpenSSH clients --
git pushauthentication,ssh-keygen -Y sign, repeated signed commits riding one grant, approving during a load, a dismissed unlock, request timeout, and client disconnect. Found and fixed three defects no automated test caught: the prompt never rendered (opening the panel reset the screen after it was set), timeouts never fed the cooldown, and the cooldown failed silently. Two design deviations were recorded rather than made quietly: docs/decisions/0002-grant-scope.md and docs/decisions/0003-request-deadline.md.
Dependencies: Tasks 8, 10, and 13
Files likely touched:
Panel.qmltests/ssh-agent-ui.test.jstests/qml/tst_ssh_agent.qml
Estimated scope: Medium: 3 files
The two-read fallback is required if the agent branch cannot drain and acknowledge a complete nonce-matching payload within the panel pipeline's deadline, if its FIFO write fails, or if process-substitution status cannot be correlated with the candidate acknowledgment. In every fallback case the panel read remains authoritative and the failed candidate is discarded before a separate bounded agent-only read is attempted.
Task 15: Project validated public-key files
Description: Export only the companion's validated public identities to a private plugin data directory, using deterministic hostile-name handling and collision-safe filenames. Keep the projection across vault lock, refresh it atomically per epoch, and clear it on logout, account change, or disable.
The panel writes the files from the validated public set the companion reports;
the companion stays out of the filesystem. See
docs/decisions/0001-ssh-agent-dependencies.md.
Acceptance criteria:
- One mode-0600
.pubfile per advertised key exists inside a mode-0700 directory; symlinks, wrong owners/types, collisions, and unexpected files cannot redirect or overwrite output. - Projection updates are atomic, survive lock, and clear on logout/account change/disable without ever writing private material.
- Real Git SSH signing and
IdentityFile/IdentitiesOnlyflows work from the exported paths using disposable keys.
Verification:
- Tests pass:
node tests/ssh-agent-export.test.js - Manual check: live, against the real vault. The projection is a 0700
directory holding one 0600
.pubper advertised key with correct OpenSSH content. A commit signed withgpg.format=ssh,user.signingkeypointing at an exported path, and a generated allowed-signers file verifies:git verify-commitreports a good ED25519 signature andgit log %G?reportsG. Commits made earlier through the locked-vault unlock-then-approve path verify the same way.
Dependencies: Tasks 4, 6, 9, and 13
Files likely touched:
BitwardenModel.jsPanel.qmlagent/src/control.rstests/ssh-agent-export.test.js
Estimated scope: Medium: 4 files
Checkpoint: End-to-End Feature (Tasks 13–15)
- Lifecycle, UI, export, full Rust, full JavaScript, and QML suites pass.
- Real authentication and signing work without a private key on disk.
- Security review confirms private material paths and final authorization.
- Human usability/security review approves packaging.
Task 16: Make the release build reproducible
Description: Define a digest-pinned x86_64 GNU build environment and one local/CI build entry point using the committed lockfile/toolchain, fixed release features, path remapping, deterministic stripping, and a byte-comparison mode. Produce separate debug symbols only as temporary artifacts.
Acceptance criteria:
- Two clean builds from distinct absolute work paths emit identical stripped helper bytes and stable checksums.
- Build inputs cover toolchain, container, linker/strip tools, target, flags, release profile, source path, and Cargo registry path.
- The script refuses unlocked dependencies or an unsupported target and reports source/binary drift without modifying the repository.
Verification:
- Tests pass:
node tests/ssh-agent-artifact.test.js - Build succeeds:
scripts/build-agent.sh --verify-reproducible-- green in CI, which runs it inside the pinned image. It refuses locally for want of that image, by design, and--explainsays so without building. - Manual check: done in CI rather than locally, this machine having no
usable container runtime. Two builds from
/tmp/*/path-oneand/tmp/*/a-considerably-longer-second-pathproduced identical bytes: sha256 7f4c38d405adf16504ea7029896365c4081b0e154a4fc1755cd624c12503e816. Built under rustc 1.98.0 and GNU ld 2.40 in the pinned Debian bookworm image -- the host's ld 2.47 would have produced different bytes, which is what the image exists to prevent.readelf/lddinspection of the committed artifact belongs with Task 18, which is where a binary is first committed.
Dependencies: Task 9
Files likely touched:
.github/agent-build.Dockerfileagent/.cargo/config.tomlscripts/build-agent.shtests/ssh-agent-artifact.test.js
Estimated scope: Medium: 4 files
Task 17: Add read-only pull-request gates
Description: Add least-privilege CI for existing tests, Rust quality, dependency/license policy, native target execution, disposable-key end-to-end tests, candidate artifacts, and conditional tracked-binary comparison. Fork PRs must remain useful without receiving secrets or repository write permission.
Acceptance criteria:
- PR jobs default to
contents: read, use no secrets/write token, pin every third-party action by full SHA, and upload but never commit candidates. - CI runs JavaScript/QML, fmt, Clippy, Rust tests, native E2E, RustSec, license/source/duplicate policy, and reproducible build checks.
- Source-only fork PRs report candidate drift without impossible blocking;
bin/changes and merges tomasterrequire exact clean-build bytes.
Verification:
- Policy passes:
cargo deny --manifest-path agent/Cargo.toml check - Workflow passes: green on the same-repository feature branch across all three jobs. The fork path is implemented and reviewed but NOT exercised -- it needs an actual fork PR, which cannot be raised against one's own repository. Worth running once before this reaches master.
- Manual check: permissions are
contents: readonly, every action is pinned to a full commit SHA with its version in a trailing comment, nosecrets.reference appears, the candidate is uploaded and never committed by CI, and the logs carry no key material. Asserted bytests/ssh-agent-artifact.test.jsas well as read by eye.
Dependencies: Task 16
Files likely touched:
.github/workflows/ci.yml.github/dependabot.ymldeny.toml.github/CODEOWNERS
Estimated scope: Medium: 4 files
Checkpoint: Candidate Artifact (Tasks 16–17)
- Reproducibility and every read-only PR gate pass.
- Fork contribution and tracked-byte policies are both workable.
- Human supply-chain review approves bundling the artifact.
Task 18: Validate the bundled helper at launch
Description: Add the tracked x86_64 GNU helper and checksum, then make the panel validate architecture/format, executable mode, checksum consistency, self-test, semantic/control versions, and handshake before enabling SSH-agent behavior. Failures must disable only this optional feature.
Acceptance criteria:
- The repository contains executable release bytes and a matching checksum produced by Task 16, without Git LFS or runtime download.
- Missing, corrupt, stale, wrong-architecture, non-executable, self-test- failing, or protocol-mismatched helpers reach clear bounded diagnostics.
- Every validation failure leaves the rest of the Bitwarden plugin usable and creates no agent socket/FIFO/private branch.
Verification:
-
Tests pass:
node tests/ssh-agent-bundle.test.js -
Artifact check passes:
scripts/build-agent.sh --compare-tracked-- green in CI: tracked and freshly built both 3b36e17fcbca8b5925b417b36c75157fc96502391720d5fcf0edac65a6abfbe3.The earlier note here recorded 69245af2 from a run that proved nothing. The comparison sat after "Build the candidate artifact", which writes `bin/`, so it read back the candidate and compared it with itself. The digests agreed because the binary happened to be current, not because anything checked. The step now runs before any build writes to `bin/`, and `tests/ssh-agent-artifact.test.js` fails if it is ever moved back. -
Manual check:
tests/ssh-agent-bundle.test.jsdoes exactly this against real files in disposable directories -- missing, non-executable, truncated, a Git LFS placeholder, and a broken shipped binary beside a working development build -- asserting the feature stays off and the diagnostic names the cause. Live: the panel launches bin/x86_64-linux/qs-bitwarden-ssh-agent, reaches ready and serves both keys.
Dependencies: Tasks 10 and 16
Files likely touched:
bin/x86_64-linux/qs-bitwarden-ssh-agentbin/SHA256SUMSBitwardenModel.jsPanel.qmltests/ssh-agent-bundle.test.js
Estimated scope: Medium: 5 files
Task 19: Protect release provenance
Description: Add a protected tag workflow that repeats all gates, verifies tracked bytes/checksums, creates GitHub build-provenance attestations, and publishes SBOM plus dependency/license reports. Restrict elevated permissions to the final release job and require review for security-sensitive paths.
Acceptance criteria:
- Protected releases rebuild/compare final bytes, run them natively, verify
checksum/mode/version/self-test, and fail before publication on drift.
The gates stage calls
agent-build.ymlrather than copying it, so the release runs the branch's own--verify-reproducibleand--compare-trackedagainst the tagged commit; the verify stage re-checksbin/SHA256SUMS, the executable bit, the ELF class, and the helper's reported crate and control-protocol versions againstagent/Cargo.tomlandBitwardenModel.js; the release stage runs the shipped bytes on the bare runner, outside the build container, before it publishes anything. - Only the reviewed release job receives
id-token: writeandattestations: write; all actions are SHA-pinned and source paths have required CODEOWNERS review. The workflow defaults tocontents: read, the gates and verify jobs restate it rather than inheriting it, and the release job sits behind thereleaseenvironment. - Release artifacts include provenance, SBOM, dependency/license report,
and separate debug symbols but no vault/test secret material. The debug
symbols are a companion build and say so: the release profile strips
symbols, and a build with debug information links differently, so its
entry point and code layout are not the shipped binary's. That was
measured, not assumed, and
dist/DEBUG-SYMBOLS.mdstates the limit where whoever downloads the file will read it.
Verification:
-
Tests pass:
node tests/release-provenance.test.js -
Environment configured:
release, required reviewer@Elevate08(self-review permitted -- single maintainer), deployments restricted tov*tags. Confirmed through the API after creation. -
Workflow passes: execute a protected test tag/release in a staging target. Done for real rather than in staging: v1.5.0 and v1.6.0 were both cut through this workflow and published with their full asset set. Everything before it has now run on real CI twice, via a temporary branch trigger that was reverted immediately afterwards (
e696fb1and its revert): gates, tag/manifest/changelog agreement, checksum and ELF checks, the version cross-check,--self-test, the SBOM, the licence and dependency reports, the debug symbols, and the secret scan all pass, and the publishing job correctly skipped itself on a non-tag ref.The rehearsal earned its cost. It found two defects that reading the file had not: a container job's default shell is `sh`, which has no arrays, so the secret scan died after every expensive step had passed; and the gates stage shared `agent-build.yml`'s concurrency group, letting a branch build and a release cancel each other. Both are fixed and both now have a test. -
Provenance verifies:
gh attestation verify bin/x86_64-linux/qs-bitwarden-ssh-agent --repo Elevate08/qs-bitwarden-cliPasses against the tracked helper as of 2026-09-03. The first protected tag run this was waiting on happened at v1.5.0. -
Manual check: audit permissions, environment protection, attestation subject/digest, SBOM, reports, and retention settings. Permissions and environment protection are audited by the tests above; the attestation subject, digest and published assets can only be audited after a run.
Dependencies: Tasks 17 and 18
Files likely touched:
.github/workflows/release.yml.github/CODEOWNERS.github/workflows/agent-build.yml(made callable, so the release reuses the branch's gates instead of duplicating them)tests/release-provenance.test.js
Estimated scope: Small: 4 files
Checkpoint: Shippable Artifact (Tasks 18–19)
- Tracked bytes equal the protected clean build and pass native validation.
- Provenance, checksum, SBOM, license, and permission checks pass. The
v1.6.0 release carries all of them:
SHA256SUMS, the CycloneDX SBOM,dependency-licences.json,dependency-tree.txt, and separated debug symbols, alongside a verifying attestation. - Human release-governance review approves the trust path.
Task 20: Complete release documentation
Description: Update user, operator, and design documentation for setup, socket routing, approvals, grants, export, Git authentication/signing, locking, security limits, provenance, upgrades, troubleshooting, disable/uninstall cleanup, and the feature's explicit threat boundary. Resolve the design draft's public-export contradiction and mark validated assumptions with evidence.
Acceptance criteria:
- Documentation accurately covers UWSM re-login, terminal diagnostics, conflicts with other agents, configuration snippets, grants, public files, version floor, helper verification, and cleanup after removal. README gains an "SSH Agent" section, three rows in the configuration reference, a feature bullet, and two removal paths; CHANGELOG and manifest carry 1.5.0.
- The threat model plainly distinguishes best-effort erasure, public cache,
same-UID/root limits,
bwexposure, checksum corruption checks, and CI provenance without overstating any guarantee. "What this does not defend against" says the checksum is not tamper detection and names what it does catch, that process attribution is reported rather than verified, and that agent forwarding is unsupported in this release. - The complete manual matrix passes for login/unlock/sync/lock/logout, both unlock-on-demand modes, auth/sign/rebase, crash/reload/update, disable/uninstall, and normal non-vault SSH keys. Needs a real desktop and a real vault; the maintainer's to run.
Two documentation defects were fixed while writing this, both stale rather
than wrong when written: manifest.json still described an approval grant as
scoped to one process, which docs/decisions/0002-grant-scope.md had
already relaxed to one program; and the design draft's "Assumptions to
Validate" named a bw floor of 2025.1.0 where the code enforces 2025.1.2.
Verification:
- Full gates pass: JavaScript, QML, Qt6 lint baseline, manifest validation,
Rust fmt/Clippy/tests, dependency policy, reproducible build, and E2E.
The JavaScript suite and
omarchy plugin validatepass on the doc commit; the Rust and build gates last ran green on the previous commit and are unaffected by documentation, but CI path filters mean a docs-only push does not re-run them. Re-run before the tag. - Docs check: follow setup, signing, verification, troubleshooting, and uninstall instructions from a clean disposable user/session.
- Manual check: complete the release matrix and obtain human security, usability, documentation, and release approval.
Three boxes above stay open because nothing can close them yet: no tag exists, so there is no release to attest and no attestation to verify. They are the release action itself, not work outstanding. Everything they depend on -- protected workflow, environment protection with a required reviewer, pinned attestation action, reproducible build compared against the tracked bytes -- has been audited and passes.
Dependencies: Tasks 3, 11, 14, 15, and 19
Files likely touched:
README.mdCHANGELOG.mdmanifest.jsondocs/ideas/ssh-agent.md
Estimated scope: Medium: 4 files
Findings from the Task 20 manual matrix
Recorded in full in tasks/bugs.md. Four defects, all found by running the
matrix rather than by review:
- The README described
sshAgentUnlockOnDemandas gating signing. The code was right and the documentation wrong -- fixed in the docs. - A grant's remaining time never counted down: announced once and frozen for the grant's whole life, then gone. Fixed.
- The helper leaks its socket, FIFO and lock when the plugin is torn down rather than shut down. Open, low severity, documented.
- The uninstall instructions told users to hand-edit
shell.json, which destroyed a stow-managed config and emptied the bar of every plugin. The step was redundant as well as dangerous; removed.
Three behaviour changes also landed after the Task 14 and 18 checkpoints had signed off on that surface: the cooldown banner and its resume control, the routing fragment restored on re-enable, and Remove Plugin Data. Their criteria describe a panel that no longer exists in those areas.
Checkpoint: Complete (Task 20)
- Every task's acceptance criteria pass.
- The full project Definition of Done passes.
- No task exceeded five files without being split and re-reviewed.
- Human approval is recorded before merge or release.
Task 21: Add the centered-prompt setting contract
Description: Add the opt-in boolean consistently to the manifest, model schema, panel setting reader, and settings tests.
Acceptance criteria:
- The setting is disabled by default and invalid values fail closed.
- Manifest, model schema, runtime reader, and settings UI agree on its key, type, label, description, and default.
- Existing SSH settings remain available and retain their defaults.
Verification: node tests/ssh-agent-setup.test.js
Dependencies: Task 20
Files likely touched: manifest.json, BitwardenModel.js, Panel.qml,
tests/ssh-agent-setup.test.js
Task 22: Route approvals to a centered surface
Description: Add a themed centered overlay and reuse the current approval screen while preserving the existing panel route when the setting is off.
Acceptance criteria:
- Popup mode never opens or navigates the anchored panel for an SSH approval; legacy mode remains unchanged.
- Approval, denial, cancellation, timeout, countdown, loading, and grants call the existing request functions and remove the overlay afterward.
- Escape and outside click deny; bare Enter never approves.
Verification: node tests/ssh-agent-ui.test.js and Qt6 qmllint on all
changed QML files.
Dependencies: Task 21
Files likely touched: Panel.qml, SshApprovalPopup.qml,
SshApprovalScreen.qml, tests/ssh-agent-ui.test.js
Checkpoint: Centered approval
- Focused and regression tests pass.
- Disabled mode is behaviorally unchanged.
- Popup mode is centered on the bar widget's output and releases focus on every terminal request state.
Task 23: Keep locked-vault SSH requests inside the popup
Description: Present unlock status and configured unlock methods in the same transient surface, then transition in place to the approval screen.
Acceptance criteria:
- PIN, fingerprint, and master-password unlock use existing auth functions while the anchored panel stays closed.
- Unlock success promotes signing requests to approval and leaves identity listing requests in their loading state until released.
- Dismissal and failure scrub transient input and never authorize signing.
Verification: node tests/ssh-agent-ui.test.js, the full JavaScript suite,
and the QML test suite.
Dependencies: Task 22
Files likely touched: Panel.qml, SshApprovalPopup.qml,
SshUnlockScreen.qml, tests/ssh-agent-ui.test.js
Task 24: Document and verify centered prompts
Description: Document the opt-in behavior and run the complete repository gates plus a real locked-vault SSH request when the desktop environment is available.
Acceptance criteria:
- README configuration and SSH approval documentation explain both modes.
- Full JavaScript/QML tests, QML lint, and plugin validation pass.
- Manual locked -> unlock -> approve and unlocked -> approve flows pass.
Verification: Commands in docs/ideas/ssh-approval-popup.md.
Dependencies: Task 23
Files likely touched: README.md, tasks/plan.md, tasks/todo.md
Checkpoint: Centered SSH prompts complete
- Every success criterion in
docs/ideas/ssh-approval-popup.mdpasses. - No private key, password, session token, payload, or signature is added to persistent QML state, logs, argv, files, or tests.
- Human review approves merge; no commit or push is performed automatically.
Task List: Colorized Menu-Bar Icon
Source design: docs/ideas/colorized-menu-bar-icon.md.
Implementation plan: tasks/plan.md, “Colorized Menu-Bar Icon”.
Task 1: Add the colorization setting contract
- Add
colorizeIconto the manifest defaults/schema and the model settings schema as a General boolean with a false default. - Extend focused settings tests for schema parity and strict false fallback on malformed values.
- Verify with
node tests/setup-settings.test.jsandnode tests/lock-state.test.js.
Dependencies: None
Task 2: Wire the theme-accent shield and General toggle
- Read
colorizeIconfrom the live setting inPanel.qml. - Use
Color.accentonly for the primary shield when enabled; preserve the existing foreground/urgent badge bindings. - Add focused settings-screen/source assertions and run QML lint.
Dependencies: Task 1
Checkpoint: Colorized Icon Behavior
- Off state matches the current bar icon.
- On state uses the active theme accent.
- Locked, setup-required, and error states retain their status indicators.
- Human review confirms the UI label and behavior before final verification.
Task 3: Add regression coverage and document the setting
- Complete the focused and full regression coverage.
- Document the General toggle and theme-derived behavior in the chosen user-facing documentation file.
- Run the full JavaScript/QML suites and
omarchy plugin validate .. - Perform the manual runtime/theme verification when the desktop environment is available.
Dependencies: Task 2
Checkpoint: Colorized Menu-Bar Icon Complete
- All acceptance criteria in
docs/ideas/colorized-menu-bar-icon.mdandtasks/plan.mdpass. - No unrelated icon, badge, or panel colors changed.
- Human review approves the implementation; no commit or push is performed automatically.