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

721 lines
42 KiB
JavaScript

#!/usr/bin/env node
// The approval prompt is the one place a user is asked to authorise a
// signature, so what it shows has to be accurate about what the companion
// actually verified -- and what it did not. These tests cover the prompt's
// presentation, the deny/approve/grant control lines, the denial cooldown
// that stops a same-UID process reopening the panel forever, and the rule
// that no prompt is ever raised over a locked screen.
//
// node tests/ssh-agent-ui.test.js
const fs = require("fs")
const path = require("path")
const repoRoot = path.join(__dirname, "..")
const Model = {}
new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8")
.replace(/^\.pragma library\s*$/m, "") + `
exports.parseAgentEvent = parseAgentEvent
exports.sshAgentApproveLine = sshAgentApproveLine
exports.sshAgentDenyLine = sshAgentDenyLine
exports.sshAgentUnlockCancelledLine = sshAgentUnlockCancelledLine
exports.sshAgentRevokeGrantLine = sshAgentRevokeGrantLine
exports.sshAgentRevokeGrantsLine = sshAgentRevokeGrantsLine
exports.sshAgentPromptView = sshAgentPromptView
exports.sshAgentGrantViews = sshAgentGrantViews
exports.sshAgentGrantsAt = sshAgentGrantsAt
exports.sshAgentDevelopmentHelperWarning = sshAgentDevelopmentHelperWarning
exports.sshAgentRoutingNotice = sshAgentRoutingNotice
exports.pluginDataRemoveCommand = pluginDataRemoveCommand
exports.parsePluginDataRemoval = parsePluginDataRemoval
exports.sshAgentShouldPrompt = sshAgentShouldPrompt
exports.sshAgentCooldownInitial = sshAgentCooldownInitial
exports.sshAgentCooldownAfter = sshAgentCooldownAfter
exports.sshAgentCooldownActive = sshAgentCooldownActive
exports.sshAgentCooldownStatus = sshAgentCooldownStatus
exports.sshAgentLoadingNote = sshAgentLoadingNote
exports.sshAgentOptionsLine = sshAgentOptionsLine
exports.sshAgentRequestDeadlineMs = sshAgentRequestDeadlineMs
exports.sshAgentEnqueuePrompt = sshAgentEnqueuePrompt
exports.sshAgentDequeuePrompt = sshAgentDequeuePrompt
exports.sshAgentRemovePrompt = sshAgentRemovePrompt
exports.sshAgentPendingCount = sshAgentPendingCount
exports.plainLabel = plainLabel
`)(Model)
let pass = 0
const failures = []
const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`)
const eq = (label, actual, expected) =>
check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
// -------------------------------------------------------------------------
// The companion's new messages must survive the bounded reader
// -------------------------------------------------------------------------
for (const [type, body] of [
["unlock_required", { requestId: 41, reason: "sign", keyName: "Work", fingerprint: "SHA256:x", pid: 12, processPath: "/usr/bin/ssh" }],
["approval_required", { requestId: 42, keyId: "k", keyName: "Work", fingerprint: "SHA256:x", pid: 12, processPath: "/usr/bin/ssh", operation: "ssh-sign", forwarded: false, grantOffered: true }],
["request_cancelled", { requestId: 42, reason: "withdrawn" }],
["grants_changed", { grants: [] }]
]) {
const parsed = Model.parseAgentEvent(JSON.stringify(Object.assign({ v: 1, type: type }, body)))
eq(`${type} parses`, parsed.ok, true)
eq(`${type} keeps its type`, parsed.ok && parsed.message.type, type)
}
// -------------------------------------------------------------------------
// Control lines
// -------------------------------------------------------------------------
eq("approve once carries a zero window", Model.sshAgentApproveLine(42, 0),
JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 0 }) + "\n")
eq("approve for a process carries its window", Model.sshAgentApproveLine(42, 120),
JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 120 }) + "\n")
eq("a grant window is clamped to the documented cap", Model.sshAgentApproveLine(42, 99999),
JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 900 }) + "\n")
eq("a negative window approves once instead", Model.sshAgentApproveLine(42, -5),
JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 0 }) + "\n")
eq("deny is a versioned v1 line", Model.sshAgentDenyLine(42),
JSON.stringify({ v: 1, type: "deny", requestId: 42 }) + "\n")
eq("a dismissed unlock is reported as user-cancelled", Model.sshAgentUnlockCancelledLine(41),
JSON.stringify({ v: 1, type: "unlock_cancelled", requestId: 41, reason: "user-cancelled" }) + "\n")
// The companion cannot read shell.json, so the panel has to tell it.
eq("unlock-on-demand is sent to the companion", Model.sshAgentOptionsLine(true),
JSON.stringify({ v: 1, type: "options", unlockOnDemand: true }) + "\n")
eq("and its default off state is sent too", Model.sshAgentOptionsLine(false),
JSON.stringify({ v: 1, type: "options", unlockOnDemand: false }) + "\n")
eq("anything that is not true is off", Model.sshAgentOptionsLine(undefined),
JSON.stringify({ v: 1, type: "options", unlockOnDemand: false }) + "\n")
eq("a single grant is revoked by id", Model.sshAgentRevokeGrantLine(9),
JSON.stringify({ v: 1, type: "revoke_grant", grantId: 9 }) + "\n")
eq("an invalid request id yields no line", Model.sshAgentApproveLine("nope", 0), "")
eq("an invalid grant id yields no line", Model.sshAgentRevokeGrantLine(-1), "")
// -------------------------------------------------------------------------
// What the prompt shows
// -------------------------------------------------------------------------
const request = {
v: 1, type: "approval_required", requestId: 42, keyId: "item-1",
keyName: "personal ed25519", fingerprint: "SHA256:9wKk2nQ8xR1vLm4pZc7dYtE0",
pid: 48213, processPath: "/usr/bin/ssh", operation: "ssh-sign",
forwarded: false, grantOffered: true
}
const view = Model.sshAgentPromptView(request, 120)
eq("the prompt names the key", view.keyName, "personal ed25519")
eq("the prompt shows the full fingerprint", view.fingerprint, "SHA256:9wKk2nQ8xR1vLm4pZc7dYtE0")
eq("the prompt shows the executable path", view.processPath, "/usr/bin/ssh")
eq("the prompt derives the process name from the path", view.processName, "ssh")
eq("the prompt shows the pid", view.pid, 48213)
eq("the prompt offers a grant", view.grantOffered, true)
check("the grant button states its window", /2m|120/.test(view.grantLabel), view.grantLabel)
// A grant covers one program, not one process (docs/decisions/0002-grant-scope.md).
// The button has to say so, or it promises a narrower thing than it does.
check("the grant button says what it actually covers",
/program/i.test(view.grantLabel) && !/this process/i.test(view.grantLabel), view.grantLabel)
// The companion verifies the peer UID and nothing else. Saying so on the
// prompt is the difference between context and a claim of identity.
check("the prompt says process details are not verified",
/not verified|reported/i.test(view.provenanceNote), view.provenanceNote)
check("the prompt never calls the process trusted or verified",
!/\b(verified|authenticated|trusted) (process|by)\b/i.test(view.provenanceNote), view.provenanceNote)
// A zero window means grants are off, so the button must not be offered.
const noGrant = Model.sshAgentPromptView(request, 0)
eq("a zero window offers no grant", noGrant.grantOffered, false)
const refusedGrant = Model.sshAgentPromptView(Object.assign({}, request, { grantOffered: false }), 120)
eq("a companion that offers no grant is respected", refusedGrant.grantOffered, false)
// Forwarding is rejected in v1; if one ever arrives it is called out, not
// shown as ordinary context.
const forwarded = Model.sshAgentPromptView(Object.assign({}, request, { forwarded: true }), 120)
check("a forwarded request is flagged", forwarded.forwardedWarning.length > 0, forwarded.forwardedWarning)
eq("a forwarded request offers no grant", forwarded.grantOffered, false)
eq("an ordinary request has no forwarding warning", view.forwardedWarning, "")
// A vault item's name is attacker-controllable by whoever shares the
// collection it came from, and the process path comes from outside too.
const hostile = Model.sshAgentPromptView(Object.assign({}, request, {
keyName: "<img src=x onerror=alert(1)>",
processPath: "/usr/bin/<b>ssh</b>"
}), 120)
check("a markup key name cannot reach a rich-text control",
Model.plainLabel(hostile.keyName).indexOf("<img") < 0, Model.plainLabel(hostile.keyName))
check("a hostile name is not silently dropped",
hostile.keyName.length > 0, hostile.keyName)
const huge = Model.sshAgentPromptView(Object.assign({}, request, {
keyName: "n".repeat(5000), processPath: "/" + "p".repeat(5000)
}), 120)
check("an absurd key name is bounded", huge.keyName.length <= 256, String(huge.keyName.length))
check("an absurd path is bounded", huge.processPath.length <= 512, String(huge.processPath.length))
// The panel's countdown has to agree with the companion's deadline, or it
// counts down to a moment nothing happens at. See
// docs/decisions/0003-request-deadline.md for the figure.
eq("the request deadline matches the companion's", Model.sshAgentRequestDeadlineMs(), 120000)
const agentSrc = fs.readFileSync(path.join(repoRoot, "agent", "src", "approvals.rs"), "utf8")
const agentDeadline = /pub const REQUEST_LIFETIME_MS: u64 = ([0-9_]+);/.exec(agentSrc)
check("the panel and the companion agree on it",
agentDeadline && Number(agentDeadline[1].replace(/_/g, "")) === Model.sshAgentRequestDeadlineMs(),
agentDeadline ? agentDeadline[1] : "REQUEST_LIFETIME_MS not found")
// -------------------------------------------------------------------------
// Grants
// -------------------------------------------------------------------------
const grants = Model.sshAgentGrantViews([
{ grantId: 9, keyName: "personal ed25519", fingerprint: "SHA256:x", pid: 48213, processPath: "/usr/bin/ssh", expiresInSec: 95 },
{ grantId: 10, keyName: "work rsa", fingerprint: "SHA256:y", pid: 5, processPath: "/usr/bin/git", expiresInSec: 0 }
])
eq("every grant is listed", grants.length, 2)
eq("a grant keeps its id", grants[0].grantId, 9)
eq("a grant names its process", grants[0].processName, "ssh")
check("a grant states its remaining time", /1m 35s|95/.test(grants[0].remainingLabel), grants[0].remainingLabel)
check("an expiring grant says so", grants[1].remainingLabel.length > 0, grants[1].remainingLabel)
check("no grant view carries key material",
grants.every(g => JSON.stringify(g).indexOf("PRIVATE") < 0), "leaked")
eq("a malformed grant list yields nothing", Model.sshAgentGrantViews(null).length, 0)
// A grant is announced once and then nothing is said until it changes, so the
// remaining time has to be re-derived rather than remembered. Without this the
// settings screen showed "1m 59s left" for the whole two minutes and then the
// row disappeared, having never counted down.
const announced = Model.sshAgentGrantViews(
[{ grantId: 9, keyName: "personal ed25519", fingerprint: "SHA256:x", pid: 48213, processPath: "/usr/bin/ssh", expiresInSec: 120 }],
10_000)
eq("an announced grant records when it expires", announced[0].expiresAtMs, 130_000)
const halfway = Model.sshAgentGrantsAt(announced, 70_000)
eq("the remaining time follows the clock", halfway[0].remainingSec, 60)
check("and the label follows it", /1m/.test(halfway[0].remainingLabel), halfway[0].remainingLabel)
eq("a lapsed grant leaves the list without waiting to be told",
Model.sshAgentGrantsAt(announced, 130_001).length, 0)
eq("a grant on its last second is still listed",
Model.sshAgentGrantsAt(announced, 129_500).length, 1)
eq("an unstamped view survives re-derivation rather than vanishing",
Model.sshAgentGrantsAt([{ grantId: 9, remainingLabel: "2m left" }], 70_000).length, 1)
eq("and so does every view before the first tick",
Model.sshAgentGrantsAt(announced, 0).length, 1)
eq("a malformed set re-derives to nothing", Model.sshAgentGrantsAt(null, 1).length, 0)
// A development helper is a state you can sit in for days without noticing,
// signing with a binary that has no recorded digest and no provenance. The
// warning has to say why that matters, and distinguish a shipped helper that
// was rejected from one that was simply never there.
const rejected = Model.sshAgentDevelopmentHelperWarning({ source: "development", checksum: "mismatch" })
check("a rejected shipped helper is named as the reason", /checksum/i.test(rejected), rejected)
const absent = Model.sshAgentDevelopmentHelperWarning({ source: "development", checksum: "unchecked" })
check("an absent one is not blamed on a checksum", !/checksum/i.test(absent), absent)
for (const [label, text] of [["rejected", rejected], ["absent", absent]]) {
check(`the ${label} warning says what is serving keys`, /locally built/i.test(text), text)
check(`the ${label} warning says what it lacks`, /provenance|digest/i.test(text), text)
check(`the ${label} warning says how to fix it`, /reinstall/i.test(text), text)
check(`the ${label} warning does not answer a user with a build command`,
!/build-agent|cargo/.test(text), text)
}
check("a missing helper record does not throw",
typeof Model.sshAgentDevelopmentHelperWarning(null) === "string", "threw or returned non-string")
// -------------------------------------------------------------------------
// Never prompt over a locked screen
// -------------------------------------------------------------------------
eq("an ordinary desktop prompts", Model.sshAgentShouldPrompt({ screenLocked: false }), true)
eq("a locked screen never prompts", Model.sshAgentShouldPrompt({ screenLocked: true }), false)
eq("an unknown screen state does not prompt", Model.sshAgentShouldPrompt(null), false)
// -------------------------------------------------------------------------
// Denial cooldown
// -------------------------------------------------------------------------
let cool = Model.sshAgentCooldownInitial()
eq("nothing is on cooldown to begin with", Model.sshAgentCooldownActive(cool, 0), false)
cool = Model.sshAgentCooldownAfter(cool, "denied", 1000)
eq("one denial does not start a cooldown", Model.sshAgentCooldownActive(cool, 1000), false)
cool = Model.sshAgentCooldownAfter(cool, "denied", 2000)
eq("two consecutive denials start one", Model.sshAgentCooldownActive(cool, 2000), true)
eq("the cooldown ends on its own", Model.sshAgentCooldownActive(cool, 2000 + 10 * 60 * 1000), false)
// A timeout is a denial for this purpose: the user saw it and did nothing.
let timedOut = Model.sshAgentCooldownInitial()
timedOut = Model.sshAgentCooldownAfter(timedOut, "timeout", 0)
timedOut = Model.sshAgentCooldownAfter(timedOut, "timeout", 100)
eq("two timeouts also start a cooldown", Model.sshAgentCooldownActive(timedOut, 100), true)
// Approving clears the history: the user is engaging, not being pestered.
let mixed = Model.sshAgentCooldownInitial()
mixed = Model.sshAgentCooldownAfter(mixed, "denied", 0)
mixed = Model.sshAgentCooldownAfter(mixed, "approved", 100)
mixed = Model.sshAgentCooldownAfter(mixed, "denied", 200)
eq("an approval resets the denial run", Model.sshAgentCooldownActive(mixed, 200), false)
// An approval cannot end a cooldown that is already running -- the cooldown is
// precisely what stops the prompt an approval would answer. Only an explicit
// resume ends it early; otherwise a user who dismissed two prompts waits out
// the full five minutes with nothing they can do about it.
let stuck = Model.sshAgentCooldownInitial()
stuck = Model.sshAgentCooldownAfter(stuck, "denied", 0)
stuck = Model.sshAgentCooldownAfter(stuck, "denied", 100)
eq("two denials leave a cooldown running", Model.sshAgentCooldownActive(stuck, 100), true)
stuck = Model.sshAgentCooldownAfter(stuck, "resumed", 200)
eq("an explicit resume ends it at once", Model.sshAgentCooldownActive(stuck, 200), false)
eq("and clears the run behind it, so one later denial does not re-arm it",
Model.sshAgentCooldownActive(Model.sshAgentCooldownAfter(stuck, "denied", 300), 300), false)
// Nothing the requesting process does may end a cooldown, or prolong one: the
// suppressed path answers the client itself and records no outcome, so only
// prompts a person actually saw ever feed the run.
let unattended = Model.sshAgentCooldownInitial()
unattended = Model.sshAgentCooldownAfter(unattended, "denied", 0)
unattended = Model.sshAgentCooldownAfter(unattended, "denied", 100)
eq("an unanswered request leaves the window where it was",
Model.sshAgentCooldownAfter(unattended, "withdrawn", 200).untilMs, unattended.untilMs)
eq("and the cooldown lapses on its own",
Model.sshAgentCooldownActive(unattended, 100 + 5 * 60 * 1000), false)
// A cooldown that fails signatures silently is worse than the pestering it
// prevents: SSH just stops working for five minutes with no explanation
// anywhere. It has to say so, and say when it lifts.
let cooled = Model.sshAgentCooldownInitial()
const quiet = Model.sshAgentCooldownStatus(cooled, 0)
eq("nothing is reported while no cooldown is running", quiet.active, false)
eq("a quiet cooldown has no message", quiet.message, "")
cooled = Model.sshAgentCooldownAfter(cooled, "denied", 0)
cooled = Model.sshAgentCooldownAfter(cooled, "denied", 0)
const cooling = Model.sshAgentCooldownStatus(cooled, 0)
eq("an active cooldown is reported", cooling.active, true)
check("it says SSH requests are being refused",
/refus|declin/i.test(cooling.message), cooling.message)
check("it says when it lifts", /\d/.test(cooling.message), cooling.message)
eq("it reports the remaining time", cooling.remainingSec, 300)
check("the remaining time counts down",
Model.sshAgentCooldownStatus(cooled, 60000).remainingSec === 240,
String(Model.sshAgentCooldownStatus(cooled, 60000).remainingSec))
eq("it clears itself when the window passes",
Model.sshAgentCooldownStatus(cooled, 5 * 60 * 1000).active, false)
check("the status never names a key or a process",
cooling.message.indexOf("ssh-") < 0 && cooling.message.indexOf("/usr/") < 0, cooling.message)
// Unlocking runs one `bw list items`, which takes seconds on a real vault.
// The request that triggered the unlock is held across it, so without a
// loading state the user unlocks and then watches nothing happen.
const waiting = Model.sshAgentPromptView(Object.assign({}, request, { type: "unlock_required" }), 120)
check("a held request can say it is still loading",
typeof Model.sshAgentLoadingNote === "function", "no loading note is available")
if (typeof Model.sshAgentLoadingNote === "function") {
const note = Model.sshAgentLoadingNote()
check("the loading note says keys are on the way", /load/i.test(note), note)
check("the loading note does not promise it is instant",
!/instant|immediat/i.test(note), note)
}
// -------------------------------------------------------------------------
// The panel wiring
// -------------------------------------------------------------------------
// Every file the SSH markup lives in: the settings sections and the approval
// screen have their own, and Panel.qml keeps the rest. Reading only the first
// would leave every "this must NOT appear" check below passing on content that
// had simply moved.
const sshUiFiles = [
"Panel.qml", "SshAgentSettings.qml", "SshApprovalScreen.qml",
"SshApprovalPopup.qml", "SshUnlockScreen.qml"
]
const panelSrc = sshUiFiles
.map(file => fs.existsSync(path.join(repoRoot, file))
? fs.readFileSync(path.join(repoRoot, file), "utf8") : "")
.join("\n")
const approvalSrc = fs.readFileSync(path.join(repoRoot, "SshApprovalScreen.qml"), "utf8")
const settingsSrc = fs.readFileSync(path.join(repoRoot, "SshAgentSettings.qml"), "utf8")
const popupSrc = fs.existsSync(path.join(repoRoot, "SshApprovalPopup.qml"))
? fs.readFileSync(path.join(repoRoot, "SshApprovalPopup.qml"), "utf8") : ""
const unlockSrc = fs.existsSync(path.join(repoRoot, "SshUnlockScreen.qml"))
? fs.readFileSync(path.join(repoRoot, "SshUnlockScreen.qml"), "utf8") : ""
// plainLabel() wraps its argument in a span when the text contains markup
// characters, which a PlainText control then renders literally. The field is
// matched with whatever object it hangs off, because the settings sections
// reach the panel as `panel` and the screens as `root`: pinning the prefix
// would let these checks pass on content that had only moved between files.
for (const field of [
"sshAgentVersion",
"modelData.keyName",
"modelData.processName",
"sshUnlockRequest.keyName",
"sshUnlockRequest.processName",
"sshPrompt.keyName",
"sshPrompt.processName",
"sshPrompt.processPath",
"sshRouting.owner"
]) {
const wrapped = new RegExp(
"plainLabel\\(\\s*(?:root|panel|section\\.panel)?\\.?"
+ field.replace(/\./g, "\\.") + "\\s*\\)")
check("SSH PlainText labels do not receive rich-text wrappers for " + field,
!wrapped.test(panelSrc), field)
}
check("there is a dedicated approval screen",
/currentScreen === "sshApproval"/.test(panelSrc), "no sshApproval screen")
check("an approval_required message raises the prompt",
/message\.type === "approval_required"[\s\S]{0,600}?showSshApproval\(message\)/.test(panelSrc),
"approval_required never raises the prompt")
// Opening the panel sends an unlocked one to the item list, so a prompt that
// claimed the screen first would be silently undone -- live state, blank
// screen. Both halves of that ordering are pinned here because the failure is
// invisible: everything reports healthy while nothing is drawn.
check("the legacy panel is opened before its approval screen is claimed",
/function showSshApproval\(message\)[\s\S]{0,1200}?sshAgentApprovalPopup[\s\S]{0,400}?return[\s\S]{0,600}?root\.open\(\)[\s\S]{0,200}?currentScreen = "sshApproval"/.test(panelSrc),
"the screen is claimed before opening, so opening resets it")
// Pinned on the ordering rather than on a character distance: what matters is
// that a live prompt claims the screen and returns before the branch that
// would send an unlocked panel to the item list, not how much housekeeping
// happens above it.
const openedBody = panelSrc.slice(panelSrc.indexOf("function onPanelOpened()"),
panelSrc.indexOf("function onPanelClosed") > 0
? panelSrc.indexOf("function onPanelClosed")
: panelSrc.indexOf("function onPanelOpened()") + 2000)
check("opening the panel does not discard a live request",
/if \(sshPrompt\)[\s\S]{0,160}?currentScreen = "sshApproval"[\s\S]{0,40}?return/.test(openedBody)
&& openedBody.indexOf("sshPrompt") < openedBody.indexOf('status === "unlocked"'),
"onPanelOpened resets away from a live prompt")
check("a withdrawn prompt counts toward the cooldown",
/message\.reason !== "released"[\s\S]{0,200}?sshAgentCooldownAfter\(root\.sshCooldown, "timeout"/.test(panelSrc),
"an unanswered prompt never feeds the cooldown")
// The panel resets currentScreen in several of its own flows -- opening the
// panel, finishing an unlock -- each of which silently dropped a live prompt
// before. Screen visibility binds to activeScreen, which a live request wins,
// so no later assignment can hide a question a client is blocked on.
check("a live prompt outranks navigation state",
/readonly property string activeScreen: sshPrompt !== null && !sshAgentApprovalPopup \? "sshApproval" : currentScreen/.test(panelSrc),
"no activeScreen; a stray currentScreen assignment can hide the prompt")
check("no screen visibility still binds to currentScreen directly",
panelSrc.split("\n").filter(l => l.trim().startsWith("visible:") && l.includes("root.currentScreen")).length === 0,
panelSrc.split("\n").filter(l => l.trim().startsWith("visible:") && l.includes("root.currentScreen")).join(" | "))
check("raising the prompt switches to the approval screen",
/function showSshApproval\(message\)[\s\S]{0,1200}?currentScreen = "sshApproval"/.test(panelSrc),
"the prompt never opens the approval screen")
check("a request that cannot prompt is denied rather than left hanging",
/message\.type === "approval_required"[\s\S]{0,400}?sshAgentMayPrompt\(\)[\s\S]{0,200}?sshAgentDenyLine/.test(panelSrc),
"a suppressed request is not answered")
// A prompt that opened the panel on the user's behalf should hand the desktop
// back when it is answered -- approved or denied alike. A panel the user had
// already opened is theirs, so answering returns them to the screen they were
// on rather than closing it under them.
check("answering a prompt that opened the panel closes it again",
/function dismissSshApproval\(\)[\s\S]{0,900}?openedForThis && root\.opened\) root\.close\(\)/.test(panelSrc),
"the panel stays open after an answer it opened itself for")
check("whether the panel was already open is captured before opening it",
/function showSshApproval\(message\)[\s\S]{0,900}?sshPromptOpenedPanel = !root\.opened/.test(panelSrc),
"nothing records whether the request opened the panel")
check("a panel the user already had open is restored, not closed",
/function dismissSshApproval\(\)[\s\S]{0,900}?screenBeforeSshApproval/.test(panelSrc),
"answering does not restore the previous screen")
check("a withdrawn request takes its prompt down",
/message\.type === "request_cancelled"[\s\S]{0,900}?dismissSshApproval\(\)/.test(panelSrc),
"request_cancelled is ignored")
// The approval decision depends on the key identity and the requesting
// program, both known before the vault read finishes. Waiting for the read
// and only then asking is delay with nothing behind it.
check("unlocking promotes the held request straight to an approval",
/function promoteUnlockToApproval\(\)[\s\S]{0,600}?showSshApproval\(raw\)/.test(panelSrc),
"unlocking never promotes the held request")
check("the promotion happens as soon as the vault unlocks",
/onStatusChanged:[\s\S]{0,200}?promoteUnlockToApproval\(\)/.test(panelSrc),
"nothing promotes on unlock")
// The panel root reaches the screens as `root` and the extracted files as
// `panel`, so the object is matched either way -- a check pinned to one of
// them starts passing or failing on which file the markup sits in.
check("the approval prompt says keys are still loading",
/visible: (?:root|panel)\.sshAgentLoadActive[\s\S]{0,200}?sshAgentLoadingNote\(\)/.test(panelSrc),
"the prompt does not say the keys are still on their way")
check("the held request stays on screen while keys load",
/sshAgentLoadingNote\(\)/.test(panelSrc), "nothing tells the user keys are loading")
check("the loading state is driven by the load actually being in flight",
/sshUnlockRequest[\s\S]{0,600}?sshAgentLoadActive|sshAgentLoadActive[\s\S]{0,600}?sshUnlockRequest/.test(panelSrc),
"the loading state is not tied to a real load")
check("the setting reaches the companion on handshake and on change",
/onSshAgentUnlockOnDemandChanged: sendSshAgentOptions\(\)/.test(panelSrc)
&& /if \(sshAgentGateOpen\) sendSshAgentOptions\(\)/.test(panelSrc),
"unlock-on-demand never reaches the companion")
check("an identity listing is not promoted into an approval",
/reason === "list-identities"/.test(panelSrc),
"a listing would be turned into a signature approval")
check("an unlock request is shown with its context",
/message\.type === "unlock_required"/.test(panelSrc), "unlock_required is ignored")
check("grants are tracked from the companion",
/message\.type === "grants_changed"/.test(panelSrc), "grants_changed is ignored")
check("denying is wired to the deny line",
/sshAgentDenyLine\(/.test(panelSrc), "nothing sends deny")
check("approving once is wired",
/sshAgentApproveLine\(/.test(panelSrc), "nothing sends approve")
check("grants can be revoked individually and together",
/sshAgentRevokeGrantLine\(/.test(panelSrc) && /sshAgentRevokeGrantsLine\(/.test(panelSrc),
"grant revocation is not wired")
check("a locked screen suppresses the prompt",
/sshAgentShouldPrompt\(/.test(panelSrc), "the screen-lock rule is not applied")
check("the cooldown is surfaced in the panel rather than failing silently",
/sshAgentCooldownStatus\(/.test(panelSrc), "the cooldown is never shown to the user")
check("entering the cooldown is announced once, not on every refusal",
/sshCooldownAnnounced/.test(panelSrc), "nothing announces the cooldown")
check("a request the cooldown refuses does not feed the cooldown",
/!sshAgentMayPrompt\(\)\)\s*\{\s*sshAgentWrite\(Model\.sshAgentDenyLine\(message\.requestId\)\)\s*return/.test(panelSrc),
"a suppressed request records an outcome, so a busy process can hold the cooldown open")
// SSH_AUTH_SOCK is fixed at login, so it says what routing *was*. A session
// that started routed keeps reporting "matches" after the file is deleted,
// which is precisely the window in which a warning would still be useful --
// the notice therefore has to read the file, not the environment.
const routed = { state: "matches" }
eq("a deleted fragment is caught even while this session still points here",
Model.sshAgentRoutingNotice({ state: "absent" }, routed).urgent, true)
check("and it says the next login is what breaks",
/next login/.test(Model.sshAgentRoutingNotice({ state: "absent" }, routed).text),
Model.sshAgentRoutingNotice({ state: "absent" }, routed).text)
eq("a foreign file is called out too",
Model.sshAgentRoutingNotice({ state: "foreign" }, routed).urgent, true)
eq("a managed fragment in a routed session says nothing",
Model.sshAgentRoutingNotice({ state: "managed" }, routed).text, "")
check("a managed fragment in an unrouted session explains the wait",
/next login/.test(Model.sshAgentRoutingNotice({ state: "managed" }, { state: "elsewhere" }).text),
"a freshly written fragment does not explain why nothing changed yet")
eq("and that is information, not a fault",
Model.sshAgentRoutingNotice({ state: "managed" }, { state: "elsewhere" }).urgent, false)
for (const quiet of ["unknown", "no-home"]) {
eq(`a ${quiet} fragment leaves the routing section to explain itself`,
Model.sshAgentRoutingNotice({ state: quiet }, routed).text, "")
}
eq("a missing record says nothing rather than throwing",
Model.sshAgentRoutingNotice(null, null).text, "")
check("the routing notice precedes the routing section",
/sshRoutingNotice\.text[\s\S]{0,500}?text: "CLIENT ROUTING"/.test(panelSrc),
"the routing notice does not precede the routing section")
check("re-enabling the agent restores the routing file it removed on disable",
/uwsmRestorePending = true[\s\S]{0,1200}?function applyUwsmRestore\(\)[\s\S]{0,600}?beginUwsmSetup\(\)/.test(panelSrc),
"disabling removes the routing file and enabling never puts it back")
check("but never over a file it did not write, or another session's agent",
/uwsmFragment\.state !== "absent" \|\| sshRouting\.state === "elsewhere"[\s\S]{0,40}?return/.test(panelSrc),
"the restore overrules a foreign routing file or an existing agent")
check("and routing is reachable before the approvals list",
panelSrc.indexOf('text: "CLIENT ROUTING"') < panelSrc.indexOf('text: "ACTIVE APPROVALS"'),
"approvals still push routing down the screen")
// `omarchy plugin remove` has no uninstall hook, so the last moment this code
// can run is while the plugin is still installed. What it misses becomes a
// command the user has to type from the README.
const wipe = Model.pluginDataRemoveCommand().join(" ")
for (const [what, needle] of [
["the keyring entries", "secret-tool clear service qs-bitwarden-cli"],
["the learned suggestions", "XDG_STATE_HOME"],
["the exported public keys", "XDG_DATA_HOME"]
]) check(`removal clears ${what}`, wipe.indexOf(needle) >= 0, wipe)
check("and never logs the vault out on its own",
wipe.indexOf("bw logout") < 0 && wipe.indexOf("bw ") < 0, wipe)
check("nor touches the shell's own settings file",
wipe.indexOf("shell.json") < 0, wipe)
const wiped = Model.parsePluginDataRemoval(0, "removed keyring state data")
eq("a full removal reports success", wiped.ok, true)
check("and names what went", /keyring|suggestions|public keys/.test(wiped.message), wiped.message)
check("and says the vault survived it", /vault/i.test(wiped.message), wiped.message)
eq("nothing to remove is still a success",
Model.parsePluginDataRemoval(0, "removed").ok, true)
check("and says so plainly",
/nothing/i.test(Model.parsePluginDataRemoval(0, "removed").message),
Model.parsePluginDataRemoval(0, "removed").message)
eq("a missing HOME is a failure, not a silent no-op",
Model.parsePluginDataRemoval(3, "").ok, false)
eq("and so is any other non-zero exit", Model.parsePluginDataRemoval(1, "").ok, false)
check("removing plugin data is confirmed before it happens",
/pluginDataConfirmPending = true[\s\S]{0,200}?return/.test(panelSrc),
"the first click wipes stored data with no confirmation")
check("and a cleared keyring is not still believed to hold a password",
/parsePluginDataRemoval[\s\S]{0,400}?fingerprintStored = false/.test(panelSrc),
"the panel still thinks a deleted master password is stored")
// A pid is noise on a prompt: it is gone by the time anyone could look it up,
// and it is deliberately not part of what a grant matches on -- so showing it
// implies a scope the approval does not have.
for (const [what, src] of [["the approval prompt", approvalSrc], ["the settings screen", settingsSrc]])
check(`${what} does not show a pid`, !/\bpid\b/.test(src), `${what} still renders a pid`)
check("the unlock prompt does not either", !/sshUnlockRequest\.pid/.test(panelSrc),
"the unlock prompt still renders a pid")
check("a development helper is called out wherever the user is",
/sshAgentHelper\.source === "development"[\s\S]{0,900}?sshAgentDevelopmentHelperWarning\(/.test(panelSrc),
"nothing warns that an unverified helper is serving keys")
check("the diagnostics say which helper is running and whether it was verified",
/helperSource: root\.sshAgentHelper\.source[\s\S]{0,200}?helperChecksum: root\.sshAgentHelper\.checksum/.test(panelSrc),
"sshAgentStatus cannot tell a shipped helper from a local build")
check("and what the panel believes about client routing",
/routingFragment: root\.uwsmFragment\.state[\s\S]{0,200}?routingNotice: root\.sshRoutingNotice\.text !== ""/.test(panelSrc),
"routing state cannot be read without squinting at the panel")
check("and why inspection rejected one, which errorCode never carries",
/helperState: root\.sshAgentHelper\.state/.test(panelSrc),
"sshAgentStatus reports an error state without naming it")
check("a running cooldown can be ended from the panel",
/function resumeSshSigning\(\)[\s\S]{0,300}?sshAgentCooldownAfter\(root\.sshCooldown, "resumed"/.test(panelSrc),
"nothing ends the cooldown early, so it cannot be escaped")
check("and the control that does it sits on the banner explaining the outage",
/sshCooldownStatus\.active[\s\S]{0,1600}?resumeSshSigning\(\)/.test(panelSrc),
"the resume control is not on the cooldown banner")
check("repeated denials enter the cooldown",
/sshAgentCooldownAfter\(/.test(panelSrc) && /sshAgentCooldownActive\(/.test(panelSrc),
"the cooldown is not applied")
check("escape denies rather than silently dismissing",
/sshApproval[\s\S]{0,900}?denySshRequest\(/.test(panelSrc), "escape does not deny")
check("the key name is rendered literally by a PlainText control",
/Text\s*\{[\s\S]{0,180}?textFormat:\s*Text\.PlainText[\s\S]{0,180}?(?:root|panel)\.sshPrompt\.keyName(?![A-Za-z0-9_])/
.test(panelSrc),
"the key name is not pinned to plain text")
// -------------------------------------------------------------------------
// Opt-in centered approval surface
// -------------------------------------------------------------------------
check("the panel reads the centered popup setting",
/readonly property bool sshAgentApprovalPopup:[^\n]*boolSetting\("sshAgentApprovalPopup"/.test(panelSrc),
"sshAgentApprovalPopup never reaches Panel.qml")
check("the popup is an overlay layer surface centered independently of the bar",
/PanelWindow\s*\{/.test(popupSrc)
&& /anchors\s*\{\s*top:\s*true\s*bottom:\s*true\s*left:\s*true\s*right:\s*true/.test(popupSrc)
&& /WlrLayer\.Overlay/.test(popupSrc)
&& /ExclusionMode\.Ignore/.test(popupSrc)
&& /namespace:\s*"qs-bitwarden-ssh-approval"/.test(popupSrc),
"the popup is not a full-screen, non-exclusive overlay layer surface")
check("the popup follows the panel's monitor",
/screen:[^\n]*anchorItem\.QsWindow\.window\.screen/.test(popupSrc),
"the popup has no screen affinity")
check("the popup exists only for opted-in pending SSH work",
/sshAgentApprovalPopup\s*&&\s*\(panel\.sshPrompt !== null \|\| panel\.sshUnlockRequest !== null\)/.test(popupSrc),
"the popup is not gated by both the setting and a pending request")
check("popup mode leaves the anchored panel closed for approvals",
/function showSshApproval\(message\)[\s\S]{0,900}?if \(root\.sshAgentApprovalPopup\)[\s\S]{0,240}?return/.test(panelSrc),
"showSshApproval always opens the panel")
check("popup mode leaves the anchored panel closed for unlock requests",
/message\.type === "unlock_required"[\s\S]{0,1400}?if \(root\.sshAgentApprovalPopup\)[\s\S]{0,240}?return/.test(panelSrc),
"unlock_required always opens the panel")
check("changing presentation mode cannot strand a live request off-screen",
/onSshAgentApprovalPopupChanged:[\s\S]{0,900}?sshPrompt \|\| root\.sshUnlockRequest[\s\S]{0,900}?root\.open\(\)/.test(panelSrc),
"turning popup mode off during a request leaves no visible approval surface")
check("the popup moves from unlock to approval without changing windows",
/SshUnlockScreen\s*\{/.test(popupSrc) && /SshApprovalScreen\s*\{/.test(popupSrc),
"unlock and approval are not hosted by one popup")
check("the popup unlock screen names the prerequisite",
/vault needs to be unlocked first/i.test(unlockSrc),
"the popup does not explain why it appeared")
check("the popup can submit every configured unlock method",
/unlockVault\(/.test(unlockSrc)
&& /submitPinUnlock\(/.test(unlockSrc)
&& /startFingerprintUnlock\(/.test(unlockSrc),
"password, PIN, or fingerprint is missing from the popup")
check("background click and Escape explicitly deny the pending request",
/MouseArea[\s\S]{0,500}?onClicked:\s*popup\.panel\.denySshRequest\(\)/.test(popupSrc)
&& /Qt\.Key_Escape[\s\S]{0,160}?denySshRequest\(\)/.test(popupSrc),
"the modal can disappear without answering the helper")
check("approval defaults keyboard focus to Deny",
/id:\s*denyButton/.test(approvalSrc)
&& /function focusDefault\(\)[\s\S]{0,120}?denyButton\.forceActiveFocus\(\)/.test(approvalSrc),
"an approval can receive accidental affirmative focus")
check("hidden panel fields cannot steal focus from the popup",
/function focusAppropriateField\(\)[\s\S]{0,120}?if \(sshApprovalPopupOpen\) return/.test(panelSrc),
"status and unlock handlers can focus an input in the closed panel")
check("approval actions opt into keyboard focus",
(approvalSrc.match(/focusable:\s*true/g) || []).length >= 2,
"approval buttons cannot be reached by Tab")
check("popup unlock is accepted while the anchored panel is closed",
/readonly property bool sshAuthSurfaceActive:\s*opened \|\| sshApprovalPopupOpen/.test(panelSrc)
&& /function prepareUnlock\(\)[\s\S]{0,180}?!sshAuthSurfaceActive/.test(panelSrc),
"unlock handlers still require root.opened")
check("closing the transient popup clears authentication state",
/function clearSshPopupUnlockState\(\)[\s\S]{0,700}?masterPassword = ""/.test(panelSrc)
&& /function dismissSshApproval\(\)[\s\S]{0,900}?clearSshPopupUnlockState\(\)/.test(panelSrc),
"password/PIN state can survive a dismissed popup")
// -------------------------------------------------------------------------
// Concurrent request queueing
// -------------------------------------------------------------------------
let q = []
q = Model.sshAgentEnqueuePrompt(q, { requestId: 1, keyName: "key1" }, 4)
eq("enqueues first item", q.length, 1)
q = Model.sshAgentEnqueuePrompt(q, { requestId: 2, keyName: "key2" }, 4)
eq("enqueues second item", q.length, 2)
q = Model.sshAgentEnqueuePrompt(q, { requestId: 2, keyName: "key2" }, 4)
eq("ignores duplicate requestId", q.length, 2)
q = Model.sshAgentEnqueuePrompt(q, { requestId: 3 }, 4)
q = Model.sshAgentEnqueuePrompt(q, { requestId: 4 }, 4)
q = Model.sshAgentEnqueuePrompt(q, { requestId: 5 }, 4)
eq("respects queue capacity cap", q.length, 4)
eq("pending count includes active and queued", Model.sshAgentPendingCount({ requestId: 0 }, q), 5)
eq("pending count with no active", Model.sshAgentPendingCount(null, q), 4)
eq("pending count with no queue", Model.sshAgentPendingCount({ requestId: 0 }, []), 1)
let deq = Model.sshAgentDequeuePrompt(q)
eq("dequeues first item", deq.next.requestId, 1)
eq("remaining queue length is decremented", deq.remaining.length, 3)
let emptyDeq = Model.sshAgentDequeuePrompt([])
eq("empty dequeue next is null", emptyDeq.next, null)
eq("empty dequeue remaining is empty", emptyDeq.remaining.length, 0)
let removed = Model.sshAgentRemovePrompt(q, 3)
eq("removes targeted requestId", removed.length, 3)
eq("requestId 3 is absent", removed.some(x => x.requestId === 3), false)
check("the panel declares an SSH prompt queue",
/property var sshPromptQueue:\s*\[\]/.test(panelSrc),
"sshPromptQueue is missing from Panel.qml")
check("the panel declares total pending counts",
/readonly property int sshPendingCount:/.test(panelSrc)
&& /readonly property int sshUnlockPendingCount:/.test(panelSrc),
"sshPendingCount or sshUnlockPendingCount is missing")
check("multiple concurrent approval requests are queued",
/root\.sshPromptQueue = Model\.sshAgentEnqueuePrompt\(root\.sshPromptQueue, message, 4\)/.test(panelSrc),
"concurrent approvals are not queued")
check("multiple concurrent unlock requests are queued",
/root\.sshUnlockQueue = Model\.sshAgentEnqueuePrompt\(root\.sshUnlockQueue, message, 4\)/.test(panelSrc),
"concurrent unlocks are not queued")
check("advancing an approval dequeues the next prompt",
/function advanceSshPrompt\(\)[\s\S]{0,400}?Model\.sshAgentDequeuePrompt\(root\.sshPromptQueue\)/.test(panelSrc),
"advanceSshPrompt does not dequeue from sshPromptQueue")
check("advancing an unlock dequeues the next unlock request",
/function advanceSshUnlock\(\)[\s\S]{0,400}?Model\.sshAgentDequeuePrompt\(root\.sshUnlockQueue\)/.test(panelSrc),
"advanceSshUnlock does not dequeue from sshUnlockQueue")
check("deny all rejects active and queued requests",
/function denyAllSshRequests\(\)[\s\S]{0,900}?sshPromptQueue[\s\S]{0,600}?sshUnlockQueue[\s\S]{0,600}?dismissSshApproval\(\)/.test(panelSrc),
"denyAllSshRequests is missing or does not clear queues")
check("approval screen displays 1 of N when multiple requests are queued",
/text:\s*"1 of "\s*\+\s*panel\.sshPendingCount/.test(approvalSrc),
"approval screen does not display queue counter")
check("approval screen provides a Deny all button when multiple requests exist",
/text:\s*"Deny all \("\s*\+\s*panel\.sshPendingCount\s*\+\s*"\)"/.test(approvalSrc),
"approval screen is missing Deny all button")
check("popup accepts Shift+Escape to deny all requests",
/event\.modifiers\s*&\s*Qt\.ShiftModifier[\s\S]{0,120}?popup\.panel\.denyAllSshRequests\(\)/.test(popupSrc),
"popup does not handle Shift+Escape for deny all")
if (failures.length) {
console.error(`\n${failures.length} failed, ${pass} passed\n`)
failures.forEach(f => console.error(` FAIL ${f}`))
process.exit(1)
}
console.log(`ssh-agent-ui: ${pass} passed`)