Files
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

1092 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:**
- [x] 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.
- [x] 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.
- [x] Raw input and QML output have producer-side caps; malformed, truncated,
or filter-failing input returns an error rather than a partial model.
- [x] The filter is static, receives values only through safe `jq --arg`
inputs, and preserves the repository's no-secret-in-argv policy.
**Verification:**
- [x] Tests pass: `node tests/ssh-items.test.js`
- [x] Regression suite passes: `for test_file in tests/*.test.js; do node "$test_file" || exit 1; done`
- [x] 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:**
- [x] 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.
- [x] SSH keys appear in normal list/search/count flows and contextual
suggestions remain login-only.
- [x] Generic `bw get item`, create, edit, and clone paths reject type 5; a
read-only detail view never displays or requests private material.
**Verification:**
- [x] Tests pass: `node tests/ssh-items.test.js && node tests/items.test.js`
- [x] QML tests pass: `QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml`
- [x] 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:**
- [x] First-run dependency checks report missing `jq` as required without
changing optional dependency semantics.
- [x] SSH UI/agent setup is unavailable below `bw` 2025.1.2 and reports
unconfirmed capability separately from a confirmed empty key set.
- [x] A whole-list failure attributable to the known upstream SSH-item issue
names the 2026.8.0 fix without exposing raw CLI output.
**Verification:**
- [x] Tests pass: `node tests/setup-settings.test.js && node tests/ssh-items.test.js`
- [x] Plugin validates: `omarchy plugin validate .`
- [x] 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)
- [x] Full JavaScript and QML suites pass.
- [x] Qt6 lint remains at the documented baseline and the manifest validates.
- [x] Marker tests prove no SSH private key or unknown cipher reaches QML.
- [x] 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:**
- [x] Current primary sources confirm maintenance status, required Ed25519/RSA
support, peer credentials, licenses, audit surface, and protocol hooks.
- [x] The decision identifies how parsed private components are zeroized or
explicitly stops the project if credible lock-time erasure is impossible.
- [x] `cargo test --locked` builds a minimal headless crate on
`x86_64-unknown-linux-gnu` with no vault/network dependency.
**Verification:**
- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked`
- [x] Build succeeds: `cargo build --manifest-path agent/Cargo.toml --locked`
- [x] 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:**
- [x] OpenSSH-compatible vectors pass for identity listing, Ed25519, RSA
SHA-256, and RSA SHA-512 signatures.
- [x] Frames over 256 KiB, truncation, invalid lengths/UTF-8, mutation requests,
and unknown extensions fail with normal bounded agent failures.
- [x] No error or debug path emits private keys, payloads, signatures, or raw
parser data.
**Verification:**
- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test protocol`
- [x] Lint passes: `cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings`
- [x] 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:**
- [x] 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.
- [x] Public blob and derived fingerprint must match vault metadata; duplicates
advertise once and re-prompt items never enter the private keystore.
- [x] Lock/epoch tests prove no authorization crosses after the atomic deny
point and secret containers are dropped/zeroized without long-lived
clones.
**Verification:**
- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test keystore`
- [x] Formatting passes: `cargo fmt --manifest-path agent/Cargo.toml --check`
- [x] 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)
- [x] Protocol, crypto, mismatch, limit, and lock tests pass.
- [x] Dependency and secret-memory findings support the stated lock semantics.
- [x] 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:**
- [x] Runtime directory/FIFO ownership, type, and modes are verified; stale,
symlinked, wrong-owner, and wrong-type paths are refused safely.
- [x] Missing, stale, duplicate, truncated, malformed, or nonce-mismatched
payloads fail the whole load and never publish a partial key set.
- [x] Stress tests prove bounded draining and document when the two-read
fallback must replace the preferred `tee` design.
**Verification:**
- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test load`
- [x] Build succeeds: `cargo build --manifest-path agent/Cargo.toml --locked`
- [x] 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:**
- [x] At most four sign requests exist, each expires within 30 seconds and is
cancelled on disconnect; late/duplicate/old-epoch approvals are rejected.
- [x] Grants are capped at 900 seconds, scoped to one key/live process, and
revoked by every specified lifecycle event or identity mismatch.
- [x] Same-UID peer enforcement and PID-reuse/re-exec tests pass while prompt
metadata remains explicitly non-authoritative.
**Verification:**
- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test approvals`
- [x] Lint passes: `cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings`
- [x] 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:**
- [x] 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.
- [x] Control lines over 64 KiB, wrong versions/types, full channels, slow
clients, or stdin EOF fail closed without blocking lock processing.
- [x] 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:**
- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test lifecycle`
- [x] Build succeeds: `cargo build --manifest-path agent/Cargo.toml --locked`
- [x] 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)
- [x] All Rust tests, format, Clippy, and initial dependency audit pass.
- [x] Real OpenSSH/Git smoke tests pass with disposable keys and a fake panel.
- [x] No vault credential or production child process enters the helper.
- [x] 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:**
- [x] Disabled mode starts nothing; enabled mode uses a tracked non-detached
`Process`, minimal environment, absolute path, and compatible handshake.
- [x] Quickshell never waits synchronously; overlong/malformed output, EOF,
mismatch, or crash closes the signing gate and reaches a bounded error state.
- [x] A crash loop stops restarts and leaves login, unlock, list, copy, sync,
edit, Send, and generator flows usable.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-control.test.js`
- [x] QML tests pass: `QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml`
- [x] 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:**
- [x] `sshAgentEnabled=false`, `sshAgentUnlockOnDemand=false`, and a clamped
`sshAgentApprovalWindowSec=120` default are consistent across manifest,
model schema, and settings UI.
- [x] Managed UWSM setup/removal uses safe parent creation, atomic mode-safe
writes, symlink refusal, unexpected-content refusal, conflict confirmation,
and explicit logout/login guidance.
- [x] Diagnostics distinguish matching, elsewhere, and unset client sockets and
print the terminal check without gating companion startup.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-setup.test.js && node tests/setup-settings.test.js`
- [x] Plugin validates: `omarchy plugin validate .`
- [x] 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:**
- [x] 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.
- [x] Whole-pipeline, branch, helper, timeout, cap, or validation failure leaves
no partial private set and retries the core list once without the branch.
- [x] 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:**
- [x] Tests pass: `node tests/ssh-agent-pipeline.test.js && cargo test --manifest-path agent/Cargo.toml --locked --test load`
- [x] Regression suite passes: `for test_file in tests/*.test.js; do node "$test_file" || exit 1; done`
- [x] 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)
- [x] Disabled mode is inert and core panel regressions pass.
- [x] Enabled mode loads from one read with bounded fallback behavior.
- [x] QML responsiveness is verified under blocked clients and failed helpers.
- [x] 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:**
- [x] Every lifecycle path advances the epoch and produces the specified public
cache, private cache, grants, pending requests, and process state.
- [x] No signature response from the previous epoch returns after lock
acknowledgment; timeout kills/reaps the helper while the vault lock proceeds.
- [x] Starting beside a remembered unlocked session performs an initial key
load, while logout/account change/disable clear public projections too.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-lifecycle.test.js && node tests/lock-triggers.test.js && node tests/lock-state.test.js`
- [x] Rust tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test lifecycle`
- [x] 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:**
- [x] Prompts clearly offer deny, approve once, and process grant only when
enabled; they show key/process/forwarding context without overstating PID
authority.
- [x] Locked cached keys prompt at sign time; no-cache identity listing prompts
only when unlock-on-demand is enabled, with coalescing and denial cooldown.
- [x] Four-request/deadline/disconnect behavior is visible and bounded, and live
grants update/revoke without freezing or exposing secret control data.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-ui.test.js`
- [x] QML tests pass: `QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml`
- [x] 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:**
- [x] 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.
- [x] Projection updates are atomic, survive lock, and clear on logout/account
change/disable without ever writing private material.
- [x] Real Git SSH signing and `IdentityFile`/`IdentitiesOnly` flows work from
the exported paths using disposable keys.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-export.test.js`
- [x] 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)
- [x] Lifecycle, UI, export, full Rust, full JavaScript, and QML suites pass.
- [x] Real authentication and signing work without a private key on disk.
- [x] Security review confirms private material paths and final authorization.
- [x] 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:**
- [x] Two clean builds from distinct absolute work paths emit identical
stripped helper bytes and stable checksums.
- [x] Build inputs cover toolchain, container, linker/strip tools, target,
flags, release profile, source path, and Cargo registry path.
- [x] The script refuses unlocked dependencies or an unsupported target and
reports source/binary drift without modifying the repository.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-artifact.test.js`
- [x] 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.
- [x] 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:**
- [x] 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.
- [x] CI runs JavaScript/QML, fmt, Clippy, Rust tests, native E2E, RustSec,
license/source/duplicate policy, and reproducible build checks.
- [x] Source-only fork PRs report candidate drift without impossible blocking;
`bin/` changes and merges to `master` require exact clean-build bytes.
**Verification:**
- [x] Policy passes: `cargo deny --manifest-path agent/Cargo.toml check`
- [x] 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.
- [x] 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)
- [x] Reproducibility and every read-only PR gate pass.
- [x] Fork contribution and tracked-byte policies are both workable.
- [x] 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:**
- [x] The repository contains executable release bytes and a matching checksum
produced by Task 16, without Git LFS or runtime download.
- [x] Missing, corrupt, stale, wrong-architecture, non-executable, self-test-
failing, or protocol-mismatched helpers reach clear bounded diagnostics.
- [x] Every validation failure leaves the rest of the Bitwarden plugin usable
and creates no agent socket/FIFO/private branch.
**Verification:**
- [x] Tests pass: `node tests/ssh-agent-bundle.test.js`
- [x] 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.
- [x] 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:**
- [x] 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.
- [x] 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.
- [x] 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:**
- [x] Tests pass: `node tests/release-provenance.test.js`
- [x] Environment configured: `release`, required reviewer `@Elevate08`
(self-review permitted -- single maintainer), deployments restricted to
`v*` tags. Confirmed through the API after creation.
- [x] 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.
- [x] 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.
- [x] 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)
- [x] Tracked bytes equal the protected clean build and pass native validation.
- [x] 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.
- [x] 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:**
- [x] 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.
- [x] 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.
- [x] 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:**
- [x] 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.
- [x] Docs check: follow setup, signing, verification, troubleshooting, and
uninstall instructions from a clean disposable user/session.
- [x] 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)
- [x] Every task's acceptance criteria pass.
- [x] The full project Definition of Done passes.
- [x] No task exceeded five files without being split and re-reviewed.
- [x] 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:**
- [x] The setting is disabled by default and invalid values fail closed.
- [x] Manifest, model schema, runtime reader, and settings UI agree on its key,
type, label, description, and default.
- [x] 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:**
- [x] Popup mode never opens or navigates the anchored panel for an SSH
approval; legacy mode remains unchanged.
- [x] Approval, denial, cancellation, timeout, countdown, loading, and grants
call the existing request functions and remove the overlay afterward.
- [x] 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
- [x] Focused and regression tests pass.
- [x] Disabled mode is behaviorally unchanged.
- [x] 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:**
- [x] PIN, fingerprint, and master-password unlock use existing auth functions
while the anchored panel stays closed.
- [x] Unlock success promotes signing requests to approval and leaves identity
listing requests in their loading state until released.
- [x] 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:**
- [x] README configuration and SSH approval documentation explain both modes.
- [x] Full JavaScript/QML tests, QML lint, and plugin validation pass.
- [x] 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
- [x] Every success criterion in `docs/ideas/ssh-approval-popup.md` passes.
- [x] No private key, password, session token, payload, or signature is added
to persistent QML state, logs, argv, files, or tests.
- [x] 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
- [x] Add `colorizeIcon` to the manifest defaults/schema and the model settings
schema as a General boolean with a false default.
- [x] Extend focused settings tests for schema parity and strict false fallback
on malformed values.
- [x] 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
- [x] Read `colorizeIcon` from the live setting in `Panel.qml`.
- [x] Use `Color.accent` only for the primary shield when enabled; preserve the
existing foreground/urgent badge bindings.
- [x] Add focused settings-screen/source assertions and run QML lint.
**Dependencies:** Task 1
## Checkpoint: Colorized Icon Behavior
- [x] Off state matches the current bar icon.
- [x] On state uses the active theme accent.
- [x] 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
- [x] Complete the focused and full regression coverage.
- [x] Document the General toggle and theme-derived behavior in the chosen
user-facing documentation file.
- [x] 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
- [x] All acceptance criteria in `docs/ideas/colorized-menu-bar-icon.md` and
`tasks/plan.md` pass.
- [x] No unrelated icon, badge, or panel colors changed.
- [ ] Human review approves the implementation; no commit or push is performed
automatically.