Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/todo.md
T
asepharyana 1cdb82a76f Sync config from arch
- 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
2026-09-23 15:19:12 +07:00

47 KiB
Raw Blame History

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 sshKey subtree 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 --arg inputs, 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.js
  • tests/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 from sshKeys with 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.js
  • Panel.qml
  • tests/ssh-items.test.js
  • tests/items.test.js
  • tests/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 jq as required without changing optional dependency semantics.
  • SSH UI/agent setup is unavailable below bw 2025.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, old bw, empty supported vault, and malformed-item fixture diagnostics.

Dependencies: Task 2

Files likely touched:

  • BitwardenModel.js
  • Panel.qml
  • manifest.json
  • tests/setup-settings.test.js
  • tests/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 --locked builds a minimal headless crate on x86_64-unknown-linux-gnu with 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.toml
  • agent/Cargo.lock
  • agent/rust-toolchain.toml
  • agent/src/lib.rs
  • docs/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 -L and disposable-key smoke test remains part of the Task 9 socket-harness checkpoint.

Dependencies: Task 4

Files likely touched:

  • agent/src/protocol.rs
  • agent/src/signing.rs
  • agent/src/lib.rs
  • agent/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.rs
  • agent/src/state.rs
  • agent/src/lib.rs
  • agent/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 tee design.

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/tee tests with slow readers, branch failure, duplicate writers, and the full 8 MiB limit.

Dependencies: Task 6

Files likely touched:

  • agent/src/load.rs
  • agent/src/runtime.rs
  • agent/src/lib.rs
  • agent/tests/load.rs
  • tests/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.rs
  • agent/src/peer.rs
  • agent/src/server.rs
  • agent/src/lib.rs
  • agent/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 no PATH/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 sign client, 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.rs
  • agent/src/control.rs
  • agent/src/runtime.rs
  • agent/src/server.rs
  • agent/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.js drives 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 only XDG_RUNTIME_DIR, reports paths under that directory, and cleans up its socket when stdin closes. Live: the shell restarted, omarchy-shell shell ping answered, the panel opened, and status returned locked with no helper process, socket, or FIFO created in the default disabled mode.

Dependencies: Tasks 3 and 9

Files likely touched:

  • BitwardenModel.js
  • Panel.qml
  • tests/ssh-agent-control.test.js
  • tests/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 clamped sshAgentApprovalWindowSec=120 default 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.js exercises 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 ping answered, 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.json
  • BitwardenModel.js
  • Panel.qml
  • tests/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 items read, and a lock can terminate/reap the entire bw/cap/tee/jq process 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.js runs 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 a bw that 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 confirms ssh-add -L lists 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.js
  • Panel.qml
  • tests/ssh-agent-pipeline.test.js
  • agent/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 status could 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 by agent/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.qml
  • tests/ssh-agent-lifecycle.test.js
  • tests/lock-triggers.test.js
  • tests/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 push authentication, 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.qml
  • tests/ssh-agent-ui.test.js
  • tests/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 .pub file 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/IdentitiesOnly flows 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 .pub per advertised key with correct OpenSSH content. A commit signed with gpg.format=ssh, user.signingkey pointing at an exported path, and a generated allowed-signers file verifies: git verify-commit reports a good ED25519 signature and git log %G? reports G. 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.js
  • Panel.qml
  • agent/src/control.rs
  • tests/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 --explain says so without building.
  • Manual check: done in CI rather than locally, this machine having no usable container runtime. Two builds from /tmp/*/path-one and /tmp/*/a-considerably-longer-second-path produced 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/ldd inspection 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.Dockerfile
  • agent/.cargo/config.toml
  • scripts/build-agent.sh
  • tests/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 to master require 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: read only, every action is pinned to a full commit SHA with its version in a trailing comment, no secrets. reference appears, the candidate is uploaded and never committed by CI, and the logs carry no key material. Asserted by tests/ssh-agent-artifact.test.js as well as read by eye.

Dependencies: Task 16

Files likely touched:

  • .github/workflows/ci.yml
  • .github/dependabot.yml
  • deny.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.js does 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-agent
  • bin/SHA256SUMS
  • BitwardenModel.js
  • Panel.qml
  • tests/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.yml rather than copying it, so the release runs the branch's own --verify-reproducible and --compare-tracked against the tagged commit; the verify stage re-checks bin/SHA256SUMS, the executable bit, the ELF class, and the helper's reported crate and control-protocol versions against agent/Cargo.toml and BitwardenModel.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: write and attestations: write; all actions are SHA-pinned and source paths have required CODEOWNERS review. The workflow defaults to contents: read, the gates and verify jobs restate it rather than inheriting it, and the release job sits behind the release environment.
  • 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.md states 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 to v* 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 (e696fb1 and 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-cli Passes 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, bw exposure, 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 validate pass 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.md
  • CHANGELOG.md
  • manifest.json
  • docs/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:

  1. The README described sshAgentUnlockOnDemand as gating signing. The code was right and the documentation wrong -- fixed in the docs.
  2. A grant's remaining time never counted down: announced once and frozen for the grant's whole life, then gone. Fixed.
  3. The helper leaks its socket, FIFO and lock when the plugin is torn down rather than shut down. Open, low severity, documented.
  4. 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.md passes.
  • 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 colorizeIcon to 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.js and node tests/lock-state.test.js.

Dependencies: None

Task 2: Wire the theme-accent shield and General toggle

  • Read colorizeIcon from the live setting in Panel.qml.
  • Use Color.accent only 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.md and tasks/plan.md pass.
  • No unrelated icon, badge, or panel colors changed.
  • Human review approves the implementation; no commit or push is performed automatically.